feat: add wasm support

This commit is contained in:
Anna 2021-08-12 16:13:25 -04:00
parent acfae81284
commit f61747d0e2
6 changed files with 215 additions and 326 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
/target
/docs/*.js
/docs/*.wasm

296
Cargo.lock generated
View File

@ -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"

View File

@ -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

79
docs/index.html Executable file
View File

@ -0,0 +1,79 @@
<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- Disable zooming: -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<head>
<title>Twitch Run Highlighter</title>
<style>
html {
/* Remove touch delay: */
touch-action: manipulation;
}
body {
/* Background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #404040;
}
/* Allow canvas to fill entire web page: */
html,
body {
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
}
/* Position canvas in center-top: */
canvas {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 0%;
left: 50%;
transform: translate(-50%, 0%);
}
</style>
</head>
<body>
<!-- The WASM code will resize this canvas to cover the entire screen -->
<canvas id="the_canvas_id"></canvas>
<script>
// The `--no-modules`-generated JS from `wasm-bindgen` attempts to use
// `WebAssembly.instantiateStreaming` to instantiate the wasm module,
// but this doesn't work with `file://` urls. This example is frequently
// viewed by simply opening `index.html` in a browser (with a `file://`
// url), so it would fail if we were to call this function!
//
// Work around this for now by deleting the function to ensure that the
// `no_modules.js` script doesn't have access to it. You won't need this
// hack when deploying over HTTP.
delete WebAssembly.instantiateStreaming;
</script>
<!-- This is the JS generated by the `wasm-bindgen` CLI tool -->
<script src="runhighlighter.js"></script>
<script>
// We'll defer our execution until the wasm is ready to go.
// Here we tell bindgen the path to the wasm file so it can start
// initialization and return to us a promise when it's done.
wasm_bindgen("./runhighlighter_bg.wasm")
.then(on_wasm_loaded)["catch"](console.error);
function on_wasm_loaded() {
// This call installs a bunch of callbacks and then returns.
wasm_bindgen.start("the_canvas_id");
}
</script>
</body>
</html>
<!-- Powered by egui: https://github.com/emilk/egui/ -->

View File

@ -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<Run> = 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<Message>, repaint_signal: Arc<dyn RepaintSignal>) {
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<F>(&self, fut: F) where F: std::future::Future<Output=()> + Send + 'static {
tokio::task::spawn(fut);
}
#[cfg(target_arch = "wasm32")]
fn spawn<F>(&self, fut: F) where F: std::future::Future<Output=()> + '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<S, D>(method: &str, url: S, headers: &[(String, String)]) -> Result<D>
where S: AsRef<str> + 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<S, D>(method: &str, url: S, headers: &[(String, String)]) -> Result<D>
where S: AsRef<str>,
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::<web_sys::Response>());
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<Vec<VideosResponseVideo>> {
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)
}

View File

@ -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))
}