Scan barcode and dropdown with search works
This commit is contained in:
parent
9cb3463826
commit
08f92e21a5
50
Cargo.lock
generated
50
Cargo.lock
generated
@ -178,6 +178,7 @@ checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"axum-macros",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
@ -225,6 +226,18 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-macros"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.74",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.73"
|
||||
@ -455,7 +468,10 @@ dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"image 0.25.2",
|
||||
"js-sys",
|
||||
"nucleo-matcher",
|
||||
"rxing",
|
||||
"serde-wasm-bindgen",
|
||||
"shared",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
@ -472,6 +488,8 @@ dependencies = [
|
||||
"mysql_common",
|
||||
"plotters",
|
||||
"plotters-canvas",
|
||||
"serde",
|
||||
"shared",
|
||||
"struct-field-names-as-array",
|
||||
"time",
|
||||
"tokio",
|
||||
@ -1933,6 +1951,16 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||
|
||||
[[package]]
|
||||
name = "nucleo-matcher"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.4.3"
|
||||
@ -2797,6 +2825,17 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-wasm-bindgen"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.208"
|
||||
@ -2873,6 +2912,17 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"mysql",
|
||||
"mysql_common",
|
||||
"serde",
|
||||
"struct-field-names-as-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
|
@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
members = ["server", "browser"]
|
||||
members = ["server", "browser", "shared"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
|
@ -20,10 +20,14 @@ console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||
|
||||
rxing = "0.6.1"
|
||||
image = "0.25.2"
|
||||
web-sys = { version = "0.3.70", features = ["Navigator", "MediaDevices", "Document", "HtmlCanvasElement", "CanvasRenderingContext2d", "VideoFrame", "HtmlVideoElement", "ImageData", "Window", "HtmlInputElement", "HtmlButtonElement", "MediaStreamConstraints", "MediaStream", "HtmlMediaElement", "console", "EventTarget"] }
|
||||
web-sys = { version = "0.3.70", features = ["Navigator", "MediaDevices", "Document", "HtmlCanvasElement", "CanvasRenderingContext2d", "VideoFrame", "HtmlVideoElement", "ImageData", "Window", "HtmlInputElement", "HtmlButtonElement", "MediaStreamConstraints", "MediaStream", "HtmlMediaElement", "console", "EventTarget", "Request", "RequestInit", "RequestMode", "Response", "Headers", "NodeList", "Event", "DomTokenList", "Element", "FocusEvent"] }
|
||||
wasm-bindgen-futures = "0.4.43"
|
||||
js-sys = "0.3.70"
|
||||
|
||||
shared = { path = "../shared" }
|
||||
serde-wasm-bindgen = "0.6.5"
|
||||
nucleo-matcher = "0.3.1"
|
||||
|
||||
[profile.release]
|
||||
# Tell `rustc` to optimize for small code size.
|
||||
opt-level = "s"
|
||||
|
@ -1,8 +1,11 @@
|
||||
#![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;
|
||||
@ -11,6 +14,11 @@ 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" {
|
||||
@ -24,8 +32,36 @@ extern "C" {
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn init_camera_feed() -> Result<(), String> {
|
||||
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<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);
|
||||
|
||||
let window = window().unwrap();
|
||||
let document = window.document().unwrap();
|
||||
@ -46,6 +82,11 @@ pub fn init_camera_feed() -> Result<(), String> {
|
||||
.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();
|
||||
@ -59,14 +100,14 @@ pub fn init_camera_feed() -> Result<(), String> {
|
||||
let _ = video.play();
|
||||
});
|
||||
let fail: Closure<dyn FnMut(JsValue)> = Closure::new(|err| {
|
||||
error(format!("An error occured: {:?}", err).as_str());
|
||||
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 width: u32 = 640;
|
||||
let width: u32 = 300;
|
||||
let height: u32 =
|
||||
((width as f32 / video3.video_width() as f32) * video3.video_height() as f32) as u32;
|
||||
|
||||
@ -81,7 +122,7 @@ pub fn init_camera_feed() -> Result<(), String> {
|
||||
set_width_and_height.forget(); // Memory leak
|
||||
//
|
||||
let scan_barcode_wrapper: Closure<dyn FnMut()> = Closure::new(move || {
|
||||
match scan_barcode() {
|
||||
match scan_barcode(product_options.clone()) {
|
||||
Ok(()) => {}
|
||||
Err(e) => error(e.as_str()),
|
||||
};
|
||||
@ -90,6 +131,42 @@ pub fn init_camera_feed() -> Result<(), String> {
|
||||
.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
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -114,7 +191,7 @@ fn decode_barcode(data: Vec<u8>, width: u32, height: u32) -> Result<String, rxin
|
||||
Ok(result.getText().to_string())
|
||||
}
|
||||
|
||||
fn scan_barcode() -> Result<(), String> {
|
||||
fn scan_barcode(product_options: Vec<Nutrition>) -> Result<(), String> {
|
||||
let window = window().unwrap();
|
||||
let document = window.document().unwrap();
|
||||
let video: HtmlVideoElement = document
|
||||
@ -127,11 +204,6 @@ fn scan_barcode() -> Result<(), String> {
|
||||
.unwrap()
|
||||
.dyn_into()
|
||||
.unwrap();
|
||||
let barcode: HtmlInputElement = document
|
||||
.get_element_by_id(BARCODE_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()),
|
||||
@ -154,9 +226,233 @@ fn scan_barcode() -> Result<(), String> {
|
||||
Err(e) => return Err(format!("{:?}", e)),
|
||||
};
|
||||
|
||||
barcode.set_value(&code);
|
||||
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<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| (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());
|
||||
}
|
||||
|
71
css/camera_test.css
Normal file
71
css/camera_test.css
Normal file
@ -0,0 +1,71 @@
|
||||
/* Dropdown Button */
|
||||
.dropbtn {
|
||||
background-color: #04AA6D;
|
||||
color: white;
|
||||
padding: 16px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Dropdown button on hover & focus */
|
||||
.dropbtn:hover, .dropbtn:focus {
|
||||
background-color: #3e8e41;
|
||||
}
|
||||
|
||||
/* The search field */
|
||||
#myInput {
|
||||
box-sizing: border-box;
|
||||
background-image: url('searchicon.png');
|
||||
background-position: 14px 12px;
|
||||
background-repeat: no-repeat;
|
||||
font-size: 16px;
|
||||
padding: 14px 20px 12px 45px;
|
||||
border: none;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* The search field when it gets focus/clicked on */
|
||||
#myInput:focus {outline: 3px solid #ddd;}
|
||||
|
||||
/* The container <div> - needed to position the dropdown content */
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Dropdown Content (Hidden by Default) */
|
||||
.dropdown-content {
|
||||
position: relative;
|
||||
background-color: #f6f6f6;
|
||||
min-width: 230px;
|
||||
border: 1px solid #ddd;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dropdown-options {
|
||||
position: absolute;
|
||||
background-color: #f6f6f6;
|
||||
min-width: 230px;
|
||||
border: 1px solid #ddd;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dropdown-options div {
|
||||
/* position: absolute; */
|
||||
display: none;
|
||||
color: black;
|
||||
padding: 12px 16px;
|
||||
text-decoration: none;
|
||||
/* display: block; */
|
||||
/* z-index: 1; */
|
||||
}
|
||||
|
||||
/* Change color of dropdown links on hover */
|
||||
.dropdown-content a:hover {background-color: #f1f1f1}
|
||||
.dropdown-content p:hover {background-color: #f1f1f1}
|
||||
.dropdown-content li:hover {background-color: #f1f1f1}
|
||||
.dropdown-content div div:hover {background-color: #f1f1f1}
|
||||
|
||||
/* Show the dropdown menu (use JS to add this class to the .dropdown-content container when the user clicks on the dropdown button) */
|
||||
.show {display:block !important;}
|
@ -1,82 +1,7 @@
|
||||
import init, { init_camera_feed } from '/pkg/calories_browser.js';
|
||||
import init, { enable_panic_debug, init_camera_feed, get_product_options, fill_search_options } from '/pkg/calories_browser.js';
|
||||
await init();
|
||||
init_camera_feed();
|
||||
enable_panic_debug();
|
||||
|
||||
// (() => {
|
||||
|
||||
// // The width and height of the captured photo. We will set the
|
||||
// // width to the value defined here, but the height will be
|
||||
// // calculated based on the aspect ratio of the input stream.
|
||||
|
||||
// const width = 1080; // We will scale the photo width to this
|
||||
// let height = 0; // This will be computed based on the input stream
|
||||
|
||||
// // |streaming| indicates whether or not we're currently streaming
|
||||
// // video from the camera. Obviously, we start at false.
|
||||
|
||||
// let streaming = false;
|
||||
|
||||
// // The various HTML elements we need to configure or control. These
|
||||
// // will be set by the startup() function.
|
||||
|
||||
// let video = null;
|
||||
// let canvas = null;
|
||||
// let startbutton = null;
|
||||
|
||||
// async function startup() {
|
||||
// await init();
|
||||
|
||||
// video = document.getElementById("video");
|
||||
// canvas = document.getElementById("canvas");
|
||||
// startbutton = document.getElementById("startbutton");
|
||||
|
||||
// navigator.mediaDevices
|
||||
// .getUserMedia({ video: true, audio: false })
|
||||
// .then((stream) => {
|
||||
// video.srcObject = stream;
|
||||
// video.play();
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// console.error(`An error occurred: ${err}`);
|
||||
// });
|
||||
|
||||
// video.addEventListener(
|
||||
// "canplay",
|
||||
// (ev) => {
|
||||
// if (!streaming) {
|
||||
// height = video.videoHeight / (video.videoWidth / width);
|
||||
|
||||
// // Firefox currently has a bug where the height can't be read from
|
||||
// // the video, so we will make assumptions if this happens.
|
||||
// if (isNaN(height)) {
|
||||
// height = width / (4 / 3);
|
||||
// }
|
||||
|
||||
// video.setAttribute("width", width);
|
||||
// video.setAttribute("height", height);
|
||||
// canvas.setAttribute("width", width);
|
||||
// canvas.setAttribute("height", height);
|
||||
// streaming = true;
|
||||
// }
|
||||
// },
|
||||
// false,
|
||||
// );
|
||||
|
||||
// startbutton.addEventListener(
|
||||
// "click",
|
||||
// (ev) => {
|
||||
// takepicture();
|
||||
// ev.preventDefault();
|
||||
// },
|
||||
// false,
|
||||
// );
|
||||
// }
|
||||
|
||||
// function takepicture() {
|
||||
// fill_barcode();
|
||||
// }
|
||||
|
||||
// // Set up our event listener to run the startup process
|
||||
// // once loading is complete.
|
||||
// // window.addEventListener("load", startup, false);
|
||||
// })();
|
||||
let product_options = await get_product_options();
|
||||
fill_search_options(product_options);
|
||||
init_camera_feed(product_options);
|
||||
|
4
run.sh
4
run.sh
@ -1,4 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
cd shared || exit;
|
||||
cargo build || exit;
|
||||
cd ..;
|
||||
|
||||
cd browser || exit;
|
||||
wasm-pack build --target web || exit;
|
||||
cd ..;
|
||||
|
@ -5,14 +5,16 @@ edition.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[dependencies]
|
||||
axum = "0.7.5"
|
||||
axum = { version = "0.7.5", features = ["macros"] }
|
||||
chrono = "0.4.38"
|
||||
image = "0.25.2"
|
||||
mysql = "25.0.1"
|
||||
mysql_common = { version = "0.32.4", features = ["frunk"] }
|
||||
plotters = "0.3.6"
|
||||
plotters-canvas = "0.3.0"
|
||||
serde = "1.0.208"
|
||||
struct-field-names-as-array = "0.3.0"
|
||||
time = "0.3.36"
|
||||
tokio = { version = "1.39.2", features = ["macros", "rt-multi-thread"] }
|
||||
tower-http = { version = "0.5.2", features = ["fs"] }
|
||||
shared = { path = "../shared", features = ["sql"] }
|
||||
|
@ -9,7 +9,7 @@ use axum::{
|
||||
},
|
||||
response::{Html, IntoResponse},
|
||||
routing::{get, get_service},
|
||||
Router,
|
||||
Json, Router,
|
||||
};
|
||||
// use image::ImageFormat;
|
||||
// use mysql::prelude::*;
|
||||
@ -19,12 +19,15 @@ use mysql::*;
|
||||
use std::sync::Arc;
|
||||
mod math;
|
||||
mod my_structs;
|
||||
use my_structs::MyError;
|
||||
use my_structs::Purchase;
|
||||
mod plotting;
|
||||
// use crate::my_structs::MyError;
|
||||
// use crate::plotting::Plotter;
|
||||
use std::{thread, time};
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
use shared::structs::Nutrition;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
sql_pool: Pool,
|
||||
@ -59,7 +62,10 @@ async fn main() {
|
||||
.route("/calorie_intake", get(calorie_intake))
|
||||
.route("/wasm_test", get(wasm_test))
|
||||
.route("/camera_test", get(camera_test))
|
||||
.route("/get_product_options", get(get_product_options))
|
||||
.nest_service("/pkg", get_service(ServeDir::new("../browser/pkg")))
|
||||
.nest_service("/css", get_service(ServeDir::new("../css")))
|
||||
.nest_service("/js", get_service(ServeDir::new("../javascript")))
|
||||
.with_state(shared_state)
|
||||
.fallback(not_found);
|
||||
|
||||
@ -72,10 +78,9 @@ async fn not_found(uri: Uri) -> (StatusCode, String) {
|
||||
(StatusCode::NOT_FOUND, format!("404 not found: {uri}"))
|
||||
}
|
||||
|
||||
async fn get_rows(State(state): State<Arc<AppState>>) -> Result<String, my_structs::MyError> {
|
||||
let nutrition_val_promise =
|
||||
my_structs::Nutrition::get_nutrition_hashmap(state.sql_pool.clone());
|
||||
let purchases_val_promise = my_structs::Purchase::get_purchase_rows(state.sql_pool.clone());
|
||||
async fn get_rows(State(state): State<Arc<AppState>>) -> Result<String, MyError> {
|
||||
let nutrition_val_promise = Nutrition::get_nutrition_hashmap(state.sql_pool.clone());
|
||||
let purchases_val_promise = Purchase::get_purchase_rows(state.sql_pool.clone());
|
||||
|
||||
let nutrition_val = nutrition_val_promise.await?;
|
||||
let purchases_val = purchases_val_promise.await?;
|
||||
@ -85,6 +90,18 @@ async fn get_rows(State(state): State<Arc<AppState>>) -> Result<String, my_struc
|
||||
Ok(format!("{:#?} {:#?}", nutrition_val, purchases_val))
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn get_product_options(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<Nutrition>>, MyError> {
|
||||
println!("Serving: get_product_options");
|
||||
|
||||
let nutrition_val_promise = Nutrition::get_nutrition_rows(state.sql_pool.clone());
|
||||
let nutrition_val = nutrition_val_promise.await?;
|
||||
|
||||
Ok(Json(nutrition_val))
|
||||
}
|
||||
|
||||
async fn html_demo(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
println!("Serving: html_demo");
|
||||
|
||||
@ -99,12 +116,11 @@ async fn svg_image(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
|
||||
async fn svg_calorie_intake(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<impl IntoResponse, my_structs::MyError> {
|
||||
) -> Result<impl IntoResponse, MyError> {
|
||||
println!("Serving: svg_calorie_intake");
|
||||
|
||||
let nutrition_val_promise =
|
||||
my_structs::Nutrition::get_nutrition_hashmap(state.sql_pool.clone());
|
||||
let purchases_val_promise = my_structs::Purchase::get_purchase_rows(state.sql_pool.clone());
|
||||
let nutrition_val_promise = Nutrition::get_nutrition_hashmap(state.sql_pool.clone());
|
||||
let purchases_val_promise = Purchase::get_purchase_rows(state.sql_pool.clone());
|
||||
|
||||
let nutrition_val = nutrition_val_promise.await?;
|
||||
let purchases_val = purchases_val_promise.await?;
|
||||
@ -131,36 +147,50 @@ async fn calorie_intake(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
Html("<body><img src=/calorie_intake.svg></img></body>".to_string())
|
||||
}
|
||||
|
||||
fn construct_js(path: &str) -> Result<String, my_structs::MyError> {
|
||||
let javascript = std::fs::read_to_string(format!("../javascript/{}", path))?;
|
||||
let module = format!(
|
||||
"
|
||||
<script type=\"module\">
|
||||
{}
|
||||
</script>
|
||||
",
|
||||
javascript
|
||||
);
|
||||
Ok(module)
|
||||
fn construct_js(path: &str) -> Result<String, MyError> {
|
||||
// let javascript = std::fs::read_to_string(format!("../javascript/{}", path))?;
|
||||
// let module = format!(
|
||||
// "
|
||||
// <script type=\"module\">
|
||||
// {}
|
||||
// </script>
|
||||
// ",
|
||||
// javascript
|
||||
// );
|
||||
// Ok(module)
|
||||
Ok(format!(
|
||||
"<script type=\"module\" src=\"/js/{}\"></script>",
|
||||
path
|
||||
))
|
||||
}
|
||||
|
||||
fn construct_tmpl(path: &str) -> Result<String, my_structs::MyError> {
|
||||
fn construct_css(path: &str) -> Result<String, MyError> {
|
||||
Ok(format!("<link rel=\"stylesheet\" href=\"/css/{}\"/>", path))
|
||||
}
|
||||
|
||||
fn construct_tmpl(path: &str) -> Result<String, MyError> {
|
||||
Ok(std::fs::read_to_string(format!("../templates/{}", path))?)
|
||||
}
|
||||
|
||||
fn construct_html(
|
||||
js_paths: Vec<&str>,
|
||||
css_paths: Vec<&str>,
|
||||
tmpl_paths: Vec<&str>,
|
||||
) -> Result<String, my_structs::MyError> {
|
||||
) -> Result<String, MyError> {
|
||||
let js_modules: Vec<String> = js_paths
|
||||
.into_iter()
|
||||
.map(construct_js)
|
||||
.collect::<Result<Vec<String>, my_structs::MyError>>()?;
|
||||
.collect::<Result<Vec<String>, MyError>>()?;
|
||||
|
||||
let css_styling: Vec<String> = css_paths
|
||||
.into_iter()
|
||||
.map(construct_css)
|
||||
.collect::<Result<Vec<String>, MyError>>()?;
|
||||
|
||||
let tmpl_snippets: Vec<String> = tmpl_paths
|
||||
.into_iter()
|
||||
.map(construct_tmpl)
|
||||
.collect::<Result<Vec<String>, my_structs::MyError>>()?;
|
||||
.collect::<Result<Vec<String>, MyError>>()?;
|
||||
|
||||
let html = format!(
|
||||
"
|
||||
@ -168,24 +198,24 @@ fn construct_html(
|
||||
<html lang=\"en\">
|
||||
<head>
|
||||
{}
|
||||
{}
|
||||
</head>
|
||||
<body>
|
||||
{}
|
||||
</body>
|
||||
",
|
||||
js_modules.join(""),
|
||||
css_styling.join(""),
|
||||
tmpl_snippets.join("")
|
||||
);
|
||||
|
||||
Ok(html)
|
||||
}
|
||||
fn get_template(path: &str) -> Result<String, my_structs::MyError> {
|
||||
fn get_template(path: &str) -> Result<String, MyError> {
|
||||
Ok(std::fs::read_to_string(format!("../templates/{}", path))?)
|
||||
}
|
||||
|
||||
async fn wasm_test(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
) -> Result<Html<String>, my_structs::MyError> {
|
||||
async fn wasm_test(State(_state): State<Arc<AppState>>) -> Result<Html<String>, MyError> {
|
||||
println!("Serving: wasm_test");
|
||||
|
||||
let content = get_template("view_calories.html")?;
|
||||
@ -193,12 +223,14 @@ async fn wasm_test(
|
||||
Ok(Html(content))
|
||||
}
|
||||
|
||||
async fn camera_test(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
) -> Result<Html<String>, my_structs::MyError> {
|
||||
async fn camera_test(State(_state): State<Arc<AppState>>) -> Result<Html<String>, MyError> {
|
||||
println!("Serving: camera_test");
|
||||
|
||||
let html = construct_html(vec!["camera.js"], vec!["camera_test.html"])?;
|
||||
let html = construct_html(
|
||||
vec!["camera.js"],
|
||||
vec!["camera_test.css"],
|
||||
vec!["camera_test.html"],
|
||||
)?;
|
||||
|
||||
Ok(Html(html))
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ use crate::plotting;
|
||||
// use chrono::TimeZone;
|
||||
use chrono::{DateTime, NaiveDate, Utc};
|
||||
use plotters::prelude::*;
|
||||
use shared::structs::Nutrition;
|
||||
use std::collections::HashMap;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use time::Date;
|
||||
@ -16,7 +17,7 @@ fn time_date_to_chrono_naive_date(d: Date) -> chrono::NaiveDate {
|
||||
}
|
||||
|
||||
fn calculate_calories_per_day(
|
||||
nutrition_map: HashMap<u32, my_structs::Nutrition>,
|
||||
nutrition_map: HashMap<u32, Nutrition>,
|
||||
purchases: Vec<my_structs::Purchase>,
|
||||
) -> (Date, Vec<f32>) {
|
||||
if purchases.is_empty() {
|
||||
@ -76,7 +77,7 @@ fn calculate_calories_per_day(
|
||||
}
|
||||
|
||||
pub fn plot_calories_per_day(
|
||||
nutrition_map: HashMap<u32, my_structs::Nutrition>,
|
||||
nutrition_map: HashMap<u32, Nutrition>,
|
||||
purchases: Vec<my_structs::Purchase>,
|
||||
) -> plotting::SVGPlotFun {
|
||||
let (mindate, calorie_days) = calculate_calories_per_day(nutrition_map, purchases);
|
||||
|
@ -1,6 +1,4 @@
|
||||
pub use self::myerror::MyError;
|
||||
pub use self::nutrition::Nutrition;
|
||||
pub use self::purchases::Purchase;
|
||||
mod myerror;
|
||||
mod nutrition;
|
||||
mod purchases;
|
||||
|
14
shared/Cargo.toml
Normal file
14
shared/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "shared"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[features]
|
||||
sql = ["mysql", "mysql_common", "struct-field-names-as-array"]
|
||||
|
||||
[dependencies]
|
||||
cfg-if = "1.0.0"
|
||||
mysql = { version = "25.0.1", optional = true }
|
||||
mysql_common = { version = "0.32.4", features = ["frunk"], optional = true }
|
||||
serde = { version = "1.0.208", features = ["derive"] }
|
||||
struct-field-names-as-array = { version = "0.3.0", optional = true }
|
2
shared/src/lib.rs
Normal file
2
shared/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
||||
#![feature(iter_intersperse)]
|
||||
pub mod structs;
|
2
shared/src/structs.rs
Normal file
2
shared/src/structs.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub use self::nutrition::Nutrition;
|
||||
mod nutrition;
|
@ -1,32 +1,39 @@
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "sql")] {
|
||||
use mysql::prelude::*;
|
||||
use mysql::*;
|
||||
use mysql_common::frunk::{hlist_pat, HList};
|
||||
use std::collections::HashMap;
|
||||
use struct_field_names_as_array::FieldNamesAsArray;
|
||||
} else {}
|
||||
}
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, PartialEq, FieldNamesAsArray)]
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "sql", derive(FieldNamesAsArray))]
|
||||
pub struct Nutrition {
|
||||
id: u32,
|
||||
name: String,
|
||||
manufacturer: String,
|
||||
barcode: String,
|
||||
amount: u32,
|
||||
divisor: u32,
|
||||
kJ: f32,
|
||||
kcal: f32,
|
||||
saturated_fat: f32,
|
||||
carbohydrate: f32,
|
||||
sugar: f32,
|
||||
fibres: f32,
|
||||
protein: f32,
|
||||
salt: f32,
|
||||
vitamin_b2: f32,
|
||||
vitamin_b12: f32,
|
||||
calcium: f32,
|
||||
phosphor: f32,
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub manufacturer: String,
|
||||
pub barcode: String,
|
||||
pub amount: u32,
|
||||
pub divisor: u32,
|
||||
pub kJ: f32,
|
||||
pub kcal: f32,
|
||||
pub saturated_fat: f32,
|
||||
pub carbohydrate: f32,
|
||||
pub sugar: f32,
|
||||
pub fibres: f32,
|
||||
pub protein: f32,
|
||||
pub salt: f32,
|
||||
pub vitamin_b2: f32,
|
||||
pub vitamin_b12: f32,
|
||||
pub calcium: f32,
|
||||
pub phosphor: f32,
|
||||
}
|
||||
|
||||
#[cfg(feature = "sql")]
|
||||
type RowType = HList!(
|
||||
u32,
|
||||
Option<String>,
|
||||
@ -49,6 +56,7 @@ type RowType = HList!(
|
||||
);
|
||||
|
||||
impl Nutrition {
|
||||
#[cfg(feature = "sql")]
|
||||
fn get_sql_fields() -> String {
|
||||
Nutrition::FIELD_NAMES_AS_ARRAY
|
||||
.iter()
|
||||
@ -57,6 +65,7 @@ impl Nutrition {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(feature = "sql")]
|
||||
fn query_map_helper() -> (String, impl Fn(RowType) -> Nutrition) {
|
||||
let sql_query = format!("SELECT {} from nutrition", Self::get_sql_fields());
|
||||
|
||||
@ -106,7 +115,8 @@ impl Nutrition {
|
||||
(sql_query, construction_closure)
|
||||
}
|
||||
|
||||
fn get_nutrition_rows(sql_pool: Pool) -> Result<Vec<Nutrition>> {
|
||||
#[cfg(feature = "sql")]
|
||||
pub async fn get_nutrition_rows(sql_pool: Pool) -> Result<Vec<Nutrition>> {
|
||||
let mut conn = sql_pool
|
||||
.get_conn()
|
||||
.expect("Cannot establish database connection");
|
||||
@ -119,8 +129,9 @@ impl Nutrition {
|
||||
Ok(nutrition_val)
|
||||
}
|
||||
|
||||
#[cfg(feature = "sql")]
|
||||
pub async fn get_nutrition_hashmap(sql_pool: Pool) -> Result<HashMap<u32, Nutrition>> {
|
||||
let nutrition_vec = Self::get_nutrition_rows(sql_pool)?;
|
||||
let nutrition_vec = Self::get_nutrition_rows(sql_pool).await?;
|
||||
let nutrition_hashmap: HashMap<u32, Nutrition> =
|
||||
nutrition_vec.into_iter().map(|n| (n.id, n)).collect();
|
||||
Ok(nutrition_hashmap)
|
||||
@ -138,4 +149,20 @@ impl Nutrition {
|
||||
// changed in any way, this function should return true
|
||||
self.amount != 0 && self.divisor != 0 && self.kcal != 0.
|
||||
}
|
||||
|
||||
pub fn id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
pub fn manufacturer(&self) -> String {
|
||||
self.manufacturer.clone()
|
||||
}
|
||||
|
||||
pub fn barcode(&self) -> String {
|
||||
self.barcode.clone()
|
||||
}
|
||||
}
|
@ -2,7 +2,32 @@
|
||||
<video id="video">Camera not available.</video>
|
||||
<canvas id="canvas" hidden> </canvas>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<div id="myDropdown" class="dropdown-content">
|
||||
<input type="text" placeholder="Search.." id="myInput">
|
||||
<div id="dropdownOptions" class="dropdown-options"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button id="scan_barcode_button">Scan barcode</button>
|
||||
<input id="barcode"/>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Valgt vare</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td><b>ID:</b></td>
|
||||
<td id="id"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Barcode:</b></td>
|
||||
<td id="barcode"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Name:</b></td>
|
||||
<td id="name"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Manufacturer:</b></td> <td id="manufacturer"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user