diff --git a/.gitignore b/.gitignore index fedaa2b..bc97e80 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target .env +dist/ diff --git a/Cargo.lock b/Cargo.lock index d97ac9d..1011435 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -372,7 +372,7 @@ dependencies = [ [[package]] name = "cool_spotify_server" -version = "0.1.0" +version = "0.1.1" dependencies = [ "actix-web", "dotenvy", @@ -620,6 +620,17 @@ dependencies = [ [[package]] name = "front" version = "0.1.0" +dependencies = [ + "gloo-net", + "js-sys", + "log", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-logger", + "web-sys", +] [[package]] name = "futures-channel" @@ -694,6 +705,40 @@ dependencies = [ "wasi", ] +[[package]] +name = "gloo-net" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3000ef231a67d5bfee6b35f2c0f6f5c8d45b3381ef5bbbea603690ec4e539762" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "h2" version = "0.3.20" @@ -1268,6 +1313,26 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "pin-project" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.22", +] + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -2093,6 +2158,17 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +[[package]] +name = "wasm-logger" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074649a66bb306c8f2068c9016395fa65d8e08d2affcbf95acf3c24c3ab19718" +dependencies = [ + "log", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.64" diff --git a/front/Cargo.toml b/front/Cargo.toml index adb5b38..646d084 100644 --- a/front/Cargo.toml +++ b/front/Cargo.toml @@ -6,3 +6,27 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +wasm-bindgen = "0.2.74" +js-sys = "0.3.51" +wasm-bindgen-futures = "0.4.24" + +gloo-net = { version = "0.3", features = ["http"] } + +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +log = "0.4" +wasm-logger = "0.2" + +[dependencies.web-sys] +version = "0.3.4" +features = [ + 'Window', + 'Document', + 'Element', + 'HtmlElement', + 'Node', + 'HtmlAnchorElement', + 'HtmlImageElement', +] + diff --git a/front/assets/scss/index.scss b/front/assets/scss/index.scss new file mode 100644 index 0000000..895a931 --- /dev/null +++ b/front/assets/scss/index.scss @@ -0,0 +1,86 @@ +@use "sass:math"; + +$card-height: 8vh; +$card-gap: 1.5vh; + +body { + box-sizing: border-box; +} + +a { + text-decoration: none; + font-family: sans-serif; + color: black; +} + +img { +} + +.outer-div { + display: flex; + flex-direction: column; +} + +.inner-div { + height: $card-height; + margin-top: ($card-gap * 0.5); + margin-bottom: ($card-gap * 0.5); + + display: flex; + justify-content: space-between; + + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); + + &:nth-child(odd) { + background: #e0e0e0; + } +} + +.track-info { + height: 100%; + width: 70%; + + display: grid; + grid-template-columns: ($card-height * 1.1) 1fr; + grid-template-rows: 1fr 1fr; + align-items: center; + + img { + height: $card-height; + grid-column: 1 / 2; + grid-row: 1 / span 2; + } + p { + margin: 0; + padding: 0; + } + + .track_name { + grid-column: 2 / span 1; + grid-row: 1 / span 1; + } + .track_album { + grid-column: 2 / span 1; + grid-row: 2 / span 1; + } +} + +.user-info { + height: 100%; + width: 30%; + + display: flex; + justify-content: flex-end; + align-items: center; + + img { + height: $card-height; + } + p { + margin: 0; + margin-right: ($card-height * 0.1); + padding: 0; + + text-align: right; + } +} diff --git a/front/index.html b/front/index.html new file mode 100644 index 0000000..9f50d24 --- /dev/null +++ b/front/index.html @@ -0,0 +1,8 @@ + + + + + + Cool Spotify Blend + + diff --git a/front/src/fetch.rs b/front/src/fetch.rs new file mode 100644 index 0000000..8d80aef --- /dev/null +++ b/front/src/fetch.rs @@ -0,0 +1,11 @@ +use gloo_net::http::Request; + +use crate::spotify_types::*; + +pub async fn fetch() -> Result, gloo_net::Error> { + Ok(Request::get("https://spotify.api.ilia.moe/status") + .send() + .await? + .json() + .await?) +} diff --git a/front/src/main.rs b/front/src/main.rs index e7a11a9..4ad3234 100644 --- a/front/src/main.rs +++ b/front/src/main.rs @@ -1,3 +1,144 @@ +mod fetch; +mod spotify_types; // Symlinked from server because I do not want to separate this into another + // crate right now. + // This is a stupid way to do this. + +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::spawn_local; +use web_sys::{Document, HtmlAnchorElement, HtmlImageElement}; + +use crate::spotify_types::*; + +// Convenience alias +type TrackUserTuple = (TrackObject, UserProfile); + fn main() { - println!("Hello, world!"); + wasm_logger::init(wasm_logger::Config::default()); + + log::info!("wasm executing now :)"); + spawn_local(app()); +} + +async fn app() { + let window = web_sys::window().expect("We have to have a window."); + let document = window.document().expect("Window should have a document."); + let body = document.body().expect("Document should have a body."); + + let outer_div = generate_outer_div(&document).unwrap(); + body.append_child(&outer_div).unwrap(); + + let data = fetch::fetch().await; + if data.is_err() { + log::error!("Could not fetch data, nothing to do."); + log::error!("{:?}", data.unwrap_err()); + return (); // This is actually a graceful exit! + } + let data = data.unwrap(); // We know this is fine from above + let rows = data.iter() + .map(|row| generate_row(&document, &row)) + .flatten(); // Intentionally not collecting--next step is to consume. + for row in rows { + outer_div.append_child(&row).unwrap(); + } + +} + +fn generate_outer_div(document: &Document) -> Result { + let outer_div = document.create_element("div")?; + outer_div.set_class_name("outer-div"); + Ok(outer_div) +} + +fn generate_row(document: &Document, data: &TrackUserTuple) -> Result { + let container = document.create_element("div")?; + container.set_class_name("inner-div"); + + let track_info = generate_track_info(document, data)?; + let user_info = generate_user_info(document, data)?; + + container.append_child(&track_info).unwrap(); + container.append_child(&user_info).unwrap(); + + Ok(container) +} + +fn generate_track_info(document: &Document, data: &TrackUserTuple) -> Result { + let track_info = document.create_element("div")?; + track_info.set_class_name("track-info"); + + // Scoping these to prevent accidentally referencing the wrong thing + // and to free memory sooner maybe + { + let track_name = document.create_element("p")?; + track_name.set_class_name("track_name"); + let track_name_a: HtmlAnchorElement = document.create_element("a")?.dyn_into().unwrap(); + track_name_a.set_text_content(Some(&data.0.name)); + track_name_a.set_href(&create_track_link(&data.0.uri)); + track_name.append_child(&track_name_a).unwrap(); + track_info.append_child(&track_name).unwrap(); + } + + if let Some(album) = &data.0.album { + { + let track_album = document.create_element("p")?; + track_album.set_class_name("track_album"); + let track_album_a: HtmlAnchorElement = document.create_element("a")?.dyn_into().unwrap(); + track_album_a.set_text_content(Some(&album.name)); + track_album_a.set_href(&create_album_link(&album.uri)); + + track_album.append_child(&track_album_a).unwrap(); + + track_info.append_child(&track_album).unwrap(); + } + if let Some(image) = &album.images.get(0) { + let track_album_img: HtmlImageElement = document.create_element("img")?.dyn_into().unwrap(); + track_album_img.set_alt("Album cover"); + track_album_img.set_src(&image.url); + + track_info.append_child(&track_album_img).unwrap(); + } + } + + Ok(track_info) +} + +fn generate_user_info(document: &Document, data: &TrackUserTuple) -> Result { + let user_info = document.create_element("div")?; + user_info.set_class_name("user-info"); + + { + let user_name = document.create_element("p")?; + user_name.set_class_name("user_name"); + + let user_name_a: HtmlAnchorElement = document.create_element("a")?.dyn_into().unwrap(); + if let Some(display_name) = &data.1.display_name { + user_name_a.set_text_content(Some(&display_name)); + } else { + user_name_a.set_text_content(Some(&data.1.id)); + } + user_name_a.set_href(&create_user_link(&data.1.id)); + user_name.append_child(&user_name_a).unwrap(); + + user_info.append_child(&user_name).unwrap(); + } + + if let Some(Some(image)) = data.1.images.as_ref().map(|imgs| imgs.get(0)) { + let user_img: HtmlImageElement = document.create_element("img")?.dyn_into().unwrap(); + user_img.set_alt("Profile picture"); + user_img.set_src(&image.url); + + user_info.append_child(&user_img).unwrap(); + } + + Ok(user_info) +} + +fn create_track_link(uri: &Uri) -> String { + "https://open.spotify.com/track/".to_owned() + &uri.get_suffix() +} +fn create_album_link(uri: &Uri) -> String { + "https://open.spotify.com/album/".to_owned() + &uri.get_suffix() +} +fn create_user_link(id: &str) -> String { + "https://open.spotify.com/user/".to_owned() + id } diff --git a/front/src/spotify_types.rs b/front/src/spotify_types.rs new file mode 120000 index 0000000..2620b47 --- /dev/null +++ b/front/src/spotify_types.rs @@ -0,0 +1 @@ +../../server/src/spotify_types.rs \ No newline at end of file diff --git a/server/src/spotify_types.rs b/server/src/spotify_types.rs index b903faa..d42f16f 100644 --- a/server/src/spotify_types.rs +++ b/server/src/spotify_types.rs @@ -52,9 +52,9 @@ pub struct SpotifyId(pub String); #[derive(Deserialize, Serialize, Debug, Clone)] #[non_exhaustive] pub struct UserProfile { - id: String, - display_name: Option, - images: Option>, + pub id: String, + pub display_name: Option, + pub images: Option>, } #[derive(Deserialize, Serialize, Debug, Clone)]