429 lines
14 KiB
Rust
429 lines
14 KiB
Rust
#![feature(iter_intersperse)]
|
|
mod utils;
|
|
|
|
use image::{DynamicImage, RgbaImage};
|
|
use shared::structs::Nutrition;
|
|
use std::collections::HashMap;
|
|
use wasm_bindgen::prelude::*;
|
|
use wasm_bindgen_futures::JsFuture;
|
|
use web_sys::*;
|
|
|
|
const CANVAS_WIDTH: u32 = 1920;
|
|
const CANVAS_HEIGHT: u32 = 1080;
|
|
const VIDEO_ID: &str = "video";
|
|
const VIDEO_CONTAINER_ID: &str = "video-container";
|
|
const CANVAS_ID: &str = "canvas";
|
|
const BARCODE_ID: &str = "barcode";
|
|
const SCAN_BUTTON_ID: &str = "scan-barcode-button";
|
|
const SEARCH_FIELD_ID: &str = "myInput";
|
|
const SEARCH_FIELD_OPTIONS_ID: &str = "dropdownOptions";
|
|
const RESULT_ID_ID: &str = "id";
|
|
const RESULT_NAME_ID: &str = "name";
|
|
const RESULT_MANUFACTURER_ID: &str = "manufacturer";
|
|
const FORM_ID_ID: &str = "form-id";
|
|
const SUBMIT_BUTTON_ID: &str = "submit-button";
|
|
|
|
#[wasm_bindgen]
|
|
extern "C" {
|
|
fn alert(s: &str);
|
|
|
|
#[wasm_bindgen(js_namespace = console)]
|
|
fn log(s: &str);
|
|
|
|
#[wasm_bindgen(js_namespace = console)]
|
|
fn error(s: &str);
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn enable_panic_debug() {
|
|
utils::set_panic_hook();
|
|
}
|
|
|
|
fn get_element_by_id<T: wasm_bindgen::JsCast>(id: &str) -> Option<T> {
|
|
window()
|
|
.and_then(|x| x.document())
|
|
.and_then(|x| x.get_element_by_id(id))
|
|
.and_then(|x| x.dyn_into::<T>().ok())
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn init_camera_feed(product_options_js: JsValue) -> Result<(), String> {
|
|
let mut product_options: Vec<Nutrition> =
|
|
serde_wasm_bindgen::from_value(product_options_js).unwrap();
|
|
let test: Nutrition = Nutrition {
|
|
id: 69,
|
|
name: "Vitamin".to_string(),
|
|
manufacturer: "Piller".to_string(),
|
|
barcode: "5702071500179".to_string(),
|
|
amount: 0,
|
|
divisor: 0,
|
|
kJ: 0.,
|
|
kcal: 0.,
|
|
saturated_fat: 0.,
|
|
carbohydrate: 0.,
|
|
sugar: 0.,
|
|
fibres: 0.,
|
|
protein: 0.,
|
|
salt: 0.,
|
|
vitamin_b2: 0.,
|
|
vitamin_b12: 0.,
|
|
calcium: 0.,
|
|
phosphor: 0.,
|
|
vitamin_d: 0.,
|
|
};
|
|
product_options.push(test);
|
|
|
|
let video: HtmlVideoElement = get_element_by_id(VIDEO_ID).unwrap();
|
|
let canvas: HtmlCanvasElement = get_element_by_id(CANVAS_ID).unwrap();
|
|
let button: HtmlButtonElement = get_element_by_id(SCAN_BUTTON_ID).unwrap();
|
|
let search: HtmlInputElement = get_element_by_id(SEARCH_FIELD_ID).unwrap();
|
|
|
|
let navigator = window().unwrap().navigator();
|
|
let media_devices = navigator.media_devices().unwrap();
|
|
let media_stream_constraints = MediaStreamConstraints::new();
|
|
let video_constraint = js_sys::Object::new();
|
|
let _ = js_sys::Reflect::set(
|
|
&video_constraint,
|
|
&JsValue::from("facingMode"),
|
|
&JsValue::from("environment"),
|
|
);
|
|
media_stream_constraints.set_video(&video_constraint);
|
|
media_stream_constraints.set_audio(&JsValue::from_bool(false));
|
|
let user_media = media_devices
|
|
.get_user_media_with_constraints(&media_stream_constraints)
|
|
.unwrap();
|
|
let succ: Closure<dyn FnMut(JsValue)> = Closure::new(move |stream: JsValue| {
|
|
let video: HtmlVideoElement = get_element_by_id(VIDEO_ID).unwrap();
|
|
video.set_src_object(Some(&MediaStream::unchecked_from_js(stream)));
|
|
let _ = video.play();
|
|
});
|
|
let fail: Closure<dyn FnMut(JsValue)> = Closure::new(|err| {
|
|
error(format!("Couldn't get camera feed: {:?}", err).as_str());
|
|
});
|
|
let _ = user_media.then(&succ).catch(&fail);
|
|
succ.forget(); // Memory leak
|
|
fail.forget(); // Memory leak
|
|
|
|
let set_width_and_height: Closure<dyn FnMut()> = Closure::new(move || {
|
|
let video: HtmlVideoElement = get_element_by_id(VIDEO_ID).unwrap();
|
|
|
|
let width: u32 = 1920;
|
|
let height: u32 =
|
|
((width as f32 / video.video_width() as f32) * video.video_height() as f32) as u32;
|
|
|
|
video.set_width(width);
|
|
video.set_height(height);
|
|
canvas.set_width(CANVAS_WIDTH);
|
|
canvas.set_height(CANVAS_HEIGHT);
|
|
});
|
|
|
|
let _ = video
|
|
.add_event_listener_with_callback("canplay", set_width_and_height.as_ref().unchecked_ref());
|
|
set_width_and_height.forget(); // Memory leak
|
|
//
|
|
let scan_barcode_wrapper: Closure<dyn FnMut()> = Closure::new(move || {
|
|
match scan_barcode(product_options.clone()) {
|
|
Ok(()) => {}
|
|
Err(e) => error(e.as_str()),
|
|
};
|
|
});
|
|
let _ = button
|
|
.add_event_listener_with_callback("click", scan_barcode_wrapper.as_ref().unchecked_ref());
|
|
scan_barcode_wrapper.forget(); // Memory leak
|
|
|
|
let show_search_options_wrapper: Closure<dyn FnMut()> = Closure::new(|| {
|
|
show_search_options();
|
|
// match show_search_options() {
|
|
// Ok(()) => {}
|
|
// Err(e) => error(format!("Failed to show search options: {:?}", e).as_str()),
|
|
// };
|
|
});
|
|
|
|
let _ = search.add_event_listener_with_callback(
|
|
"focusin",
|
|
show_search_options_wrapper.as_ref().unchecked_ref(),
|
|
);
|
|
show_search_options_wrapper.forget(); // Memory leak
|
|
//
|
|
let hide_search_options_wrapper: Closure<dyn FnMut(JsValue)> =
|
|
Closure::new(|event: JsValue| {
|
|
hide_search_options(event);
|
|
// match hide_search_options(event) {
|
|
// Ok(()) => {}
|
|
// Err(e) => error(format!("Failed to hide search options: {:?}", e).as_str()),
|
|
// };
|
|
});
|
|
|
|
let _ = search.add_event_listener_with_callback(
|
|
"focusout",
|
|
hide_search_options_wrapper.as_ref().unchecked_ref(),
|
|
);
|
|
hide_search_options_wrapper.forget(); // Memory leak
|
|
//
|
|
let filter_search_options_wrapper: Closure<dyn FnMut()> =
|
|
Closure::new(|| filter_search_options());
|
|
|
|
let _ = search.add_event_listener_with_callback(
|
|
"keyup",
|
|
filter_search_options_wrapper.as_ref().unchecked_ref(),
|
|
);
|
|
filter_search_options_wrapper.forget(); // Memory leak
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn convert_rgba_to_luma(data: &[u8]) -> Vec<u8> {
|
|
let rgba = RgbaImage::from_raw((data.len() / 4) as u32, 1, data.to_vec());
|
|
let luma = DynamicImage::ImageRgba8(rgba.unwrap()).into_luma8();
|
|
luma.to_vec()
|
|
}
|
|
|
|
fn decode_barcode(data: Vec<u8>, width: u32, height: u32) -> Result<String, rxing::Exceptions> {
|
|
let mut hints: rxing::DecodingHintDictionary = HashMap::new();
|
|
hints.insert(
|
|
rxing::DecodeHintType::TRY_HARDER,
|
|
rxing::DecodeHintValue::TryHarder(true),
|
|
);
|
|
|
|
let result =
|
|
match rxing::helpers::detect_in_luma_with_hints(data, width, height, None, &mut hints) {
|
|
Ok(r) => r,
|
|
Err(e) => return Err(e),
|
|
};
|
|
Ok(result.getText().to_string())
|
|
}
|
|
|
|
fn scan_barcode(product_options: Vec<Nutrition>) -> Result<(), String> {
|
|
let video: HtmlVideoElement = get_element_by_id(VIDEO_ID).unwrap();
|
|
let video_container: HtmlElement = get_element_by_id(VIDEO_CONTAINER_ID).unwrap();
|
|
video_container.class_list().add_1("show").unwrap();
|
|
let canvas: HtmlCanvasElement = get_element_by_id(CANVAS_ID).unwrap();
|
|
let context: CanvasRenderingContext2d = match canvas.get_context("2d").unwrap() {
|
|
Some(c) => c.dyn_into().unwrap(),
|
|
None => return Err("Could not get canvas context".to_string()),
|
|
};
|
|
for _ in 0..100 {
|
|
let _ = context.draw_image_with_html_video_element(&video, 0., 0.);
|
|
|
|
let rgba = context
|
|
.get_image_data(0., 0., CANVAS_WIDTH.into(), CANVAS_HEIGHT.into())
|
|
.unwrap()
|
|
.data();
|
|
let luma = convert_rgba_to_luma(&rgba);
|
|
|
|
let code: String = match decode_barcode(luma, CANVAS_WIDTH, CANVAS_HEIGHT) {
|
|
Ok(c) => c,
|
|
Err(rxing::Exceptions::NotFoundException(_)) => {
|
|
log(".");
|
|
continue;
|
|
}
|
|
Err(e) => return Err(format!("{:?}", e)),
|
|
};
|
|
|
|
if let Some(product) = product_options.iter().find(|x| x.barcode() == code) {
|
|
set_product(product);
|
|
}
|
|
|
|
break;
|
|
}
|
|
video_container.class_list().remove_1("show").unwrap();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn request_json(url: String) -> Result<JsValue, JsValue> {
|
|
let opts = RequestInit::new();
|
|
opts.set_method("GET");
|
|
opts.set_mode(RequestMode::Cors);
|
|
|
|
let request = Request::new_with_str_and_init(url.as_str(), &opts)?;
|
|
|
|
request.headers().set("Accept", "application/json")?;
|
|
|
|
let window = web_sys::window().unwrap();
|
|
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
|
|
|
|
// `resp_value` is a `Response` object.
|
|
assert!(resp_value.is_instance_of::<Response>());
|
|
let resp: Response = resp_value.dyn_into().unwrap();
|
|
|
|
// Convert this other `Promise` into a rust `Future`.
|
|
let json = JsFuture::from(resp.json()?).await?;
|
|
|
|
Ok(json)
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub async fn get_product_options() -> Result<JsValue, JsValue> {
|
|
let url = "/get_product_options";
|
|
|
|
let json = request_json(url.to_string()).await?;
|
|
|
|
Ok(json)
|
|
}
|
|
|
|
fn set_product(product: &Nutrition) {
|
|
let id: HtmlElement = get_element_by_id(RESULT_ID_ID).unwrap();
|
|
let form_id: HtmlElement = get_element_by_id(FORM_ID_ID).unwrap();
|
|
let barcode: HtmlElement = get_element_by_id(BARCODE_ID).unwrap();
|
|
let name: HtmlElement = get_element_by_id(RESULT_NAME_ID).unwrap();
|
|
let manufacturer: HtmlElement = get_element_by_id(RESULT_MANUFACTURER_ID).unwrap();
|
|
|
|
id.set_inner_html(&product.id().to_string());
|
|
form_id.set_inner_html(&product.id().to_string());
|
|
barcode.set_inner_html(&product.barcode());
|
|
name.set_inner_html(&product.name());
|
|
manufacturer.set_inner_html(&product.manufacturer());
|
|
|
|
log("foo!");
|
|
let submit_button: HtmlElement = get_element_by_id(SUBMIT_BUTTON_ID).unwrap();
|
|
submit_button
|
|
.attributes()
|
|
.remove_named_item("disabled")
|
|
.ok();
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
pub fn fill_search_options(product_options_js: JsValue) {
|
|
let product_options: Vec<Nutrition> =
|
|
serde_wasm_bindgen::from_value(product_options_js).unwrap();
|
|
|
|
let nutrition_to_option = |n: &Nutrition| -> String {
|
|
format!(
|
|
"<div id=\"option{}\" class=\"dropdownOption\" tabindex=\"0\">{} - {}</div>",
|
|
n.id(),
|
|
n.name(),
|
|
n.manufacturer()
|
|
)
|
|
};
|
|
|
|
let window = window().unwrap();
|
|
let document = window.document().unwrap();
|
|
let search_options: HtmlElement = document
|
|
.get_element_by_id(SEARCH_FIELD_OPTIONS_ID)
|
|
.unwrap()
|
|
.dyn_into()
|
|
.unwrap();
|
|
|
|
let html: String = product_options.iter().map(nutrition_to_option).collect();
|
|
|
|
search_options.set_inner_html(&html);
|
|
|
|
product_options.iter().cloned().for_each(|x| {
|
|
let elem: HtmlElement = document
|
|
.get_element_by_id(format!("option{}", x.id()).as_str())
|
|
.unwrap()
|
|
.dyn_into()
|
|
.unwrap();
|
|
let callback_fun: Closure<dyn FnMut()> = Closure::new(move || {
|
|
set_product(&x);
|
|
});
|
|
let _ =
|
|
elem.add_event_listener_with_callback("click", callback_fun.as_ref().unchecked_ref());
|
|
callback_fun.forget();
|
|
});
|
|
}
|
|
|
|
fn get_search_options() -> Vec<HtmlElement> {
|
|
window()
|
|
.and_then(|x| x.document())
|
|
.and_then(|x| x.query_selector_all(".dropdown-options div").ok())
|
|
.map(|nodelist| {
|
|
nodelist
|
|
.values()
|
|
.into_iter()
|
|
.filter_map(|x| x.ok())
|
|
.map(|x| x.dyn_into::<HtmlElement>())
|
|
.filter_map(|x| x.ok())
|
|
.collect()
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn show_search_options() {
|
|
get_search_options()
|
|
.into_iter()
|
|
.for_each(|elem| elem.class_list().add_1("show").unwrap());
|
|
}
|
|
|
|
fn hide_search_options(event_js: JsValue) {
|
|
get_search_options()
|
|
.into_iter()
|
|
.for_each(|elem| elem.class_list().remove_1("show").unwrap());
|
|
|
|
if let Ok(event) = event_js.dyn_into::<FocusEvent>() {
|
|
if let Some(search_elem) = event
|
|
.target()
|
|
.and_then(|x| x.dyn_into::<HtmlElement>().ok())
|
|
{
|
|
if let Some(related_target_elem) = event
|
|
.related_target()
|
|
.and_then(|x| x.dyn_into::<HtmlElement>().ok())
|
|
{
|
|
if let Some(search_elem_2) = related_target_elem
|
|
.parent_element()
|
|
.and_then(|x| x.previous_element_sibling())
|
|
.and_then(|x| x.dyn_into::<HtmlElement>().ok())
|
|
{
|
|
if search_elem.eq(&search_elem_2) {
|
|
related_target_elem.click();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn filter_search_options() {
|
|
let search_options = get_search_options();
|
|
let search_string: String = get_element_by_id::<HtmlInputElement>(SEARCH_FIELD_ID)
|
|
.unwrap()
|
|
.value();
|
|
|
|
let mut matcher = nucleo_matcher::Matcher::new(nucleo_matcher::Config::DEFAULT);
|
|
let parser = nucleo_matcher::pattern::Pattern::parse(
|
|
search_string.as_str(),
|
|
nucleo_matcher::pattern::CaseMatching::Ignore,
|
|
nucleo_matcher::pattern::Normalization::Smart,
|
|
);
|
|
|
|
let search_option_map: HashMap<String, HtmlElement> = search_options
|
|
.clone()
|
|
.into_iter()
|
|
.map(|x| (x.inner_text(), x))
|
|
.collect();
|
|
|
|
let mut search_res = parser.match_list(search_option_map.keys(), &mut matcher);
|
|
search_res.sort_unstable_by_key(|x| std::cmp::Reverse((x.1, x.0)));
|
|
|
|
// Note that this way the children are sorted the same order as search_res
|
|
let shown_children: Vec<HtmlElement> = search_res
|
|
.into_iter()
|
|
.map(|x| search_option_map.get(x.0).unwrap().clone())
|
|
.collect();
|
|
|
|
let hidden_children: Vec<HtmlElement> = search_options
|
|
.into_iter()
|
|
.filter(|x| shown_children.iter().all(|y| x != y))
|
|
.collect();
|
|
|
|
let search_options_field: HtmlElement = get_element_by_id(SEARCH_FIELD_OPTIONS_ID).unwrap();
|
|
|
|
let children_arr: js_sys::Array = shown_children
|
|
.iter()
|
|
.chain(hidden_children.iter())
|
|
.clone()
|
|
.map(JsValue::from)
|
|
.collect();
|
|
|
|
search_options_field.replace_children_with_node(&children_arr.into_iter().collect());
|
|
|
|
hidden_children
|
|
.into_iter()
|
|
.for_each(|elem| elem.class_list().remove_1("show").unwrap());
|
|
|
|
shown_children
|
|
.into_iter()
|
|
.for_each(|elem| elem.class_list().add_1("show").unwrap());
|
|
}
|