diff --git a/Cargo.lock b/Cargo.lock index 19ea2c7..8e8c8c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 6eb889f..99b4ae7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["server", "browser"] +members = ["server", "browser", "shared"] resolver = "2" [workspace.package] diff --git a/browser/Cargo.toml b/browser/Cargo.toml index ca47e51..bcf5ff6 100644 --- a/browser/Cargo.toml +++ b/browser/Cargo.toml @@ -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" diff --git a/browser/src/lib.rs b/browser/src/lib.rs index a7c87ef..22f9cdb 100644 --- a/browser/src/lib.rs +++ b/browser/src/lib.rs @@ -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 = + 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 = 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 = 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 = 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 = Closure::new(|| { + match show_search_options() { + Ok(()) => {} + Err(e) => error(format!("Failed to show search options: {:?}", e).as_str()), + }; + }); + + let _ = search.add_event_listener_with_callback( + "focusin", + show_search_options_wrapper.as_ref().unchecked_ref(), + ); + show_search_options_wrapper.forget(); // Memory leak + // + let hide_search_options_wrapper: Closure = + Closure::new(|event: JsValue| { + match hide_search_options(event) { + Ok(()) => {} + Err(e) => error(format!("Failed to hide search options: {:?}", e).as_str()), + }; + }); + + let _ = search.add_event_listener_with_callback( + "focusout", + hide_search_options_wrapper.as_ref().unchecked_ref(), + ); + hide_search_options_wrapper.forget(); // Memory leak + // + let filter_search_options_wrapper: Closure = + Closure::new(|| filter_search_options()); + + let _ = search.add_event_listener_with_callback( + "keyup", + filter_search_options_wrapper.as_ref().unchecked_ref(), + ); + filter_search_options_wrapper.forget(); // Memory leak + Ok(()) } @@ -114,7 +191,7 @@ fn decode_barcode(data: Vec, width: u32, height: u32) -> Result Result<(), String> { +fn scan_barcode(product_options: Vec) -> 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 { + let opts = RequestInit::new(); + opts.set_method("GET"); + opts.set_mode(RequestMode::Cors); + + let request = Request::new_with_str_and_init(url.as_str(), &opts)?; + + request.headers().set("Accept", "application/json")?; + + let window = web_sys::window().unwrap(); + let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?; + + // `resp_value` is a `Response` object. + assert!(resp_value.is_instance_of::()); + let resp: Response = resp_value.dyn_into().unwrap(); + + // Convert this other `Promise` into a rust `Future`. + let json = JsFuture::from(resp.json()?).await?; + + Ok(json) +} + +#[wasm_bindgen] +pub async fn get_product_options() -> Result { + let url = "/get_product_options"; + + let json = request_json(url.to_string()).await?; + + Ok(json) +} + +fn set_product(product: &Nutrition) { + let window = window().unwrap(); + let document = window.document().unwrap(); + let id: HtmlElement = document + .get_element_by_id(RESULT_ID_ID) + .unwrap() + .dyn_into() + .unwrap(); + let barcode: HtmlElement = document + .get_element_by_id(BARCODE_ID) + .unwrap() + .dyn_into() + .unwrap(); + let name: HtmlElement = document + .get_element_by_id(RESULT_NAME_ID) + .unwrap() + .dyn_into() + .unwrap(); + let manufacturer: HtmlElement = document + .get_element_by_id(RESULT_MANUFACTURER_ID) + .unwrap() + .dyn_into() + .unwrap(); + + id.set_inner_html(&product.id().to_string()); + barcode.set_inner_html(&product.barcode()); + name.set_inner_html(&product.name()); + manufacturer.set_inner_html(&product.manufacturer()); +} + +#[wasm_bindgen] +pub fn fill_search_options(product_options_js: JsValue) { + let product_options: Vec = + serde_wasm_bindgen::from_value(product_options_js).unwrap(); + + let nutrition_to_option = |n: &Nutrition| -> String { + format!( + "
{} - {}
", + n.id(), + n.name(), + n.manufacturer() + ) + }; + + let window = window().unwrap(); + let document = window.document().unwrap(); + let search_options: HtmlElement = document + .get_element_by_id(SEARCH_FIELD_OPTIONS_ID) + .unwrap() + .dyn_into() + .unwrap(); + + let html: String = product_options.iter().map(nutrition_to_option).collect(); + + search_options.set_inner_html(&html); + + product_options.iter().cloned().for_each(|x| { + let elem: HtmlElement = document + .get_element_by_id(format!("option{}", x.id()).as_str()) + .unwrap() + .dyn_into() + .unwrap(); + let callback_fun: Closure = Closure::new(move || { + set_product(&x); + }); + let _ = + elem.add_event_listener_with_callback("click", callback_fun.as_ref().unchecked_ref()); + callback_fun.forget(); + }); +} + +fn get_search_options() -> Vec { + window() + .and_then(|x| x.document()) + .and_then(|x| x.query_selector_all(".dropdown-options div").ok()) + .map(|nodelist| { + nodelist + .values() + .into_iter() + .filter_map(|x| x.ok()) + .map(|x| x.dyn_into::()) + .filter_map(|x| x.ok()) + .collect() + }) + .unwrap_or_default() +} + +fn show_search_options() -> Result<(), JsValue> { + get_search_options() + .into_iter() + .for_each(|elem| elem.class_list().add_1("show").unwrap()); + + Ok(()) +} + +fn hide_search_options(event_js: JsValue) -> Result<(), JsValue> { + get_search_options() + .into_iter() + .for_each(|elem| elem.class_list().remove_1("show").unwrap()); + + if let Ok(event) = event_js.dyn_into::() { + if let Some(search_elem) = event + .target() + .and_then(|x| x.dyn_into::().ok()) + { + if let Some(related_target_elem) = event + .related_target() + .and_then(|x| x.dyn_into::().ok()) + { + if let Some(search_elem_2) = related_target_elem + .parent_element() + .and_then(|x| x.previous_element_sibling()) + .and_then(|x| x.dyn_into::().ok()) + { + if search_elem.eq(&search_elem_2) { + related_target_elem.click(); + } + } + } + } + } + + Ok(()) +} + +fn filter_search_options() { + let search_options = get_search_options(); + let search_string: String = window() + .unwrap() + .document() + .unwrap() + .get_element_by_id(SEARCH_FIELD_ID) + .unwrap() + .dyn_into::() + .unwrap() + .value(); + + let mut matcher = nucleo_matcher::Matcher::new(nucleo_matcher::Config::DEFAULT); + let parser = nucleo_matcher::pattern::Pattern::parse( + search_string.as_str(), + nucleo_matcher::pattern::CaseMatching::Ignore, + nucleo_matcher::pattern::Normalization::Smart, + ); + + let search_option_map: HashMap = search_options + .clone() + .into_iter() + .map(|x| (x.inner_text(), x)) + .collect(); + + let mut search_res = parser.match_list(search_option_map.keys(), &mut matcher); + search_res.sort_unstable_by_key(|x| (x.1, x.0)); + + // Note that this way the children are sorted the same order as search_res + let shown_children: Vec = search_res + .into_iter() + .map(|x| search_option_map.get(x.0).unwrap().clone()) + .collect(); + + let hidden_children: Vec = search_options + .into_iter() + .filter(|x| shown_children.iter().all(|y| x != y)) + .collect(); + + let window = window().unwrap(); + let document = window.document().unwrap(); + let search_options_field: HtmlElement = document + .get_element_by_id(SEARCH_FIELD_OPTIONS_ID) + .unwrap() + .dyn_into() + .unwrap(); + + let children_arr: js_sys::Array = shown_children + .iter() + .chain(hidden_children.iter()) + .clone() + .map(JsValue::from) + .collect(); + + search_options_field.replace_children_with_node(&children_arr.into_iter().collect()); + + hidden_children + .into_iter() + .for_each(|elem| elem.class_list().remove_1("show").unwrap()); + + shown_children + .into_iter() + .for_each(|elem| elem.class_list().add_1("show").unwrap()); +} diff --git a/css/camera_test.css b/css/camera_test.css new file mode 100644 index 0000000..bee053e --- /dev/null +++ b/css/camera_test.css @@ -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
- 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;} diff --git a/javascript/camera.js b/javascript/camera.js index 90a0935..0490e1c 100644 --- a/javascript/camera.js +++ b/javascript/camera.js @@ -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); diff --git a/run.sh b/run.sh index 892d318..572e5e8 100755 --- a/run.sh +++ b/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 ..; diff --git a/server/Cargo.toml b/server/Cargo.toml index db9aef1..a93549c 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -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"] } diff --git a/server/src/main.rs b/server/src/main.rs index c6d45bb..09cab48 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -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>) -> Result { - 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>) -> Result { + 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>) -> Result>, +) -> Result>, 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>) -> Html { println!("Serving: html_demo"); @@ -99,12 +116,11 @@ async fn svg_image(State(_state): State>) -> impl IntoResponse { async fn svg_calorie_intake( State(state): State>, -) -> Result { +) -> Result { 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>) -> Html { Html("".to_string()) } -fn construct_js(path: &str) -> Result { - let javascript = std::fs::read_to_string(format!("../javascript/{}", path))?; - let module = format!( - " - -", - javascript - ); - Ok(module) +fn construct_js(path: &str) -> Result { + // let javascript = std::fs::read_to_string(format!("../javascript/{}", path))?; + // let module = format!( + // " + // + // ", + // javascript + // ); + // Ok(module) + Ok(format!( + "", + path + )) } -fn construct_tmpl(path: &str) -> Result { +fn construct_css(path: &str) -> Result { + Ok(format!("", path)) +} + +fn construct_tmpl(path: &str) -> Result { 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 { +) -> Result { let js_modules: Vec = js_paths .into_iter() .map(construct_js) - .collect::, my_structs::MyError>>()?; + .collect::, MyError>>()?; + + let css_styling: Vec = css_paths + .into_iter() + .map(construct_css) + .collect::, MyError>>()?; let tmpl_snippets: Vec = tmpl_paths .into_iter() .map(construct_tmpl) - .collect::, my_structs::MyError>>()?; + .collect::, MyError>>()?; let html = format!( " @@ -168,24 +198,24 @@ fn construct_html( {} + {} {} ", js_modules.join(""), + css_styling.join(""), tmpl_snippets.join("") ); Ok(html) } -fn get_template(path: &str) -> Result { +fn get_template(path: &str) -> Result { Ok(std::fs::read_to_string(format!("../templates/{}", path))?) } -async fn wasm_test( - State(_state): State>, -) -> Result, my_structs::MyError> { +async fn wasm_test(State(_state): State>) -> Result, 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>, -) -> Result, my_structs::MyError> { +async fn camera_test(State(_state): State>) -> Result, 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)) } diff --git a/server/src/math/calculations.rs b/server/src/math/calculations.rs index 7ac049a..9b9f6fa 100644 --- a/server/src/math/calculations.rs +++ b/server/src/math/calculations.rs @@ -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, + nutrition_map: HashMap, purchases: Vec, ) -> (Date, Vec) { if purchases.is_empty() { @@ -76,7 +77,7 @@ fn calculate_calories_per_day( } pub fn plot_calories_per_day( - nutrition_map: HashMap, + nutrition_map: HashMap, purchases: Vec, ) -> plotting::SVGPlotFun { let (mindate, calorie_days) = calculate_calories_per_day(nutrition_map, purchases); diff --git a/server/src/my_structs.rs b/server/src/my_structs.rs index 2713bf9..14603ff 100644 --- a/server/src/my_structs.rs +++ b/server/src/my_structs.rs @@ -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; diff --git a/shared/Cargo.toml b/shared/Cargo.toml new file mode 100644 index 0000000..eacab87 --- /dev/null +++ b/shared/Cargo.toml @@ -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 } diff --git a/shared/src/lib.rs b/shared/src/lib.rs new file mode 100644 index 0000000..6c81a52 --- /dev/null +++ b/shared/src/lib.rs @@ -0,0 +1,2 @@ +#![feature(iter_intersperse)] +pub mod structs; diff --git a/shared/src/structs.rs b/shared/src/structs.rs new file mode 100644 index 0000000..da295b3 --- /dev/null +++ b/shared/src/structs.rs @@ -0,0 +1,2 @@ +pub use self::nutrition::Nutrition; +mod nutrition; diff --git a/server/src/my_structs/nutrition.rs b/shared/src/structs/nutrition.rs similarity index 70% rename from server/src/my_structs/nutrition.rs rename to shared/src/structs/nutrition.rs index 282d076..5b77173 100644 --- a/server/src/my_structs/nutrition.rs +++ b/shared/src/structs/nutrition.rs @@ -1,32 +1,39 @@ -use mysql::prelude::*; -use mysql::*; -use mysql_common::frunk::{hlist_pat, HList}; -use std::collections::HashMap; -use struct_field_names_as_array::FieldNamesAsArray; +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, @@ -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> { + #[cfg(feature = "sql")] + pub async fn get_nutrition_rows(sql_pool: Pool) -> Result> { 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> { - let nutrition_vec = Self::get_nutrition_rows(sql_pool)?; + let nutrition_vec = Self::get_nutrition_rows(sql_pool).await?; let nutrition_hashmap: HashMap = 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() + } } diff --git a/templates/camera_test.html b/templates/camera_test.html index e8365e1..a56492b 100644 --- a/templates/camera_test.html +++ b/templates/camera_test.html @@ -2,7 +2,32 @@
+
- +
+
+

Valgt vare

+ + + + + + + + + + + + + + + + +
ID:
Barcode:
Name:
Manufacturer: