Scan barcode and dropdown with search works

This commit is contained in:
Knyffen 2024-08-21 20:16:31 +02:00
parent 9cb3463826
commit 08f92e21a5
16 changed files with 611 additions and 158 deletions

50
Cargo.lock generated
View File

@ -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"

View File

@ -1,5 +1,5 @@
[workspace] [workspace]
members = ["server", "browser"] members = ["server", "browser", "shared"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]

View File

@ -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"

View File

@ -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
View 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;}

View File

@ -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
View File

@ -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 ..;

View File

@ -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"] }

View File

@ -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))
} }

View File

@ -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);

View File

@ -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
View 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
View File

@ -0,0 +1,2 @@
#![feature(iter_intersperse)]
pub mod structs;

2
shared/src/structs.rs Normal file
View File

@ -0,0 +1,2 @@
pub use self::nutrition::Nutrition;
mod nutrition;

View File

@ -1,32 +1,39 @@
cfg_if::cfg_if! {
if #[cfg(feature = "sql")] {
use mysql::prelude::*; use mysql::prelude::*;
use mysql::*; use mysql::*;
use mysql_common::frunk::{hlist_pat, HList}; use mysql_common::frunk::{hlist_pat, HList};
use std::collections::HashMap; use std::collections::HashMap;
use struct_field_names_as_array::FieldNamesAsArray; 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()
}
} }

View File

@ -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>