calories/browser/src/lib.rs

459 lines
14 KiB
Rust
Raw Normal View History

#![feature(iter_intersperse)]
mod utils;
2024-08-19 18:11:59 +02:00
use image::{DynamicImage, RgbaImage};
use shared::structs::Nutrition;
2024-08-19 18:11:59 +02:00
use std::collections::HashMap;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
2024-08-19 18:11:59 +02:00
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);
2024-08-19 18:11:59 +02:00
#[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() {
2024-08-19 18:11:59 +02:00
utils::set_panic_hook();
}
#[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 {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);
2024-08-19 18:11:59 +02:00
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();
2024-08-19 18:11:59 +02:00
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<dyn FnMut(JsValue)> = Closure::new(move |stream: JsValue| {
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());
2024-08-19 18:11:59 +02:00
});
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 width: u32 = 300;
2024-08-19 18:11:59 +02:00
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<dyn FnMut()> = Closure::new(move || {
match scan_barcode(product_options.clone()) {
2024-08-19 18:11:59 +02:00
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(|| {
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| {
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
2024-08-19 18:11:59 +02:00
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> {
2024-08-19 18:11:59 +02:00
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);
}
2024-08-19 18:11:59 +02:00
break;
}
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 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<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() -> 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::<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();
}
}
}
}
}
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::<HtmlInputElement>()
.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 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());
}