diff --git a/.gitignore b/.gitignore index ea8c4bf..49e0abb 100755 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +/docs/*.js +/docs/*.wasm diff --git a/Cargo.lock b/Cargo.lock index 168f37f..3e7e83c 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,12 +150,6 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" -[[package]] -name = "bytes" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" - [[package]] name = "cairo-sys-rs" version = "0.14.0" @@ -273,6 +267,16 @@ dependencies = [ "objc", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8d976903543e0c48546a91908f21588a680a8c8f984df9a5d69feccb2b2a211" +dependencies = [ + "cfg-if 0.1.10", + "wasm-bindgen", +] + [[package]] name = "copypasta" version = "0.7.1" @@ -605,15 +609,6 @@ dependencies = [ "serde", ] -[[package]] -name = "encoding_rs" -version = "0.8.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" -dependencies = [ - "cfg-if 1.0.0", -] - [[package]] name = "epaint" version = "0.13.0" @@ -968,31 +963,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "h2" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "825343c4eef0b63f541f8903f395dc5beb362a979b5799a84062527ef1e37726" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" - [[package]] name = "heck" version = "0.3.3" @@ -1011,79 +981,6 @@ dependencies = [ "libc", ] -[[package]] -name = "http" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5" -dependencies = [ - "bytes", - "http", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" - -[[package]] -name = "httpdate" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" - -[[package]] -name = "hyper" -version = "0.14.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b61cf2d1aebcf6e6352c97b81dc2244ca29194be1b276f5d8ad5c6330fffb11" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64" -dependencies = [ - "futures-util", - "hyper", - "log", - "rustls", - "tokio", - "tokio-rustls", - "webpki", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -1101,16 +998,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "indexmap" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" -dependencies = [ - "autocfg", - "hashbrown", -] - [[package]] name = "instant" version = "0.1.10" @@ -1120,12 +1007,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "ipnet" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" - [[package]] name = "itertools" version = "0.10.1" @@ -1294,12 +1175,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "mime" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" - [[package]] name = "miniz_oxide" version = "0.4.4" @@ -1720,42 +1595,6 @@ dependencies = [ "redox_syscall", ] -[[package]] -name = "reqwest" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "http", - "http-body", - "hyper", - "hyper-rustls", - "ipnet", - "js-sys", - "lazy_static", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "rustls", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-rustls", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", - "winreg", -] - [[package]] name = "rfd" version = "0.4.2" @@ -1811,18 +1650,24 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", + "console_error_panic_hook", "eframe", "futures", "itertools", + "js-sys", "livesplit", "nom", "quick-xml", - "reqwest", "rfd", "serde", + "serde_json", "tokio", "toml", + "ureq", "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] @@ -1922,18 +1767,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "shared_library" version = "0.1.9" @@ -2002,16 +1835,6 @@ dependencies = [ "wayland-client", ] -[[package]] -name = "socket2" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "spin" version = "0.5.2" @@ -2141,38 +1964,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b7b349f11a7047e6d1276853e612d152f5e8a352c61917887cc2169e2366b4c" dependencies = [ "autocfg", - "bytes", - "libc", - "memchr", - "mio", "num_cpus", "pin-project-lite", - "winapi", -] - -[[package]] -name = "tokio-rustls" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" -dependencies = [ - "rustls", - "tokio", - "webpki", -] - -[[package]] -name = "tokio-util" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "log", - "pin-project-lite", - "tokio", ] [[package]] @@ -2184,38 +1977,6 @@ dependencies = [ "serde", ] -[[package]] -name = "tower-service" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" - -[[package]] -name = "tracing" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" -dependencies = [ - "cfg-if 1.0.0", - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "try-lock" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" - [[package]] name = "ttf-parser" version = "0.6.2" @@ -2275,6 +2036,8 @@ dependencies = [ "log", "once_cell", "rustls", + "serde", + "serde_json", "url", "webpki", "webpki-roots", @@ -2315,16 +2078,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "want" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" -dependencies = [ - "log", - "try-lock", -] - [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" @@ -2338,8 +2091,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b608ecc8f4198fe8680e2ed18eccab5f0cd4caaf3d83516fa5fb2e927fda2586" dependencies = [ "cfg-if 1.0.0", - "serde", - "serde_json", "wasm-bindgen-macro", ] @@ -2591,15 +2342,6 @@ dependencies = [ "x11-dl", ] -[[package]] -name = "winreg" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" -dependencies = [ - "winapi", -] - [[package]] name = "wyz" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index d987e77..f8b3cea 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ anyhow = "1" eframe = "0.13" chrono = { version = "0.4", features = ["serde"] } livesplit = { path = "../livesplit-rs" } -quick-xml = { version = "0.21", features = [ "serialize" ] } +quick-xml = { version = "0.21", features = ["serialize"] } nom = "6" url = "2" toml = "0.5" @@ -22,20 +22,28 @@ futures = "0.3" itertools = "0.10" rfd = "0.4" -[dependencies.reqwest] -version = "0.11" +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.ureq] +version = "2" default-features = false -features = ["json", "rustls-tls-webpki-roots"] +features = ["json", "tls"] [dependencies.serde] version = "1" features = ["derive"] -[dependencies.tokio] +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio] version = "1" default-features = false features = ["rt-multi-thread"] +[target.'cfg(target_arch = "wasm32")'.dependencies] +console_error_panic_hook = "0.1" +wasm-bindgen = { version = "0.2" } +wasm-bindgen-futures = "0.4" +serde_json = "1" +web-sys = { version = "0.3", features = ["Headers", "Response", "Request", "RequestInit", "RequestMode", "Window"] } +js-sys = "0.3" + [features] default = ["persistence"] http = ["eframe/http"] # Enable if you want to do http requests diff --git a/docs/index.html b/docs/index.html new file mode 100755 index 0000000..d693038 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,79 @@ + + + + + + + + + Twitch Run Highlighter + + + + + + + + + + + + + + + + + + diff --git a/src/app.rs b/src/app.rs index ece2d56..656d585 100755 --- a/src/app.rs +++ b/src/app.rs @@ -3,7 +3,9 @@ use chrono::{Duration, Local, Utc, TimeZone, FixedOffset, DateTime}; use std::{ cell::RefCell, collections::HashMap, + io::Cursor, sync::{ + Arc, atomic::{AtomicBool, Ordering}, mpsc::{SyncSender, Receiver}, }, @@ -17,11 +19,10 @@ use url::Url; use eframe::{ egui, epi, egui::{CtxRef, Sense, Rgba, Widget}, - epi::{Frame, Storage}, + epi::{Frame, RepaintSignal, Storage}, }; use serde::Deserialize; use itertools::Itertools; -use std::io::Cursor; #[derive(Default)] #[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] @@ -123,7 +124,7 @@ impl epi::App for App { let repaint_signal = frame.repaint_signal(); let sender = self.state.channel.0.clone(); - tokio::task::spawn(async move { + self.spawn(async move { let res: Result = try { let bytes = Cursor::new(data); let run: Run = quick_xml::de::from_reader(bytes)?; @@ -210,22 +211,7 @@ impl epi::App for App { let repaint_signal = frame.repaint_signal(); let sender = self.state.channel.0.clone(); - tokio::task::spawn(async move { - let handle = rfd::AsyncFileDialog::default() - .add_filter("LiveSplit splits", &["lss"]) - .pick_file() - .await; - let data = match handle { - Some(handle) => { - let name = handle.file_name(); - let data = handle.read().await; - Some((name, data)) - }, - None => None, - }; - sender.send(Message::SplitsPath(data)).ok(); - repaint_signal.request_repaint(); - }); + self.spawn(Self::choose_splits(sender, repaint_signal)); } if let Some(name) = &self.state.splits_file_name { @@ -294,7 +280,7 @@ impl epi::App for App { let repaint_signal = frame.repaint_signal(); let client_id = self.config.client.id.clone(); let client_secret = self.config.client.secret.clone(); - tokio::task::spawn(async move { + self.spawn(async move { let res = get_videos(&channel.unwrap(), client_id, client_secret).await; sender.send(Message::Videos(id, res)).ok(); repaint_signal.request_repaint(); @@ -319,7 +305,8 @@ impl epi::App for App { #[cfg(feature = "persistence")] fn setup(&mut self, _ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>, storage: Option<&dyn epi::Storage>) { if let Some(storage) = storage { - *self = epi::get_value(storage, epi::APP_KEY).unwrap_or_default(); + let loaded: Self = epi::get_value(storage, epi::APP_KEY).unwrap_or_default(); + self.config = loaded.config; } } @@ -388,6 +375,90 @@ impl App { .append_pair("title", &format!("{} PB in {}", splits.game_name, to_string(&run_time))); self.state.highlight_urls.borrow_mut().insert(attempt.id, url); } + + async fn choose_splits(sender: SyncSender, repaint_signal: Arc) { + let handle = rfd::AsyncFileDialog::default() + .add_filter("LiveSplit splits", &["lss"]) + .pick_file() + .await; + let data = match handle { + Some(handle) => { + let name = handle.file_name(); + let data = handle.read().await; + Some((name, data)) + } + None => None, + }; + + sender.send(Message::SplitsPath(data)).ok(); + repaint_signal.request_repaint(); + } + + #[cfg(not(target_arch = "wasm32"))] + fn spawn(&self, fut: F) where F: std::future::Future + Send + 'static { + tokio::task::spawn(fut); + } + + #[cfg(target_arch = "wasm32")] + fn spawn(&self, fut: F) where F: std::future::Future + 'static { + use futures::FutureExt; + let _ = wasm_bindgen_futures::future_to_promise(fut.then(|()| async { Ok(wasm_bindgen::JsValue::undefined()) })); + } + + #[cfg(not(target_arch = "wasm32"))] + async fn send_request(method: &str, url: S, headers: &[(String, String)]) -> Result + where S: AsRef + Send + Sync, + D: serde::de::DeserializeOwned + Send + 'static, + { + let method = method.to_string(); + let url = url.as_ref().to_string(); + let headers = headers.to_vec(); + tokio::task::spawn_blocking(move || { + let mut req = ureq::request(&method, &url); + for (name, value) in headers { + req = req.set(&name, &value); + } + let d: D = req.call()?.into_json()?; + Ok(d) + }).await.unwrap() + } + + #[cfg(target_arch = "wasm32")] + async fn send_request(method: &str, url: S, headers: &[(String, String)]) -> Result + where S: AsRef, + D: serde::de::DeserializeOwned, + { + use wasm_bindgen::JsCast; + use wasm_bindgen_futures::JsFuture; + + let mut opts = web_sys::RequestInit::new(); + opts.method(method); + opts.mode(web_sys::RequestMode::Cors); + + let request = web_sys::Request::new_with_str_and_init(url.as_ref(), &opts) + .map_err(|e| anyhow::anyhow!("couldn't make request: {:?}", e))?; + request.headers().set("Accept", "*/*").map_err(|e| anyhow::anyhow!("couldn't make request: {:?}", e))?; + + for (name, value) in headers { + request.headers().set(name, value).map_err(|e| anyhow::anyhow!("couldn't make request: {:?}", e))?; + } + + let window = web_sys::window().unwrap(); + let response = JsFuture::from(window.fetch_with_request(&request)).await + .map_err(|e| anyhow::anyhow!("couldn't make request: {:?}", e))?; + assert!(response.is_instance_of::()); + let response: web_sys::Response = response.dyn_into().unwrap(); + + let array_buffer = response.array_buffer() + .map_err(|e| anyhow::anyhow!("couldn't make request: {:?}", e))?; + let array_buffer = JsFuture::from(array_buffer).await + .map_err(|e| anyhow::anyhow!("couldn't make request: {:?}", e))?; + let uint8_array = js_sys::Uint8Array::new(&array_buffer); + let bytes = uint8_array.to_vec(); + + let d: D = serde_json::from_slice(&bytes)?; + Ok(d) + } } #[derive(Deserialize)] @@ -420,30 +491,22 @@ struct VideosResponseVideo { } async fn get_videos(channel: &str, client_id: String, client_secret: String) -> Result> { - let client = reqwest::Client::new(); - let mut token_url = Url::parse("https://id.twitch.tv/oauth2/token").unwrap(); token_url.query_pairs_mut() .append_pair("client_id", &client_id) .append_pair("client_secret", &client_secret) .append_pair("grant_type", "client_credentials"); - let creds: AppCredentials = client.post(token_url) - .send() - .await? - .json() - .await?; + let creds: AppCredentials = App::send_request("POST", token_url, &[]).await?; + + let headers = &[ + ("Client-ID".into(), client_id), + ("Authorization".into(), format!("Bearer {}", creds.access_token)), + ][..]; let mut user_url = Url::parse("https://api.twitch.tv/helix/users").unwrap(); user_url.query_pairs_mut() .append_pair("login", channel); - let users: UsersResponse = client.get(user_url) - .header("Client-ID", &client_id) - .header("Authorization", format!("Bearer {}", creds.access_token)) - .send() - .await? - .json() - .await?; - + let users: UsersResponse = App::send_request("GET", user_url, &headers).await?; if users.data.is_empty() { anyhow::bail!("no such twitch channel"); } @@ -453,13 +516,7 @@ async fn get_videos(channel: &str, client_id: String, client_secret: String) -> let mut vids_url = Url::parse("https://api.twitch.tv/helix/videos").unwrap(); vids_url.query_pairs_mut() .append_pair("user_id", &user.id); - let videos: VideosResponse = client.get(vids_url) - .header("Client-ID", &client_id) - .header("Authorization", format!("Bearer {}", creds.access_token)) - .send() - .await? - .json() - .await?; + let videos: VideosResponse = App::send_request("GET", vids_url, &headers).await?; Ok(videos.data) } diff --git a/src/lib.rs b/src/lib.rs index 6dadac4..0cc776f 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,8 @@ use eframe::wasm_bindgen::{self, prelude::*}; /// You can add more callbacks like this if you want to call in to your code. #[cfg(target_arch = "wasm32")] #[wasm_bindgen] -pub async fn start(canvas_id: &str) -> Result<(), eframe::wasm_bindgen::JsValue> { +pub fn start(canvas_id: &str) -> Result<(), eframe::wasm_bindgen::JsValue> { + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); let app = App::default(); eframe::start_web(canvas_id, Box::new(app)) }