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 = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum-core",
|
"axum-core",
|
||||||
|
"axum-macros",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
@ -225,6 +226,18 @@ dependencies = [
|
|||||||
"tracing",
|
"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]]
|
[[package]]
|
||||||
name = "backtrace"
|
name = "backtrace"
|
||||||
version = "0.3.73"
|
version = "0.3.73"
|
||||||
@ -455,7 +468,10 @@ dependencies = [
|
|||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"image 0.25.2",
|
"image 0.25.2",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
"nucleo-matcher",
|
||||||
"rxing",
|
"rxing",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"shared",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
@ -472,6 +488,8 @@ dependencies = [
|
|||||||
"mysql_common",
|
"mysql_common",
|
||||||
"plotters",
|
"plotters",
|
||||||
"plotters-canvas",
|
"plotters-canvas",
|
||||||
|
"serde",
|
||||||
|
"shared",
|
||||||
"struct-field-names-as-array",
|
"struct-field-names-as-array",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -1933,6 +1951,16 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
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]]
|
[[package]]
|
||||||
name = "num"
|
name = "num"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@ -2797,6 +2825,17 @@ dependencies = [
|
|||||||
"serde_derive",
|
"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]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.208"
|
version = "1.0.208"
|
||||||
@ -2873,6 +2912,17 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shared"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"mysql",
|
||||||
|
"mysql_common",
|
||||||
|
"serde",
|
||||||
|
"struct-field-names-as-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["server", "browser"]
|
members = ["server", "browser", "shared"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
@ -20,10 +20,14 @@ console_error_panic_hook = { version = "0.1.7", optional = true }
|
|||||||
|
|
||||||
rxing = "0.6.1"
|
rxing = "0.6.1"
|
||||||
image = "0.25.2"
|
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"
|
wasm-bindgen-futures = "0.4.43"
|
||||||
js-sys = "0.3.70"
|
js-sys = "0.3.70"
|
||||||
|
|
||||||
|
shared = { path = "../shared" }
|
||||||
|
serde-wasm-bindgen = "0.6.5"
|
||||||
|
nucleo-matcher = "0.3.1"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
# Tell `rustc` to optimize for small code size.
|
# Tell `rustc` to optimize for small code size.
|
||||||
opt-level = "s"
|
opt-level = "s"
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
#![feature(iter_intersperse)]
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use image::{DynamicImage, RgbaImage};
|
use image::{DynamicImage, RgbaImage};
|
||||||
|
use shared::structs::Nutrition;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
use wasm_bindgen_futures::JsFuture;
|
||||||
use web_sys::*;
|
use web_sys::*;
|
||||||
|
|
||||||
const CANVAS_WIDTH: u32 = 1920;
|
const CANVAS_WIDTH: u32 = 1920;
|
||||||
@ -11,6 +14,11 @@ const VIDEO_ID: &str = "video";
|
|||||||
const CANVAS_ID: &str = "canvas";
|
const CANVAS_ID: &str = "canvas";
|
||||||
const BARCODE_ID: &str = "barcode";
|
const BARCODE_ID: &str = "barcode";
|
||||||
const SCAN_BUTTON_ID: &str = "scan_barcode_button";
|
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]
|
#[wasm_bindgen]
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@ -24,8 +32,36 @@ extern "C" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn init_camera_feed() -> Result<(), String> {
|
pub fn enable_panic_debug() {
|
||||||
utils::set_panic_hook();
|
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 window = window().unwrap();
|
||||||
let document = window.document().unwrap();
|
let document = window.document().unwrap();
|
||||||
@ -46,6 +82,11 @@ pub fn init_camera_feed() -> Result<(), String> {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.dyn_into()
|
.dyn_into()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let search: HtmlInputElement = document
|
||||||
|
.get_element_by_id(SEARCH_FIELD_ID)
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into()
|
||||||
|
.unwrap();
|
||||||
let navigator = window.navigator();
|
let navigator = window.navigator();
|
||||||
let media_devices = navigator.media_devices().unwrap();
|
let media_devices = navigator.media_devices().unwrap();
|
||||||
let media_stream_constraints = MediaStreamConstraints::new();
|
let media_stream_constraints = MediaStreamConstraints::new();
|
||||||
@ -59,14 +100,14 @@ pub fn init_camera_feed() -> Result<(), String> {
|
|||||||
let _ = video.play();
|
let _ = video.play();
|
||||||
});
|
});
|
||||||
let fail: Closure<dyn FnMut(JsValue)> = Closure::new(|err| {
|
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);
|
let _ = user_media.then(&succ).catch(&fail);
|
||||||
succ.forget(); // Memory leak
|
succ.forget(); // Memory leak
|
||||||
fail.forget(); // Memory leak
|
fail.forget(); // Memory leak
|
||||||
|
|
||||||
let set_width_and_height: Closure<dyn FnMut()> = Closure::new(move || {
|
let set_width_and_height: Closure<dyn FnMut()> = Closure::new(move || {
|
||||||
let width: u32 = 640;
|
let width: u32 = 300;
|
||||||
let height: u32 =
|
let height: u32 =
|
||||||
((width as f32 / video3.video_width() as f32) * video3.video_height() as f32) as 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
|
set_width_and_height.forget(); // Memory leak
|
||||||
//
|
//
|
||||||
let scan_barcode_wrapper: Closure<dyn FnMut()> = Closure::new(move || {
|
let scan_barcode_wrapper: Closure<dyn FnMut()> = Closure::new(move || {
|
||||||
match scan_barcode() {
|
match scan_barcode(product_options.clone()) {
|
||||||
Ok(()) => {}
|
Ok(()) => {}
|
||||||
Err(e) => error(e.as_str()),
|
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());
|
.add_event_listener_with_callback("click", scan_barcode_wrapper.as_ref().unchecked_ref());
|
||||||
scan_barcode_wrapper.forget(); // Memory leak
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +191,7 @@ fn decode_barcode(data: Vec<u8>, width: u32, height: u32) -> Result<String, rxin
|
|||||||
Ok(result.getText().to_string())
|
Ok(result.getText().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scan_barcode() -> Result<(), String> {
|
fn scan_barcode(product_options: Vec<Nutrition>) -> Result<(), String> {
|
||||||
let window = window().unwrap();
|
let window = window().unwrap();
|
||||||
let document = window.document().unwrap();
|
let document = window.document().unwrap();
|
||||||
let video: HtmlVideoElement = document
|
let video: HtmlVideoElement = document
|
||||||
@ -127,11 +204,6 @@ fn scan_barcode() -> Result<(), String> {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.dyn_into()
|
.dyn_into()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let barcode: HtmlInputElement = document
|
|
||||||
.get_element_by_id(BARCODE_ID)
|
|
||||||
.unwrap()
|
|
||||||
.dyn_into()
|
|
||||||
.unwrap();
|
|
||||||
let context: CanvasRenderingContext2d = match canvas.get_context("2d").unwrap() {
|
let context: CanvasRenderingContext2d = match canvas.get_context("2d").unwrap() {
|
||||||
Some(c) => c.dyn_into().unwrap(),
|
Some(c) => c.dyn_into().unwrap(),
|
||||||
None => return Err("Could not get canvas context".to_string()),
|
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)),
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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();
|
await init();
|
||||||
init_camera_feed();
|
enable_panic_debug();
|
||||||
|
|
||||||
// (() => {
|
let product_options = await get_product_options();
|
||||||
|
fill_search_options(product_options);
|
||||||
// // The width and height of the captured photo. We will set the
|
init_camera_feed(product_options);
|
||||||
// // 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);
|
|
||||||
// })();
|
|
||||||
|
4
run.sh
4
run.sh
@ -1,4 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
cd shared || exit;
|
||||||
|
cargo build || exit;
|
||||||
|
cd ..;
|
||||||
|
|
||||||
cd browser || exit;
|
cd browser || exit;
|
||||||
wasm-pack build --target web || exit;
|
wasm-pack build --target web || exit;
|
||||||
cd ..;
|
cd ..;
|
||||||
|
@ -5,14 +5,16 @@ edition.workspace = true
|
|||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.7.5"
|
axum = { version = "0.7.5", features = ["macros"] }
|
||||||
chrono = "0.4.38"
|
chrono = "0.4.38"
|
||||||
image = "0.25.2"
|
image = "0.25.2"
|
||||||
mysql = "25.0.1"
|
mysql = "25.0.1"
|
||||||
mysql_common = { version = "0.32.4", features = ["frunk"] }
|
mysql_common = { version = "0.32.4", features = ["frunk"] }
|
||||||
plotters = "0.3.6"
|
plotters = "0.3.6"
|
||||||
plotters-canvas = "0.3.0"
|
plotters-canvas = "0.3.0"
|
||||||
|
serde = "1.0.208"
|
||||||
struct-field-names-as-array = "0.3.0"
|
struct-field-names-as-array = "0.3.0"
|
||||||
time = "0.3.36"
|
time = "0.3.36"
|
||||||
tokio = { version = "1.39.2", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.39.2", features = ["macros", "rt-multi-thread"] }
|
||||||
tower-http = { version = "0.5.2", features = ["fs"] }
|
tower-http = { version = "0.5.2", features = ["fs"] }
|
||||||
|
shared = { path = "../shared", features = ["sql"] }
|
||||||
|
@ -9,7 +9,7 @@ use axum::{
|
|||||||
},
|
},
|
||||||
response::{Html, IntoResponse},
|
response::{Html, IntoResponse},
|
||||||
routing::{get, get_service},
|
routing::{get, get_service},
|
||||||
Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
// use image::ImageFormat;
|
// use image::ImageFormat;
|
||||||
// use mysql::prelude::*;
|
// use mysql::prelude::*;
|
||||||
@ -19,12 +19,15 @@ use mysql::*;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
mod math;
|
mod math;
|
||||||
mod my_structs;
|
mod my_structs;
|
||||||
|
use my_structs::MyError;
|
||||||
|
use my_structs::Purchase;
|
||||||
mod plotting;
|
mod plotting;
|
||||||
// use crate::my_structs::MyError;
|
|
||||||
// use crate::plotting::Plotter;
|
// use crate::plotting::Plotter;
|
||||||
use std::{thread, time};
|
use std::{thread, time};
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
|
|
||||||
|
use shared::structs::Nutrition;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct AppState {
|
struct AppState {
|
||||||
sql_pool: Pool,
|
sql_pool: Pool,
|
||||||
@ -59,7 +62,10 @@ async fn main() {
|
|||||||
.route("/calorie_intake", get(calorie_intake))
|
.route("/calorie_intake", get(calorie_intake))
|
||||||
.route("/wasm_test", get(wasm_test))
|
.route("/wasm_test", get(wasm_test))
|
||||||
.route("/camera_test", get(camera_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("/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)
|
.with_state(shared_state)
|
||||||
.fallback(not_found);
|
.fallback(not_found);
|
||||||
|
|
||||||
@ -72,10 +78,9 @@ async fn not_found(uri: Uri) -> (StatusCode, String) {
|
|||||||
(StatusCode::NOT_FOUND, format!("404 not found: {uri}"))
|
(StatusCode::NOT_FOUND, format!("404 not found: {uri}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_rows(State(state): State<Arc<AppState>>) -> Result<String, my_structs::MyError> {
|
async fn get_rows(State(state): State<Arc<AppState>>) -> Result<String, MyError> {
|
||||||
let nutrition_val_promise =
|
let nutrition_val_promise = Nutrition::get_nutrition_hashmap(state.sql_pool.clone());
|
||||||
my_structs::Nutrition::get_nutrition_hashmap(state.sql_pool.clone());
|
let purchases_val_promise = Purchase::get_purchase_rows(state.sql_pool.clone());
|
||||||
let purchases_val_promise = my_structs::Purchase::get_purchase_rows(state.sql_pool.clone());
|
|
||||||
|
|
||||||
let nutrition_val = nutrition_val_promise.await?;
|
let nutrition_val = nutrition_val_promise.await?;
|
||||||
let purchases_val = purchases_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))
|
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> {
|
async fn html_demo(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
println!("Serving: html_demo");
|
println!("Serving: html_demo");
|
||||||
|
|
||||||
@ -99,12 +116,11 @@ async fn svg_image(State(_state): State<Arc<AppState>>) -> impl IntoResponse {
|
|||||||
|
|
||||||
async fn svg_calorie_intake(
|
async fn svg_calorie_intake(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
) -> Result<impl IntoResponse, my_structs::MyError> {
|
) -> Result<impl IntoResponse, MyError> {
|
||||||
println!("Serving: svg_calorie_intake");
|
println!("Serving: svg_calorie_intake");
|
||||||
|
|
||||||
let nutrition_val_promise =
|
let nutrition_val_promise = Nutrition::get_nutrition_hashmap(state.sql_pool.clone());
|
||||||
my_structs::Nutrition::get_nutrition_hashmap(state.sql_pool.clone());
|
let purchases_val_promise = Purchase::get_purchase_rows(state.sql_pool.clone());
|
||||||
let purchases_val_promise = my_structs::Purchase::get_purchase_rows(state.sql_pool.clone());
|
|
||||||
|
|
||||||
let nutrition_val = nutrition_val_promise.await?;
|
let nutrition_val = nutrition_val_promise.await?;
|
||||||
let purchases_val = purchases_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())
|
Html("<body><img src=/calorie_intake.svg></img></body>".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn construct_js(path: &str) -> Result<String, my_structs::MyError> {
|
fn construct_js(path: &str) -> Result<String, MyError> {
|
||||||
let javascript = std::fs::read_to_string(format!("../javascript/{}", path))?;
|
// let javascript = std::fs::read_to_string(format!("../javascript/{}", path))?;
|
||||||
let module = format!(
|
// let module = format!(
|
||||||
"
|
// "
|
||||||
<script type=\"module\">
|
// <script type=\"module\">
|
||||||
{}
|
// {}
|
||||||
</script>
|
// </script>
|
||||||
",
|
// ",
|
||||||
javascript
|
// javascript
|
||||||
);
|
// );
|
||||||
Ok(module)
|
// 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))?)
|
Ok(std::fs::read_to_string(format!("../templates/{}", path))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn construct_html(
|
fn construct_html(
|
||||||
js_paths: Vec<&str>,
|
js_paths: Vec<&str>,
|
||||||
|
css_paths: Vec<&str>,
|
||||||
tmpl_paths: Vec<&str>,
|
tmpl_paths: Vec<&str>,
|
||||||
) -> Result<String, my_structs::MyError> {
|
) -> Result<String, MyError> {
|
||||||
let js_modules: Vec<String> = js_paths
|
let js_modules: Vec<String> = js_paths
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(construct_js)
|
.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
|
let tmpl_snippets: Vec<String> = tmpl_paths
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(construct_tmpl)
|
.map(construct_tmpl)
|
||||||
.collect::<Result<Vec<String>, my_structs::MyError>>()?;
|
.collect::<Result<Vec<String>, MyError>>()?;
|
||||||
|
|
||||||
let html = format!(
|
let html = format!(
|
||||||
"
|
"
|
||||||
@ -168,24 +198,24 @@ fn construct_html(
|
|||||||
<html lang=\"en\">
|
<html lang=\"en\">
|
||||||
<head>
|
<head>
|
||||||
{}
|
{}
|
||||||
|
{}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{}
|
{}
|
||||||
</body>
|
</body>
|
||||||
",
|
",
|
||||||
js_modules.join(""),
|
js_modules.join(""),
|
||||||
|
css_styling.join(""),
|
||||||
tmpl_snippets.join("")
|
tmpl_snippets.join("")
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(html)
|
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))?)
|
Ok(std::fs::read_to_string(format!("../templates/{}", path))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wasm_test(
|
async fn wasm_test(State(_state): State<Arc<AppState>>) -> Result<Html<String>, MyError> {
|
||||||
State(_state): State<Arc<AppState>>,
|
|
||||||
) -> Result<Html<String>, my_structs::MyError> {
|
|
||||||
println!("Serving: wasm_test");
|
println!("Serving: wasm_test");
|
||||||
|
|
||||||
let content = get_template("view_calories.html")?;
|
let content = get_template("view_calories.html")?;
|
||||||
@ -193,12 +223,14 @@ async fn wasm_test(
|
|||||||
Ok(Html(content))
|
Ok(Html(content))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn camera_test(
|
async fn camera_test(State(_state): State<Arc<AppState>>) -> Result<Html<String>, MyError> {
|
||||||
State(_state): State<Arc<AppState>>,
|
|
||||||
) -> Result<Html<String>, my_structs::MyError> {
|
|
||||||
println!("Serving: camera_test");
|
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))
|
Ok(Html(html))
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ use crate::plotting;
|
|||||||
// use chrono::TimeZone;
|
// use chrono::TimeZone;
|
||||||
use chrono::{DateTime, NaiveDate, Utc};
|
use chrono::{DateTime, NaiveDate, Utc};
|
||||||
use plotters::prelude::*;
|
use plotters::prelude::*;
|
||||||
|
use shared::structs::Nutrition;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use time::Date;
|
use time::Date;
|
||||||
@ -16,7 +17,7 @@ fn time_date_to_chrono_naive_date(d: Date) -> chrono::NaiveDate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_calories_per_day(
|
fn calculate_calories_per_day(
|
||||||
nutrition_map: HashMap<u32, my_structs::Nutrition>,
|
nutrition_map: HashMap<u32, Nutrition>,
|
||||||
purchases: Vec<my_structs::Purchase>,
|
purchases: Vec<my_structs::Purchase>,
|
||||||
) -> (Date, Vec<f32>) {
|
) -> (Date, Vec<f32>) {
|
||||||
if purchases.is_empty() {
|
if purchases.is_empty() {
|
||||||
@ -76,7 +77,7 @@ fn calculate_calories_per_day(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn plot_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>,
|
purchases: Vec<my_structs::Purchase>,
|
||||||
) -> plotting::SVGPlotFun {
|
) -> plotting::SVGPlotFun {
|
||||||
let (mindate, calorie_days) = calculate_calories_per_day(nutrition_map, purchases);
|
let (mindate, calorie_days) = calculate_calories_per_day(nutrition_map, purchases);
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
pub use self::myerror::MyError;
|
pub use self::myerror::MyError;
|
||||||
pub use self::nutrition::Nutrition;
|
|
||||||
pub use self::purchases::Purchase;
|
pub use self::purchases::Purchase;
|
||||||
mod myerror;
|
mod myerror;
|
||||||
mod nutrition;
|
|
||||||
mod purchases;
|
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 @@
|
|||||||
use mysql::prelude::*;
|
cfg_if::cfg_if! {
|
||||||
use mysql::*;
|
if #[cfg(feature = "sql")] {
|
||||||
use mysql_common::frunk::{hlist_pat, HList};
|
use mysql::prelude::*;
|
||||||
use std::collections::HashMap;
|
use mysql::*;
|
||||||
use struct_field_names_as_array::FieldNamesAsArray;
|
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)]
|
#[allow(non_snake_case)]
|
||||||
#[derive(Debug, PartialEq, FieldNamesAsArray)]
|
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "sql", derive(FieldNamesAsArray))]
|
||||||
pub struct Nutrition {
|
pub struct Nutrition {
|
||||||
id: u32,
|
pub id: u32,
|
||||||
name: String,
|
pub name: String,
|
||||||
manufacturer: String,
|
pub manufacturer: String,
|
||||||
barcode: String,
|
pub barcode: String,
|
||||||
amount: u32,
|
pub amount: u32,
|
||||||
divisor: u32,
|
pub divisor: u32,
|
||||||
kJ: f32,
|
pub kJ: f32,
|
||||||
kcal: f32,
|
pub kcal: f32,
|
||||||
saturated_fat: f32,
|
pub saturated_fat: f32,
|
||||||
carbohydrate: f32,
|
pub carbohydrate: f32,
|
||||||
sugar: f32,
|
pub sugar: f32,
|
||||||
fibres: f32,
|
pub fibres: f32,
|
||||||
protein: f32,
|
pub protein: f32,
|
||||||
salt: f32,
|
pub salt: f32,
|
||||||
vitamin_b2: f32,
|
pub vitamin_b2: f32,
|
||||||
vitamin_b12: f32,
|
pub vitamin_b12: f32,
|
||||||
calcium: f32,
|
pub calcium: f32,
|
||||||
phosphor: f32,
|
pub phosphor: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "sql")]
|
||||||
type RowType = HList!(
|
type RowType = HList!(
|
||||||
u32,
|
u32,
|
||||||
Option<String>,
|
Option<String>,
|
||||||
@ -49,6 +56,7 @@ type RowType = HList!(
|
|||||||
);
|
);
|
||||||
|
|
||||||
impl Nutrition {
|
impl Nutrition {
|
||||||
|
#[cfg(feature = "sql")]
|
||||||
fn get_sql_fields() -> String {
|
fn get_sql_fields() -> String {
|
||||||
Nutrition::FIELD_NAMES_AS_ARRAY
|
Nutrition::FIELD_NAMES_AS_ARRAY
|
||||||
.iter()
|
.iter()
|
||||||
@ -57,6 +65,7 @@ impl Nutrition {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "sql")]
|
||||||
fn query_map_helper() -> (String, impl Fn(RowType) -> Nutrition) {
|
fn query_map_helper() -> (String, impl Fn(RowType) -> Nutrition) {
|
||||||
let sql_query = format!("SELECT {} from nutrition", Self::get_sql_fields());
|
let sql_query = format!("SELECT {} from nutrition", Self::get_sql_fields());
|
||||||
|
|
||||||
@ -106,7 +115,8 @@ impl Nutrition {
|
|||||||
(sql_query, construction_closure)
|
(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
|
let mut conn = sql_pool
|
||||||
.get_conn()
|
.get_conn()
|
||||||
.expect("Cannot establish database connection");
|
.expect("Cannot establish database connection");
|
||||||
@ -119,8 +129,9 @@ impl Nutrition {
|
|||||||
Ok(nutrition_val)
|
Ok(nutrition_val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "sql")]
|
||||||
pub async fn get_nutrition_hashmap(sql_pool: Pool) -> Result<HashMap<u32, Nutrition>> {
|
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> =
|
let nutrition_hashmap: HashMap<u32, Nutrition> =
|
||||||
nutrition_vec.into_iter().map(|n| (n.id, n)).collect();
|
nutrition_vec.into_iter().map(|n| (n.id, n)).collect();
|
||||||
Ok(nutrition_hashmap)
|
Ok(nutrition_hashmap)
|
||||||
@ -138,4 +149,20 @@ impl Nutrition {
|
|||||||
// changed in any way, this function should return true
|
// changed in any way, this function should return true
|
||||||
self.amount != 0 && self.divisor != 0 && self.kcal != 0.
|
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>
|
<video id="video">Camera not available.</video>
|
||||||
<canvas id="canvas" hidden> </canvas>
|
<canvas id="canvas" hidden> </canvas>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<button id="scan_barcode_button">Scan barcode</button>
|
<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>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user