implement add SVG by URL, prepare for dimensions support

master
Sameer Puri 4 years ago
parent 8a5c068738
commit 2a8ac774fd

3
Cargo.lock generated

@ -669,12 +669,15 @@ dependencies = [
"g-code", "g-code",
"gloo-file", "gloo-file",
"gloo-timers 0.1.0", "gloo-timers 0.1.0",
"js-sys",
"log", "log",
"paste", "paste",
"roxmltree", "roxmltree",
"serde", "serde",
"svg2gcode", "svg2gcode",
"svgtypes",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures",
"wasm-logger", "wasm-logger",
"web-sys", "web-sys",
"yew", "yew",

@ -18,6 +18,7 @@ codespan = "0.11"
serde = "1" serde = "1"
paste = "1" paste = "1"
log = "0" log = "0"
svgtypes = "0"
yew = { git = "https://github.com/yewstack/yew.git" } yew = { git = "https://github.com/yewstack/yew.git" }
yewdux-functional = { git = "https://github.com/intendednull/yewdux.git" } yewdux-functional = { git = "https://github.com/intendednull/yewdux.git" }
@ -28,3 +29,5 @@ wasm-logger = "0.2"
gloo-file = { version = "0.1", features = ["futures"] } gloo-file = { version = "0.1", features = ["futures"] }
gloo-timers = "0.1" gloo-timers = "0.1"
base64 = "0.13" base64 = "0.13"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"

@ -2,10 +2,14 @@ use codespan_reporting::term::{emit, termcolor::NoColor, Config};
use g_code::parse::{into_diagnostic, snippet_parser}; use g_code::parse::{into_diagnostic, snippet_parser};
use gloo_file::futures::read_as_text; use gloo_file::futures::read_as_text;
use gloo_timers::callback::Timeout; use gloo_timers::callback::Timeout;
use js_sys::TypeError;
use log::info;
use paste::paste; use paste::paste;
use roxmltree::Document; use roxmltree::Document;
use std::num::ParseFloatError; use std::num::ParseFloatError;
use web_sys::{FileList, HtmlElement}; use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{window, FileList, HtmlElement, Response};
use yew::prelude::*; use yew::prelude::*;
use yewdux::prelude::{BasicStore, Dispatcher}; use yewdux::prelude::{BasicStore, Dispatcher};
use yewdux_functional::use_store; use yewdux_functional::use_store;
@ -182,31 +186,31 @@ macro_rules! gcode_input {
let oninput = { let oninput = {
let timeout = timeout.clone(); let timeout = timeout.clone();
form.dispatch().input(move |state, value| { form.dispatch().input(move |state, value| {
let res = Some(match snippet_parser(&value) { let res = Some(match snippet_parser(&value) {
Ok(_) => Ok(value), Ok(_) => Ok(value),
Err(err) => { Err(err) => {
let mut buf = NoColor::new(vec![]); let mut buf = NoColor::new(vec![]);
let config = Config::default(); let config = Config::default();
emit( emit(
&mut buf, &mut buf,
&config, &config,
&codespan_reporting::files::SimpleFile::new("<input>", value), &codespan_reporting::files::SimpleFile::new("<input>", value),
&into_diagnostic(&err), &into_diagnostic(&err),
) )
.unwrap(); .unwrap();
Err(String::from_utf8_lossy(buf.get_ref().as_slice()).to_string()) Err(String::from_utf8_lossy(buf.get_ref().as_slice()).to_string())
} }
}).filter(|res| { }).filter(|res| {
!res.as_ref().ok().map(|value| value.is_empty()).unwrap_or(false) !res.as_ref().ok().map(|value| value.is_empty()).unwrap_or(false)
}); });
let timeout_inner = timeout.clone(); let timeout_inner = timeout.clone();
timeout.set(Some(Timeout::new(VALIDATION_TIMEOUT, move || { timeout.set(Some(Timeout::new(VALIDATION_TIMEOUT, move || {
timeout_inner.set(None); timeout_inner.set(None);
}))); })));
state.$accessor $([$idx])? = res; state.$accessor $([$idx])? = res;
}) })
}; };
html! { html! {
<FormGroup success={form.state().map(|state| (state.$accessor $([$idx])?).as_ref().map(Result::is_ok)).flatten()}> <FormGroup success={form.state().map(|state| (state.$accessor $([$idx])?).as_ref().map(Result::is_ok)).flatten()}>
<TextArea<String, String> label=$label desc=$desc <TextArea<String, String> label=$label desc=$desc
@ -441,68 +445,151 @@ pub fn settings_form() -> Html {
#[function_component(SvgInput)] #[function_component(SvgInput)]
pub fn svg_input() -> Html { pub fn svg_input() -> Html {
let app = use_store::<AppStore>(); let app = use_store::<AppStore>();
let parsed_state = use_ref(Vec::default);
let parsed_state_cloned = parsed_state.clone();
let onchange = app let file_upload_state = use_ref(Vec::default);
.dispatch() let file_upload_state_cloned = file_upload_state.clone();
.future_callback_with(move |app, file_list: FileList| { let file_upload_onchange =
let parsed_state_cloned = parsed_state_cloned.clone(); app.dispatch()
async move { .future_callback_with(move |app, file_list: FileList| {
let mut results = Vec::with_capacity(file_list.length() as usize); let file_upload_state_cloned = file_upload_state_cloned.clone();
for file in (0..file_list.length()).filter_map(|i| file_list.item(i)) { async move {
let filename = file.name(); let mut results = Vec::with_capacity(file_list.length() as usize);
results.push( for file in (0..file_list.length()).filter_map(|i| file_list.item(i)) {
read_as_text(&gloo_file::File::from(file)) let filename = file.name();
.await results.push(
.map_err(|err| err.to_string()) read_as_text(&gloo_file::File::from(file))
.and_then(|text| { .await
if let Some(err) = Document::parse(&text).err() { .map_err(|err| err.to_string())
Err(format!("Error parsing {}: {}", &filename, err)) .and_then(|text| {
} else { if let Some(err) = Document::parse(&text).err() {
Ok(Svg { Err(format!("Error parsing {}: {}", &filename, err))
content: text, } else {
filename, Ok(Svg {
}) content: text,
} filename,
}), dimensions: [None; 2],
); })
} }
app.reduce(move |app| { }),
// Clear any errors from previous entry, add new successfully parsed SVGs );
(*parsed_state_cloned).borrow_mut().clear();
for result in results.iter() {
(*parsed_state_cloned)
.borrow_mut()
.push(result.clone().map(|_| ()));
} }
app.svgs.extend(results.drain(..).filter_map(Result::ok)); app.reduce(move |app| {
}); // Clear any errors from previous entry, add new successfully parsed SVGs
} (*file_upload_state_cloned).borrow_mut().clear();
}); for result in results.iter() {
(*file_upload_state_cloned)
.borrow_mut()
.push(result.clone().map(|_| ()));
}
app.svgs.extend(results.drain(..).filter_map(Result::ok));
});
}
});
let errors = parsed_state let file_upload_errors = file_upload_state
.borrow() .borrow()
.iter() .iter()
.filter_map(|res| res.as_ref().err()) .filter_map(|res| res.as_ref().err())
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let res = if parsed_state.borrow().is_empty() { let file_upload_res = if file_upload_state.borrow().is_empty() {
None None
} else if errors.is_empty() { } else if file_upload_errors.is_empty() {
Some(Ok(())) Some(Ok(()))
} else { } else {
Some(Err(errors.join("\n"))) Some(Err(file_upload_errors.join("\n")))
};
let url_input_state = use_state(|| Option::<String>::None);
let url_input_parsed = use_state(|| Option::<Result<String, String>>::None);
let url_input_oninput = {
let url_input_state = url_input_state.clone();
let url_input_parsed = url_input_parsed.clone();
Callback::from(move |url: InputData| {
url_input_state.set(Some(url.value));
url_input_parsed.set(None);
})
}; };
let url_add_loading = use_state(|| false);
let url_add_onclick = {
let url_input_state = url_input_state.clone();
let url_input_parsed = url_input_parsed.clone();
let url_add_loading = url_add_loading.clone();
app.dispatch().future_callback_with(move |app, _| {
let url_input_state = url_input_state.clone();
let url_input_parsed = url_input_parsed.clone();
let url_add_loading = url_add_loading.clone();
url_add_loading.set(true);
let request_url = url_input_state.as_ref().unwrap().clone();
async move {
url_input_parsed.set(None);
let res = JsFuture::from(window().unwrap().fetch_with_str(&request_url))
.await
.map(|res| res.dyn_into::<Response>().unwrap());
url_add_loading.set(false);
match res {
Ok(res) => {
let response_url = res.url();
let text = JsFuture::from(res.text().unwrap())
.await
.unwrap()
.as_string()
.unwrap();
if let Some(err) = Document::parse(&text).err() {
url_input_parsed.set(Some(Err(format!(
"Error parsing {}: {}",
&response_url, err
))));
} else {
app.reduce(move |app| {
app.svgs.push(Svg {
content: text,
filename: response_url,
dimensions: [None; 2],
})
});
};
}
Err(err) => {
url_input_parsed.set(Some(Err(format!(
"Error fetching {}: {:?}",
&request_url,
err.dyn_into::<TypeError>().unwrap().message()
))));
}
}
}
})
};
html! { html! {
<FormGroup success={res.as_ref().map(Result::is_ok)}> <FormGroup success={file_upload_res.as_ref().map(Result::is_ok).or(url_input_parsed.as_ref().map(Result::is_ok))}>
<FileUpload<(), String> <FileUpload<(), String>
label="Select SVG files" label="Select SVG files"
accept=".svg" accept=".svg"
multiple={true} multiple={true}
parsed={res} onchange={file_upload_onchange}
onchange={onchange} />
<div class="divider text-center" data-content="OR"/>
<Input<String, String>
label="Add an SVG file by URL"
r#type={InputType::Url}
placeholder="https://raw.githubusercontent.com/sameer/svg2gcode/master/examples/Vanderbilt_Commodores_logo.svg"
oninput={url_input_oninput}
button={html_nested!(
<Button
style={ButtonStyle::Primary}
title="Add"
input_group=true
disabled={(*url_input_state).is_none()}
onclick={url_add_onclick}
loading={*url_add_loading}
/>
)}
parsed={(*url_input_parsed).clone()}
/> />
</FormGroup> </FormGroup>
} }

@ -46,7 +46,7 @@ macro_rules! css_class_enum {
#[derive(Properties, PartialEq, Clone)] #[derive(Properties, PartialEq, Clone)]
pub struct InputProps<T, E> pub struct InputProps<T, E>
where where
T: Display + Clone + PartialEq, T: Clone + PartialEq,
E: Display + Clone + PartialEq, E: Display + Clone + PartialEq,
{ {
pub label: &'static str, pub label: &'static str,
@ -54,8 +54,19 @@ where
pub parsed: Option<Result<T, E>>, pub parsed: Option<Result<T, E>>,
pub placeholder: Option<T>, pub placeholder: Option<T>,
pub default: Option<T>, pub default: Option<T>,
#[prop_or(InputType::Text)]
pub r#type: InputType,
#[prop_or_default] #[prop_or_default]
pub oninput: Callback<InputData>, pub oninput: Callback<InputData>,
#[prop_or_default]
pub button: Option<VChild<Button>>,
}
css_class_enum! {
InputType {
Text => "text",
Url => "url"
}
} }
#[function_component(Input)] #[function_component(Input)]
@ -86,9 +97,12 @@ where
{ props.label } { props.label }
</label> </label>
<div class={classes!(if success || error { Some("has-icon-right") } else { None })}> <div class={classes!(if success || error { Some("has-icon-right") } else { None })}>
<input id={id} class="form-input" type="text" ref={(*node_ref).clone()} <div class={classes!(if props.button.is_some() { Some("input-group") } else { None })}>
oninput={props.oninput.clone()} placeholder={ props.placeholder.as_ref().map(ToString::to_string) } <input id={id} class="form-input" type={props.r#type.to_string()} ref={(*node_ref).clone()}
/> oninput={props.oninput.clone()} placeholder={ props.placeholder.as_ref().map(ToString::to_string) }
/>
{ props.button.clone().map(Html::from).unwrap_or_default() }
</div>
{ {
if let Some(parsed) = props.parsed.as_ref() { if let Some(parsed) = props.parsed.as_ref() {
match parsed { match parsed {
@ -380,6 +394,8 @@ pub struct ButtonProps {
pub disabled: bool, pub disabled: bool,
#[prop_or(false)] #[prop_or(false)]
pub loading: bool, pub loading: bool,
#[prop_or(false)]
pub input_group: bool,
pub title: Option<&'static str>, pub title: Option<&'static str>,
pub icon: Option<VChild<Icon>>, pub icon: Option<VChild<Icon>>,
#[prop_or_default] #[prop_or_default]
@ -395,6 +411,7 @@ pub fn button(props: &ButtonProps) -> Html {
props.style.to_string(), props.style.to_string(),
if props.disabled { Some("disabled") } else { None }, if props.disabled { Some("disabled") } else { None },
if props.loading { Some("loading" )} else { None }, if props.loading { Some("loading" )} else { None },
if props.input_group { Some("input-group-btn") } else { None }
)} )}
disabled={props.disabled} disabled={props.disabled}
onclick={props.onclick.clone()} onclick={props.onclick.clone()}

@ -1,5 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::num::ParseFloatError; use std::num::ParseFloatError;
use svgtypes::Length;
use yewdux::prelude::{BasicStore, Persistent, PersistentStore}; use yewdux::prelude::{BasicStore, Persistent, PersistentStore};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -56,10 +57,11 @@ pub struct AppState {
pub svgs: Vec<Svg>, pub svgs: Vec<Svg>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct Svg { pub struct Svg {
pub content: String, pub content: String,
pub filename: String, pub filename: String,
pub dimensions: [Option<Length>; 2],
} }
impl Default for AppState { impl Default for AppState {

Loading…
Cancel
Save