#![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 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"; #[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(); } #[wasm_bindgen] pub fn init_camera_feed(product_options_js: JsValue) -> Result<(), String> { let mut product_options: Vec = serde_wasm_bindgen::from_value(product_options_js).unwrap(); let test: Nutrition = //Nutrition {69, "Vitamin", "Piller", "5702071500179", 0, 0, 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.}; 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., }; product_options.push(test); let window = window().unwrap(); let document = window.document().unwrap(); let video: HtmlVideoElement = document .get_element_by_id(VIDEO_ID) .unwrap() .dyn_into() .unwrap(); let video2 = video.clone(); let video3 = video.clone(); let canvas: HtmlCanvasElement = document .get_element_by_id(CANVAS_ID) .unwrap() .dyn_into() .unwrap(); let button: HtmlButtonElement = document .get_element_by_id(SCAN_BUTTON_ID) .unwrap() .dyn_into() .unwrap(); let search: HtmlInputElement = document .get_element_by_id(SEARCH_FIELD_ID) .unwrap() .dyn_into() .unwrap(); let navigator = window.navigator(); let media_devices = navigator.media_devices().unwrap(); let media_stream_constraints = MediaStreamConstraints::new(); media_stream_constraints.set_video(&JsValue::from_bool(true)); 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 = Closure::new(move |stream: JsValue| { video.set_src_object(Some(&MediaStream::unchecked_from_js(stream))); let _ = video.play(); }); let fail: Closure = 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 = Closure::new(move || { let width: u32 = 300; let height: u32 = ((width as f32 / video3.video_width() as f32) * video3.video_height() as f32) as u32; video3.set_width(width); video3.set_height(height); canvas.set_width(CANVAS_WIDTH); canvas.set_height(CANVAS_HEIGHT); }); let _ = video2 .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 = 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 = Closure::new(|| { 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 = Closure::new(|event: JsValue| { 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 = 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 { 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, width: u32, height: u32) -> Result { 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) -> Result<(), String> { let window = window().unwrap(); let document = window.document().unwrap(); let video: HtmlVideoElement = document .get_element_by_id(VIDEO_ID) .unwrap() .dyn_into() .unwrap(); let canvas: HtmlCanvasElement = document .get_element_by_id(CANVAS_ID) .unwrap() .dyn_into() .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; } Ok(()) } pub async fn request_json(url: String) -> Result { 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::()); 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 { let url = "/get_product_options"; let json = request_json(url.to_string()).await?; Ok(json) } fn set_product(product: &Nutrition) { let window = window().unwrap(); let document = window.document().unwrap(); let id: HtmlElement = document .get_element_by_id(RESULT_ID_ID) .unwrap() .dyn_into() .unwrap(); let barcode: HtmlElement = document .get_element_by_id(BARCODE_ID) .unwrap() .dyn_into() .unwrap(); let name: HtmlElement = document .get_element_by_id(RESULT_NAME_ID) .unwrap() .dyn_into() .unwrap(); let manufacturer: HtmlElement = document .get_element_by_id(RESULT_MANUFACTURER_ID) .unwrap() .dyn_into() .unwrap(); 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()); } #[wasm_bindgen] pub fn fill_search_options(product_options_js: JsValue) { let product_options: Vec = serde_wasm_bindgen::from_value(product_options_js).unwrap(); let nutrition_to_option = |n: &Nutrition| -> String { format!( "
{} - {}
", 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 = 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 { 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::()) .filter_map(|x| x.ok()) .collect() }) .unwrap_or_default() } fn show_search_options() -> Result<(), JsValue> { get_search_options() .into_iter() .for_each(|elem| elem.class_list().add_1("show").unwrap()); Ok(()) } fn hide_search_options(event_js: JsValue) -> Result<(), JsValue> { get_search_options() .into_iter() .for_each(|elem| elem.class_list().remove_1("show").unwrap()); if let Ok(event) = event_js.dyn_into::() { if let Some(search_elem) = event .target() .and_then(|x| x.dyn_into::().ok()) { if let Some(related_target_elem) = event .related_target() .and_then(|x| x.dyn_into::().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::().ok()) { if search_elem.eq(&search_elem_2) { related_target_elem.click(); } } } } } Ok(()) } fn filter_search_options() { let search_options = get_search_options(); let search_string: String = window() .unwrap() .document() .unwrap() .get_element_by_id(SEARCH_FIELD_ID) .unwrap() .dyn_into::() .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 = 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 = search_res .into_iter() .map(|x| search_option_map.get(x.0).unwrap().clone()) .collect(); let hidden_children: Vec = search_options .into_iter() .filter(|x| shown_children.iter().all(|y| x != y)) .collect(); let window = window().unwrap(); let document = window.document().unwrap(); let search_options_field: HtmlElement = document .get_element_by_id(SEARCH_FIELD_OPTIONS_ID) .unwrap() .dyn_into() .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()); }