mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-28 20:29:53 -04:00
Compare commits
12 Commits
1c55a315b0
...
aba1e114cf
Author | SHA1 | Date | |
---|---|---|---|
aba1e114cf | |||
9126d434d5 | |||
548f336e1a | |||
9f3b11a01f | |||
f173204d2d | |||
1f90cc2ef6 | |||
ed44e929f4 | |||
51d165a50b | |||
84cc2a2713 | |||
f75652befa | |||
b93edd2701 | |||
4767115da6 |
1674
Cargo.lock
generated
1674
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -6,10 +6,12 @@ edition = "2021"
|
|||||||
# 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]
|
||||||
serde = "<=1.0.171"
|
|
||||||
recipes = { path = "../recipes" }
|
recipes = { path = "../recipes" }
|
||||||
chrono = "0.4.22"
|
chrono = "0.4.22"
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
version = "1.0.204"
|
||||||
|
features = ["derive"]
|
||||||
|
|
||||||
[dependencies.axum]
|
[dependencies.axum]
|
||||||
version = "0.5.16"
|
version = "0.5.16"
|
||||||
|
@ -18,14 +18,19 @@ async-trait = "0.1.57"
|
|||||||
async-session = "3.0.0"
|
async-session = "3.0.0"
|
||||||
ciborium = "0.2.0"
|
ciborium = "0.2.0"
|
||||||
tower = "0.4.13"
|
tower = "0.4.13"
|
||||||
serde = "<=1.0.171"
|
|
||||||
cookie = "0.17.0"
|
cookie = "0.17.0"
|
||||||
chrono = "0.4.22"
|
|
||||||
metrics = "0.20.1"
|
metrics = "0.20.1"
|
||||||
metrics-exporter-prometheus = "0.11.0"
|
metrics-exporter-prometheus = "0.11.0"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
metrics-process = "1.0.8"
|
metrics-process = "1.0.8"
|
||||||
|
|
||||||
|
[dependencies.chrono]
|
||||||
|
version = "0.4.22"
|
||||||
|
features = ["serde"]
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
version = "1.0.204"
|
||||||
|
|
||||||
[dependencies.argon2]
|
[dependencies.argon2]
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ impl AsyncFileStore {
|
|||||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||||
debug!("adding recipe file {}", file_name);
|
debug!("adding recipe file {}", file_name);
|
||||||
let recipe_contents = read_to_string(entry.path()).await?;
|
let recipe_contents = read_to_string(entry.path()).await?;
|
||||||
entry_vec.push(RecipeEntry(file_name, recipe_contents, None, None));
|
entry_vec.push(RecipeEntry::new(file_name, recipe_contents));
|
||||||
} else {
|
} else {
|
||||||
warn!(
|
warn!(
|
||||||
file = %entry.path().to_string_lossy(),
|
file = %entry.path().to_string_lossy(),
|
||||||
@ -119,12 +119,12 @@ impl AsyncFileStore {
|
|||||||
if recipe_path.exists().await && recipe_path.is_file().await {
|
if recipe_path.exists().await && recipe_path.is_file().await {
|
||||||
debug!("Found recipe file {}", recipe_path.to_string_lossy());
|
debug!("Found recipe file {}", recipe_path.to_string_lossy());
|
||||||
let recipe_contents = read_to_string(recipe_path).await?;
|
let recipe_contents = read_to_string(recipe_path).await?;
|
||||||
return Ok(Some(RecipeEntry(
|
return Ok(Some(RecipeEntry {
|
||||||
id.as_ref().to_owned(),
|
id: id.as_ref().to_owned(),
|
||||||
recipe_contents,
|
text: recipe_contents,
|
||||||
None,
|
category: None,
|
||||||
None,
|
serving_count: None,
|
||||||
)));
|
}));
|
||||||
} else {
|
} else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
@ -440,12 +440,12 @@ impl APIStore for SqliteStore {
|
|||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
.map(|row| {
|
.map(|row| {
|
||||||
RecipeEntry(
|
RecipeEntry {
|
||||||
row.recipe_id.clone(),
|
id: row.recipe_id.clone(),
|
||||||
row.recipe_text.clone().unwrap_or_else(|| String::new()),
|
text: row.recipe_text.clone().unwrap_or_else(|| String::new()),
|
||||||
row.category.clone(),
|
category: row.category.clone(),
|
||||||
row.serving_count.clone(),
|
serving_count: row.serving_count.clone(),
|
||||||
)
|
}
|
||||||
})
|
})
|
||||||
.nth(0);
|
.nth(0);
|
||||||
Ok(entry)
|
Ok(entry)
|
||||||
@ -460,12 +460,12 @@ impl APIStore for SqliteStore {
|
|||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
.map(|row| {
|
.map(|row| {
|
||||||
RecipeEntry(
|
RecipeEntry {
|
||||||
row.recipe_id.clone(),
|
id: row.recipe_id.clone(),
|
||||||
row.recipe_text.clone().unwrap_or_else(|| String::new()),
|
text: row.recipe_text.clone().unwrap_or_else(|| String::new()),
|
||||||
row.category.clone(),
|
category: row.category.clone(),
|
||||||
row.serving_count.clone(),
|
serving_count: row.serving_count.clone(),
|
||||||
)
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Ok(Some(rows))
|
Ok(Some(rows))
|
||||||
|
@ -8,8 +8,14 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
abortable_parser = "~0.2.6"
|
abortable_parser = "~0.2.6"
|
||||||
chrono = "~0.4"
|
|
||||||
serde = "1.0.144"
|
[dependencies.chrono]
|
||||||
|
version = "0.4.22"
|
||||||
|
features = ["serde"]
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
version = "1.0.204"
|
||||||
|
features = ["derive"]
|
||||||
|
|
||||||
[dependencies.num-rational]
|
[dependencies.num-rational]
|
||||||
version = "~0.4.0"
|
version = "~0.4.0"
|
||||||
|
@ -50,39 +50,49 @@ impl Mealplan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct RecipeEntry(pub String, pub String, pub Option<String>, pub Option<i64>);
|
pub struct RecipeEntry {
|
||||||
|
pub id: String,
|
||||||
|
pub text: String,
|
||||||
|
pub category: Option<String>,
|
||||||
|
pub serving_count: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
impl RecipeEntry {
|
impl RecipeEntry {
|
||||||
pub fn new<IS: Into<String>, TS: Into<String>>(recipe_id: IS, text: TS) -> Self {
|
pub fn new<IS: Into<String>, TS: Into<String>>(recipe_id: IS, text: TS) -> Self {
|
||||||
Self(recipe_id.into(), text.into(), None, None)
|
Self {
|
||||||
|
id: recipe_id.into(),
|
||||||
|
text: text.into(),
|
||||||
|
category: None,
|
||||||
|
serving_count: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_recipe_id<S: Into<String>>(&mut self, id: S) {
|
pub fn set_recipe_id<S: Into<String>>(&mut self, id: S) {
|
||||||
self.0 = id.into();
|
self.id = id.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn recipe_id(&self) -> &str {
|
pub fn recipe_id(&self) -> &str {
|
||||||
self.0.as_str()
|
self.id.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_recipe_text<S: Into<String>>(&mut self, text: S) {
|
pub fn set_recipe_text<S: Into<String>>(&mut self, text: S) {
|
||||||
self.1 = text.into();
|
self.text = text.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn recipe_text(&self) -> &str {
|
pub fn recipe_text(&self) -> &str {
|
||||||
self.1.as_str()
|
self.text.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_category<S: Into<String>>(&mut self, cat: S) {
|
pub fn set_category<S: Into<String>>(&mut self, cat: S) {
|
||||||
self.2 = Some(cat.into());
|
self.category = Some(cat.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn category(&self) -> Option<&String> {
|
pub fn category(&self) -> Option<&String> {
|
||||||
self.2.as_ref()
|
self.category.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serving_count(&self) -> Option<i64> {
|
pub fn serving_count(&self) -> Option<i64> {
|
||||||
self.3.clone()
|
self.serving_count.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,7 +220,7 @@ pub struct Step {
|
|||||||
impl Step {
|
impl Step {
|
||||||
pub fn new<S: Into<String>>(prep_time: Option<std::time::Duration>, instructions: S) -> Self {
|
pub fn new<S: Into<String>>(prep_time: Option<std::time::Duration>, instructions: S) -> Self {
|
||||||
Self {
|
Self {
|
||||||
prep_time: prep_time,
|
prep_time,
|
||||||
instructions: instructions.into(),
|
instructions: instructions.into(),
|
||||||
ingredients: Vec::new(),
|
ingredients: Vec::new(),
|
||||||
}
|
}
|
||||||
|
@ -27,9 +27,12 @@ sycamore-router = "0.8"
|
|||||||
js-sys = "0.3.60"
|
js-sys = "0.3.60"
|
||||||
wasm-web-component = { git = "https://github.com/zaphar/wasm-web-components.git", rev = "v0.3.0" }
|
wasm-web-component = { git = "https://github.com/zaphar/wasm-web-components.git", rev = "v0.3.0" }
|
||||||
maud = "*"
|
maud = "*"
|
||||||
|
indexed-db = "0.4.1"
|
||||||
|
anyhow = "1.0.86"
|
||||||
|
serde-wasm-bindgen = "0.6.5"
|
||||||
|
|
||||||
[dependencies.serde]
|
[dependencies.serde]
|
||||||
version = "<=1.0.171"
|
version = "1.0.204"
|
||||||
features = ["derive"]
|
features = ["derive"]
|
||||||
|
|
||||||
[dependencies.tracing-subscriber]
|
[dependencies.tracing-subscriber]
|
||||||
|
324
web/src/api.rs
324
web/src/api.rs
@ -16,18 +16,27 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||||||
use base64::{self, Engine};
|
use base64::{self, Engine};
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use gloo_net;
|
use gloo_net;
|
||||||
use serde_json::{from_str, to_string};
|
// TODO(jwall): Remove this when we have gone a few migrations past.
|
||||||
|
use serde_json::from_str;
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
use tracing::{debug, error, instrument};
|
use tracing::{debug, error, instrument};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
use client_api::*;
|
use client_api::*;
|
||||||
use recipes::{IngredientKey, RecipeEntry};
|
use recipes::{IngredientKey, RecipeEntry};
|
||||||
|
use serde_wasm_bindgen::{from_value, Serializer};
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
|
// TODO(jwall): Remove this when we have gone a few migrations past.
|
||||||
use web_sys::Storage;
|
use web_sys::Storage;
|
||||||
|
|
||||||
|
fn to_js<T: serde::ser::Serialize>(value: T) -> Result<JsValue, serde_wasm_bindgen::Error> {
|
||||||
|
let s = Serializer::new().serialize_maps_as_objects(true);
|
||||||
|
value.serialize(&s)
|
||||||
|
}
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_state::{parse_recipes, AppState},
|
app_state::{parse_recipes, AppState},
|
||||||
js_lib,
|
js_lib::{self, DBFactory},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@ -76,48 +85,110 @@ impl From<gloo_net::Error> for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn recipe_key<S: std::fmt::Display>(id: S) -> String {
|
|
||||||
format!("recipe:{}", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn token68(user: String, pass: String) -> String {
|
fn token68(user: String, pass: String) -> String {
|
||||||
base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", user, pass))
|
base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", user, pass))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn convert_to_io_error<V, E>(res: Result<V, E>) -> Result<V, std::io::Error>
|
||||||
|
where
|
||||||
|
E: Into<Box<dyn std::error::Error>> + std::fmt::Debug,
|
||||||
|
{
|
||||||
|
match res {
|
||||||
|
Ok(v) => Ok(v),
|
||||||
|
Err(e) => Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Other,
|
||||||
|
format!("{:?}", e),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct LocalStore {
|
pub struct LocalStore {
|
||||||
store: Storage,
|
// TODO(zaphar): Remove this when it's safe to delete the migration
|
||||||
|
old_store: Storage,
|
||||||
|
store: DBFactory<'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const APP_STATE_KEY: &'static str = "app-state";
|
||||||
|
const USER_DATA_KEY: &'static str = "user_data";
|
||||||
|
|
||||||
impl LocalStore {
|
impl LocalStore {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
store: js_lib::get_storage(),
|
store: DBFactory::default(),
|
||||||
|
old_store: js_lib::get_storage(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn store_app_state(&self, state: &AppState) {
|
pub async fn migrate(&self) {
|
||||||
self.migrate_local_store();
|
// 1. migrate app-state from localstore to indexeddb
|
||||||
let state = match to_string(state) {
|
debug!("Peforming localstorage migration");
|
||||||
|
if let Ok(Some(v)) = self.old_store.get("app_state") {
|
||||||
|
if let Ok(Some(local_state)) = from_str::<Option<AppState>>(&v) {
|
||||||
|
self.store_app_state(&local_state).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = self.old_store.remove_item("app_state");
|
||||||
|
// 2. migrate user-state from localstore to indexeddb
|
||||||
|
if let Ok(Some(v)) = self.old_store.get(USER_DATA_KEY) {
|
||||||
|
if let Ok(local_user_data) = from_str::<Option<UserData>>(&v) {
|
||||||
|
self.set_user_data(local_user_data.as_ref()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = self.old_store.remove_item(USER_DATA_KEY);
|
||||||
|
// 3. Recipes
|
||||||
|
let store_len = self.old_store.length().unwrap();
|
||||||
|
let mut key_list = Vec::new();
|
||||||
|
for i in 0..store_len {
|
||||||
|
let key = self.old_store.key(i).unwrap().unwrap();
|
||||||
|
if key.starts_with("recipe:") {
|
||||||
|
key_list.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k in key_list {
|
||||||
|
if let Ok(Some(recipe)) = self.old_store.get(&k) {
|
||||||
|
if let Ok(recipe) = from_str::<RecipeEntry>(&recipe) {
|
||||||
|
self.set_recipe_entry(&recipe).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = self.old_store.delete(&k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn store_app_state(&self, state: &AppState) {
|
||||||
|
let state = match to_js(state) {
|
||||||
Ok(state) => state,
|
Ok(state) => state,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, ?state, "Error deserializing app_state");
|
error!(?err, ?state, "Error deserializing app_state");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
web_sys::console::log_1(&state);
|
||||||
|
let key = to_js(APP_STATE_KEY).expect("Failed to serialize key");
|
||||||
self.store
|
self.store
|
||||||
.set("app_state", &state)
|
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||||
.expect("Failed to set our app state");
|
let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
|
||||||
|
object_store.put_kv(&key, &state).await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Failed to store app-state");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_app_state(&self) -> Option<AppState> {
|
#[instrument]
|
||||||
|
pub async fn fetch_app_state(&self) -> Option<AppState> {
|
||||||
debug!("Loading state from local store");
|
debug!("Loading state from local store");
|
||||||
self.store.get("app_state").map_or(None, |val| {
|
let recipes = parse_recipes(&self.get_recipes().await).expect("Failed to parse recipes");
|
||||||
val.map(|s| {
|
self.store
|
||||||
debug!("Found an app_state object");
|
.ro_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||||
let mut app_state: AppState =
|
let key = convert_to_io_error(to_js(APP_STATE_KEY))?;
|
||||||
from_str(&s).expect("Failed to deserialize app state");
|
let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
|
||||||
let recipes = parse_recipes(&self.get_recipes()).expect("Failed to parse recipes");
|
let mut app_state: AppState = match object_store.get(&key).await? {
|
||||||
|
Some(s) => convert_to_io_error(from_value(s))?,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(recipes) = recipes {
|
if let Some(recipes) = recipes {
|
||||||
debug!("Populating recipes");
|
debug!("Populating recipes");
|
||||||
for (id, recipe) in recipes {
|
for (id, recipe) in recipes {
|
||||||
@ -125,122 +196,173 @@ impl LocalStore {
|
|||||||
app_state.recipes.insert(id, recipe);
|
app_state.recipes.insert(id, recipe);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app_state
|
Ok(Some(app_state))
|
||||||
})
|
})
|
||||||
})
|
.await
|
||||||
|
.expect("Failed to fetch app-state")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
/// Gets user data from local storage.
|
/// Gets user data from local storage.
|
||||||
pub fn get_user_data(&self) -> Option<UserData> {
|
pub async fn get_user_data(&self) -> Option<UserData> {
|
||||||
self.store
|
self.store
|
||||||
.get("user_data")
|
.ro_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||||
.map_or(None, |val| val.map(|val| from_str(&val).unwrap_or(None)))
|
let key = to_js(USER_DATA_KEY).expect("Failed to serialize key");
|
||||||
.flatten()
|
let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
|
||||||
|
let user_data: UserData = match object_store.get(&key).await? {
|
||||||
|
Some(s) => convert_to_io_error(from_value(s))?,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
Ok(Some(user_data))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Failed to fetch user_data")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
// Set's user data to local storage.
|
// Set's user data to local storage.
|
||||||
pub fn set_user_data(&self, data: Option<&UserData>) {
|
pub async fn set_user_data(&self, data: Option<&UserData>) {
|
||||||
|
let key = to_js(USER_DATA_KEY).expect("Failed to serialize key");
|
||||||
if let Some(data) = data {
|
if let Some(data) = data {
|
||||||
|
let data = data.clone();
|
||||||
self.store
|
self.store
|
||||||
.set(
|
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||||
"user_data",
|
let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
|
||||||
&to_string(data).expect("Failed to desrialize user_data"),
|
object_store
|
||||||
)
|
.put_kv(&key, &convert_to_io_error(to_js(&data))?)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
.expect("Failed to set user_data");
|
.expect("Failed to set user_data");
|
||||||
} else {
|
} else {
|
||||||
self.store
|
self.store
|
||||||
.delete("user_data")
|
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||||
|
let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
|
||||||
|
object_store.delete(&key).await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
.expect("Failed to delete user_data");
|
.expect("Failed to delete user_data");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_storage_keys(&self) -> Vec<String> {
|
#[instrument]
|
||||||
let mut keys = Vec::new();
|
async fn get_recipe_keys(&self) -> impl Iterator<Item = String> {
|
||||||
for idx in 0..self.store.length().unwrap() {
|
self.store
|
||||||
if let Some(k) = self.store.key(idx).expect("Failed to get storage key") {
|
.ro_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||||
keys.push(k)
|
let mut keys = Vec::new();
|
||||||
}
|
let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
|
||||||
}
|
let key_vec = object_store.get_all_keys(None).await?;
|
||||||
keys
|
for k in key_vec {
|
||||||
}
|
if let Ok(v) = from_value(k) {
|
||||||
|
keys.push(v);
|
||||||
fn migrate_local_store(&self) {
|
|
||||||
for k in self.get_storage_keys().into_iter().filter(|k| {
|
|
||||||
k.starts_with("categor") || k == "inventory" || k.starts_with("plan") || k == "staples"
|
|
||||||
}) {
|
|
||||||
// Deleting old local store key
|
|
||||||
debug!("Deleting old local store key {}", k);
|
|
||||||
self.store.delete(&k).expect("Failed to delete storage key");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_recipe_keys(&self) -> impl Iterator<Item = String> {
|
|
||||||
self.get_storage_keys()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|k| k.starts_with("recipe:"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets all the recipes from local storage.
|
|
||||||
pub fn get_recipes(&self) -> Option<Vec<RecipeEntry>> {
|
|
||||||
let mut recipe_list = Vec::new();
|
|
||||||
for recipe_key in self.get_recipe_keys() {
|
|
||||||
if let Some(entry) = self
|
|
||||||
.store
|
|
||||||
.get(&recipe_key)
|
|
||||||
.expect(&format!("Failed to get recipe: {}", recipe_key))
|
|
||||||
{
|
|
||||||
match from_str(&entry) {
|
|
||||||
Ok(entry) => {
|
|
||||||
recipe_list.push(entry);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!(recipe_key, err = ?e, "Failed to parse recipe entry");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
Ok(keys)
|
||||||
}
|
})
|
||||||
if recipe_list.is_empty() {
|
.await
|
||||||
return None;
|
.expect("Failed to get storage keys")
|
||||||
}
|
.into_iter()
|
||||||
Some(recipe_list)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_recipe_entry(&self, id: &str) -> Option<RecipeEntry> {
|
#[instrument]
|
||||||
let key = recipe_key(id);
|
/// Gets all the recipes from local storage.
|
||||||
|
pub async fn get_recipes(&self) -> Option<Vec<RecipeEntry>> {
|
||||||
self.store
|
self.store
|
||||||
.get(&key)
|
.ro_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||||
.expect(&format!("Failed to get recipe {}", key))
|
let mut recipe_list = Vec::new();
|
||||||
.map(|entry| from_str(&entry).expect(&format!("Failed to get recipe {}", key)))
|
let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
|
||||||
|
let mut c = object_store.cursor().open().await?;
|
||||||
|
while let Some(value) = c.value() {
|
||||||
|
recipe_list.push(convert_to_io_error(from_value(value))?);
|
||||||
|
c.advance(1).await?;
|
||||||
|
}
|
||||||
|
if recipe_list.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Ok(Some(recipe_list))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Failed to get recipes")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
pub async fn get_recipe_entry(&self, id: &str) -> Option<RecipeEntry> {
|
||||||
|
let key = to_js(id).expect("Failed to serialize key");
|
||||||
|
self.store
|
||||||
|
.ro_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||||
|
let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
|
||||||
|
let entry: Option<RecipeEntry> = match object_store.get(&key).await? {
|
||||||
|
Some(v) => convert_to_io_error(from_value(v))?,
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
Ok(entry)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Failed to get recipes")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
/// Sets the set of recipes to the entries passed in. Deletes any recipes not
|
/// Sets the set of recipes to the entries passed in. Deletes any recipes not
|
||||||
/// in the list.
|
/// in the list.
|
||||||
pub fn set_all_recipes(&self, entries: &Vec<RecipeEntry>) {
|
pub async fn set_all_recipes(&self, entries: &Vec<RecipeEntry>) {
|
||||||
for recipe_key in self.get_recipe_keys() {
|
for recipe_key in self.get_recipe_keys().await {
|
||||||
|
let key = to_js(&recipe_key).expect("Failed to serialize key");
|
||||||
self.store
|
self.store
|
||||||
.delete(&recipe_key)
|
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||||
.expect(&format!("Failed to get recipe {}", recipe_key));
|
let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
|
||||||
|
object_store.delete(&key).await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Failed to delete user_data");
|
||||||
}
|
}
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
self.set_recipe_entry(entry);
|
let entry = entry.clone();
|
||||||
|
let key = to_js(entry.recipe_id()).expect("Failed to serialize recipe key");
|
||||||
|
self.store
|
||||||
|
.rw_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||||
|
let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
|
||||||
|
object_store
|
||||||
|
.put_kv(&key, &convert_to_io_error(to_js(&entry))?)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Failed to store recipe entry");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
/// Set recipe entry in local storage.
|
/// Set recipe entry in local storage.
|
||||||
pub fn set_recipe_entry(&self, entry: &RecipeEntry) {
|
pub async fn set_recipe_entry(&self, entry: &RecipeEntry) {
|
||||||
|
let entry = entry.clone();
|
||||||
|
let key = to_js(entry.recipe_id()).expect("Failed to serialize recipe key");
|
||||||
self.store
|
self.store
|
||||||
.set(
|
.rw_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||||
&recipe_key(entry.recipe_id()),
|
let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
|
||||||
&to_string(&entry).expect(&format!("Failed to get recipe {}", entry.recipe_id())),
|
object_store
|
||||||
)
|
.put_kv(&key, &convert_to_io_error(to_js(&entry))?)
|
||||||
.expect(&format!("Failed to store recipe {}", entry.recipe_id()))
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Failed to store recipe entry");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
/// Delete recipe entry from local storage.
|
/// Delete recipe entry from local storage.
|
||||||
pub fn delete_recipe_entry(&self, recipe_id: &str) {
|
pub async fn delete_recipe_entry(&self, recipe_id: &str) {
|
||||||
|
let key = to_js(recipe_id).expect("Failed to serialize key");
|
||||||
self.store
|
self.store
|
||||||
.delete(&recipe_key(recipe_id))
|
.rw_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||||
.expect(&format!("Failed to delete recipe {}", recipe_id))
|
let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
|
||||||
|
object_store.delete(&key).await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Failed to delete user_data");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,7 +487,7 @@ impl HttpStore {
|
|||||||
Ok(resp) => resp,
|
Ok(resp) => resp,
|
||||||
Err(gloo_net::Error::JsError(err)) => {
|
Err(gloo_net::Error::JsError(err)) => {
|
||||||
error!(path, ?err, "Error hitting api");
|
error!(path, ?err, "Error hitting api");
|
||||||
return Ok(self.local_store.get_recipes());
|
return Ok(self.local_store.get_recipes().await);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(err)?;
|
return Err(err)?;
|
||||||
@ -395,7 +517,7 @@ impl HttpStore {
|
|||||||
Ok(resp) => resp,
|
Ok(resp) => resp,
|
||||||
Err(gloo_net::Error::JsError(err)) => {
|
Err(gloo_net::Error::JsError(err)) => {
|
||||||
error!(path, ?err, "Error hitting api");
|
error!(path, ?err, "Error hitting api");
|
||||||
return Ok(self.local_store.get_recipe_entry(id.as_ref()));
|
return Ok(self.local_store.get_recipe_entry(id.as_ref()).await);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(err)?;
|
return Err(err)?;
|
||||||
@ -415,7 +537,7 @@ impl HttpStore {
|
|||||||
.as_success()
|
.as_success()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
if let Some(ref entry) = entry {
|
if let Some(ref entry) = entry {
|
||||||
self.local_store.set_recipe_entry(entry);
|
self.local_store.set_recipe_entry(entry).await;
|
||||||
}
|
}
|
||||||
Ok(entry)
|
Ok(entry)
|
||||||
}
|
}
|
||||||
|
@ -37,12 +37,14 @@ fn bool_true() -> bool {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub recipe_counts: BTreeMap<String, usize>,
|
pub recipe_counts: BTreeMap<String, u32>,
|
||||||
pub recipe_categories: BTreeMap<String, String>,
|
pub recipe_categories: BTreeMap<String, String>,
|
||||||
pub extras: Vec<(String, String)>,
|
pub extras: Vec<(String, String)>,
|
||||||
#[serde(skip)] // FIXME(jwall): This should really be storable I think?
|
// FIXME(jwall): This should really be storable I think?
|
||||||
|
#[serde(skip_deserializing,skip_serializing)]
|
||||||
pub staples: Option<BTreeSet<Ingredient>>,
|
pub staples: Option<BTreeSet<Ingredient>>,
|
||||||
#[serde(skip)] // FIXME(jwall): This should really be storable I think?
|
// FIXME(jwall): This should really be storable I think?
|
||||||
|
#[serde(skip_deserializing,skip_serializing)]
|
||||||
pub recipes: BTreeMap<String, Recipe>,
|
pub recipes: BTreeMap<String, Recipe>,
|
||||||
pub category_map: BTreeMap<String, String>,
|
pub category_map: BTreeMap<String, String>,
|
||||||
pub filtered_ingredients: BTreeSet<IngredientKey>,
|
pub filtered_ingredients: BTreeSet<IngredientKey>,
|
||||||
@ -75,7 +77,7 @@ impl AppState {
|
|||||||
|
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
ResetRecipeCounts,
|
ResetRecipeCounts,
|
||||||
UpdateRecipeCount(String, usize),
|
UpdateRecipeCount(String, u32),
|
||||||
AddExtra(String, String),
|
AddExtra(String, String),
|
||||||
RemoveExtra(usize),
|
RemoveExtra(usize),
|
||||||
UpdateExtra(usize, String, String),
|
UpdateExtra(usize, String, String),
|
||||||
@ -169,6 +171,7 @@ impl StateMachine {
|
|||||||
Self { store, local_store }
|
Self { store, local_store }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all)]
|
||||||
async fn load_state(
|
async fn load_state(
|
||||||
store: &HttpStore,
|
store: &HttpStore,
|
||||||
local_store: &LocalStore,
|
local_store: &LocalStore,
|
||||||
@ -178,7 +181,7 @@ impl StateMachine {
|
|||||||
// call set on the signal once. When the LinearSignal get's dropped it
|
// call set on the signal once. When the LinearSignal get's dropped it
|
||||||
// will call set on the contained Signal.
|
// will call set on the contained Signal.
|
||||||
let mut original: LinearSignal<AppState> = original.into();
|
let mut original: LinearSignal<AppState> = original.into();
|
||||||
if let Some(state) = local_store.fetch_app_state() {
|
if let Some(state) = local_store.fetch_app_state().await {
|
||||||
original = original.update(state);
|
original = original.update(state);
|
||||||
}
|
}
|
||||||
let mut state = original.get().as_ref().clone();
|
let mut state = original.get().as_ref().clone();
|
||||||
@ -201,7 +204,7 @@ impl StateMachine {
|
|||||||
|
|
||||||
info!("Synchronizing recipe");
|
info!("Synchronizing recipe");
|
||||||
if let Some(recipe_entries) = recipe_entries {
|
if let Some(recipe_entries) = recipe_entries {
|
||||||
local_store.set_all_recipes(recipe_entries);
|
local_store.set_all_recipes(recipe_entries).await;
|
||||||
state.recipe_categories = recipe_entries
|
state.recipe_categories = recipe_entries
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| {
|
.map(|entry| {
|
||||||
@ -236,7 +239,7 @@ impl StateMachine {
|
|||||||
// set the counts.
|
// set the counts.
|
||||||
let mut plan_map = BTreeMap::new();
|
let mut plan_map = BTreeMap::new();
|
||||||
for (id, count) in plan {
|
for (id, count) in plan {
|
||||||
plan_map.insert(id, count as usize);
|
plan_map.insert(id, count as u32);
|
||||||
}
|
}
|
||||||
state.recipe_counts = plan_map;
|
state.recipe_counts = plan_map;
|
||||||
for (id, _) in state.recipes.iter() {
|
for (id, _) in state.recipes.iter() {
|
||||||
@ -255,11 +258,11 @@ impl StateMachine {
|
|||||||
info!("Checking for user account data");
|
info!("Checking for user account data");
|
||||||
if let Some(user_data) = store.fetch_user_data().await {
|
if let Some(user_data) = store.fetch_user_data().await {
|
||||||
debug!("Successfully got account data from server");
|
debug!("Successfully got account data from server");
|
||||||
local_store.set_user_data(Some(&user_data));
|
local_store.set_user_data(Some(&user_data)).await;
|
||||||
state.auth = Some(user_data);
|
state.auth = Some(user_data);
|
||||||
} else {
|
} else {
|
||||||
debug!("Using account data from local store");
|
debug!("Using account data from local store");
|
||||||
let user_data = local_store.get_user_data();
|
let user_data = local_store.get_user_data().await;
|
||||||
state.auth = user_data;
|
state.auth = user_data;
|
||||||
}
|
}
|
||||||
info!("Synchronizing categories");
|
info!("Synchronizing categories");
|
||||||
@ -293,7 +296,7 @@ impl StateMachine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Finally we store all of this app state back to our localstore
|
// Finally we store all of this app state back to our localstore
|
||||||
local_store.store_app_state(&state);
|
local_store.store_app_state(&state).await;
|
||||||
original.update(state);
|
original.update(state);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -349,8 +352,9 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
|||||||
.or_insert(cat);
|
.or_insert(cat);
|
||||||
}
|
}
|
||||||
let store = self.store.clone();
|
let store = self.store.clone();
|
||||||
self.local_store.set_recipe_entry(&entry);
|
let local_store = self.local_store.clone();
|
||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
|
local_store.set_recipe_entry(&entry).await;
|
||||||
if let Err(e) = store.store_recipes(vec![entry]).await {
|
if let Err(e) = store.store_recipes(vec![entry]).await {
|
||||||
// FIXME(jwall): We should have a global way to trigger error messages
|
// FIXME(jwall): We should have a global way to trigger error messages
|
||||||
error!(err=?e, "Unable to save Recipe");
|
error!(err=?e, "Unable to save Recipe");
|
||||||
@ -363,9 +367,10 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
|||||||
Message::RemoveRecipe(recipe, callback) => {
|
Message::RemoveRecipe(recipe, callback) => {
|
||||||
original_copy.recipe_counts.remove(&recipe);
|
original_copy.recipe_counts.remove(&recipe);
|
||||||
original_copy.recipes.remove(&recipe);
|
original_copy.recipes.remove(&recipe);
|
||||||
self.local_store.delete_recipe_entry(&recipe);
|
|
||||||
let store = self.store.clone();
|
let store = self.store.clone();
|
||||||
|
let local_store = self.local_store.clone();
|
||||||
spawn_local_scoped(cx, async move {
|
spawn_local_scoped(cx, async move {
|
||||||
|
local_store.delete_recipe_entry(&recipe).await;
|
||||||
if let Err(err) = store.delete_recipe(&recipe).await {
|
if let Err(err) = store.delete_recipe(&recipe).await {
|
||||||
error!(?err, "Failed to delete recipe");
|
error!(?err, "Failed to delete recipe");
|
||||||
}
|
}
|
||||||
@ -396,8 +401,11 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
|||||||
original_copy.modified_amts.insert(key, amt);
|
original_copy.modified_amts.insert(key, amt);
|
||||||
}
|
}
|
||||||
Message::SetUserData(user_data) => {
|
Message::SetUserData(user_data) => {
|
||||||
self.local_store.set_user_data(Some(&user_data));
|
let local_store = self.local_store.clone();
|
||||||
original_copy.auth = Some(user_data);
|
original_copy.auth = Some(user_data.clone());
|
||||||
|
spawn_local_scoped(cx, async move {
|
||||||
|
local_store.set_user_data(Some(&user_data)).await;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Message::SaveState(f) => {
|
Message::SaveState(f) => {
|
||||||
let mut original_copy = original_copy.clone();
|
let mut original_copy = original_copy.clone();
|
||||||
@ -417,7 +425,7 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
|||||||
if let Err(e) = store.store_app_state(&original_copy).await {
|
if let Err(e) = store.store_app_state(&original_copy).await {
|
||||||
error!(err=?e, "Error saving app state");
|
error!(err=?e, "Error saving app state");
|
||||||
};
|
};
|
||||||
local_store.store_app_state(&original_copy);
|
local_store.store_app_state(&original_copy).await;
|
||||||
original.set(original_copy);
|
original.set(original_copy);
|
||||||
f.map(|f| f());
|
f.map(|f| f());
|
||||||
});
|
});
|
||||||
@ -463,7 +471,7 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
|||||||
// Note(jwall): This is a little unusual but because this
|
// Note(jwall): This is a little unusual but because this
|
||||||
// is async code we can't rely on the set below.
|
// is async code we can't rely on the set below.
|
||||||
original_copy.recipe_counts =
|
original_copy.recipe_counts =
|
||||||
BTreeMap::from_iter(plan.drain(0..).map(|(k, v)| (k, v as usize)));
|
BTreeMap::from_iter(plan.drain(0..).map(|(k, v)| (k, v as u32)));
|
||||||
}
|
}
|
||||||
let (filtered, modified, extras) = store
|
let (filtered, modified, extras) = store
|
||||||
.fetch_inventory_for_date(&date)
|
.fetch_inventory_for_date(&date)
|
||||||
@ -478,7 +486,7 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
|||||||
.store_plan_for_date(vec![], &date)
|
.store_plan_for_date(vec![], &date)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to init meal plan for date");
|
.expect("Failed to init meal plan for date");
|
||||||
local_store.store_app_state(&original_copy);
|
local_store.store_app_state(&original_copy).await;
|
||||||
original.set(original_copy);
|
original.set(original_copy);
|
||||||
|
|
||||||
callback.map(|f| f());
|
callback.map(|f| f());
|
||||||
@ -501,7 +509,7 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
|||||||
original_copy.filtered_ingredients = BTreeSet::new();
|
original_copy.filtered_ingredients = BTreeSet::new();
|
||||||
original_copy.modified_amts = BTreeMap::new();
|
original_copy.modified_amts = BTreeMap::new();
|
||||||
original_copy.extras = Vec::new();
|
original_copy.extras = Vec::new();
|
||||||
local_store.store_app_state(&original_copy);
|
local_store.store_app_state(&original_copy).await;
|
||||||
original.set(original_copy);
|
original.set(original_copy);
|
||||||
|
|
||||||
callback.map(|f| f());
|
callback.map(|f| f());
|
||||||
@ -513,8 +521,13 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.local_store.store_app_state(&original_copy);
|
spawn_local_scoped(cx, {
|
||||||
original.set(original_copy);
|
let local_store = self.local_store.clone();
|
||||||
|
async move {
|
||||||
|
local_store.store_app_state(&original_copy).await;
|
||||||
|
original.set(original_copy);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,19 +42,19 @@ pub fn AddRecipe<'ctx, G: Html>(cx: Scope<'ctx>, sh: StateHandler<'ctx>) -> View
|
|||||||
} else {
|
} else {
|
||||||
Some(category)
|
Some(category)
|
||||||
};
|
};
|
||||||
RecipeEntry(
|
RecipeEntry {
|
||||||
recipe_title
|
id: recipe_title
|
||||||
.get()
|
.get()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.to_lowercase()
|
.to_lowercase()
|
||||||
.replace(" ", "_")
|
.replace(" ", "_")
|
||||||
.replace("\n", ""),
|
.replace("\n", ""),
|
||||||
STARTER_RECIPE
|
text: STARTER_RECIPE
|
||||||
.replace("TITLE_PLACEHOLDER", recipe_title.get().as_str())
|
.replace("TITLE_PLACEHOLDER", recipe_title.get().as_str())
|
||||||
.replace("\r", ""),
|
.replace("\r", ""),
|
||||||
category,
|
category,
|
||||||
None,
|
serving_count: None,
|
||||||
)
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
view! {cx,
|
view! {cx,
|
||||||
|
@ -115,12 +115,12 @@ pub fn Editor<'ctx, G: Html>(cx: Scope<'ctx>, props: RecipeComponentProps<'ctx>)
|
|||||||
} else {
|
} else {
|
||||||
Some(category.as_ref().clone())
|
Some(category.as_ref().clone())
|
||||||
};
|
};
|
||||||
let recipe_entry = RecipeEntry(
|
let recipe_entry = RecipeEntry {
|
||||||
id.get_untracked().as_ref().clone(),
|
id: id.get_untracked().as_ref().clone(),
|
||||||
text.get_untracked().as_ref().clone(),
|
text: text.get_untracked().as_ref().clone(),
|
||||||
category,
|
category,
|
||||||
None,
|
serving_count: None,
|
||||||
);
|
};
|
||||||
sh.dispatch(cx, Message::SaveRecipe(recipe_entry, None));
|
sh.dispatch(cx, Message::SaveRecipe(recipe_entry, None));
|
||||||
dirty.set(false);
|
dirty.set(false);
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ pub fn RecipeSelection<'ctx, G: Html>(
|
|||||||
label(for=for_id, class="flex-item-grow") { a(href=href) { (*title) } }
|
label(for=for_id, class="flex-item-grow") { a(href=href) { (*title) } }
|
||||||
NumberField(name=name, class="flex-item-shrink".to_string(), counter=count, min=0.0, on_change=Some(move |_| {
|
NumberField(name=name, class="flex-item-shrink".to_string(), counter=count, min=0.0, on_change=Some(move |_| {
|
||||||
debug!(idx=%id, count=%(*count.get_untracked()), "setting recipe count");
|
debug!(idx=%id, count=%(*count.get_untracked()), "setting recipe count");
|
||||||
sh.dispatch(cx, Message::UpdateRecipeCount(id.as_ref().clone(), *count.get_untracked() as usize));
|
sh.dispatch(cx, Message::UpdateRecipeCount(id.as_ref().clone(), *count.get_untracked() as u32));
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,17 +11,123 @@
|
|||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use indexed_db::{self, Database, Factory, Transaction};
|
||||||
use js_sys::Date;
|
use js_sys::Date;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::future::Future;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
use web_sys::{window, Storage, Window};
|
use web_sys::{window, Window};
|
||||||
|
|
||||||
pub fn get_storage() -> Storage {
|
pub fn get_storage() -> web_sys::Storage {
|
||||||
get_window()
|
get_window()
|
||||||
.local_storage()
|
.local_storage()
|
||||||
.expect("Failed to get storage")
|
.expect("Failed to get storage")
|
||||||
.expect("No storage available")
|
.expect("No storage available")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const STATE_STORE_NAME: &'static str = "state-store";
|
||||||
|
pub const RECIPE_STORE_NAME: &'static str = "recipe-store";
|
||||||
|
pub const SERVING_COUNT_IDX: &'static str = "recipe-serving-count";
|
||||||
|
pub const CATEGORY_IDX: &'static str = "recipe-category";
|
||||||
|
pub const DB_VERSION: u32 = 1;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DBFactory<'name> {
|
||||||
|
name: &'name str,
|
||||||
|
version: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DBFactory<'static> {
|
||||||
|
fn default() -> Self {
|
||||||
|
DBFactory {
|
||||||
|
name: STATE_STORE_NAME,
|
||||||
|
version: Some(DB_VERSION),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn version1_setup<'db>(
|
||||||
|
stores: &HashSet<String>,
|
||||||
|
db: &'db Database<std::io::Error>,
|
||||||
|
) -> Result<(), indexed_db::Error<std::io::Error>> {
|
||||||
|
// We use out of line keys for this object store
|
||||||
|
if !stores.contains(STATE_STORE_NAME) {
|
||||||
|
db.build_object_store(STATE_STORE_NAME).create()?;
|
||||||
|
}
|
||||||
|
if !stores.contains(RECIPE_STORE_NAME) {
|
||||||
|
let recipe_store = db.build_object_store(RECIPE_STORE_NAME).create()?;
|
||||||
|
recipe_store
|
||||||
|
.build_index(CATEGORY_IDX, "category")
|
||||||
|
.create()?;
|
||||||
|
recipe_store
|
||||||
|
.build_index(SERVING_COUNT_IDX, "serving_count")
|
||||||
|
.create()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'name> DBFactory<'name> {
|
||||||
|
pub async fn get_indexed_db(&self) -> Result<Database<std::io::Error>> {
|
||||||
|
let factory = Factory::<std::io::Error>::get().context("opening IndexedDB")?;
|
||||||
|
let db = factory
|
||||||
|
.open(self.name, self.version.unwrap_or(0), |evt| async move {
|
||||||
|
// NOTE(zaphar): This is the on upgradeneeded handler. It get's called on new databases or
|
||||||
|
// databases with an older version than the one we requested to build.
|
||||||
|
let db = evt.database();
|
||||||
|
let stores = db
|
||||||
|
.object_store_names()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<HashSet<String>>();
|
||||||
|
// NOTE(jwall): This needs to be somewhat clever in handling version upgrades.
|
||||||
|
if db.version() > 0 {
|
||||||
|
version1_setup(&stores, db).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context(format!("Opening or creating the database {}", self.name))?;
|
||||||
|
Ok(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn rw_transaction<Fun, RetFut, Ret>(
|
||||||
|
&self,
|
||||||
|
stores: &[&str],
|
||||||
|
transaction: Fun,
|
||||||
|
) -> indexed_db::Result<Ret, std::io::Error>
|
||||||
|
where
|
||||||
|
Fun: 'static + FnOnce(Transaction<std::io::Error>) -> RetFut,
|
||||||
|
RetFut: 'static + Future<Output = indexed_db::Result<Ret, std::io::Error>>,
|
||||||
|
Ret: 'static,
|
||||||
|
{
|
||||||
|
self.get_indexed_db()
|
||||||
|
.await
|
||||||
|
.expect("Failed to open database")
|
||||||
|
.transaction(stores)
|
||||||
|
.rw()
|
||||||
|
.run(transaction)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn ro_transaction<Fun, RetFut, Ret>(
|
||||||
|
&self,
|
||||||
|
stores: &[&str],
|
||||||
|
transaction: Fun,
|
||||||
|
) -> indexed_db::Result<Ret, std::io::Error>
|
||||||
|
where
|
||||||
|
Fun: 'static + FnOnce(Transaction<std::io::Error>) -> RetFut,
|
||||||
|
RetFut: 'static + Future<Output = indexed_db::Result<Ret, std::io::Error>>,
|
||||||
|
Ret: 'static,
|
||||||
|
{
|
||||||
|
self.get_indexed_db()
|
||||||
|
.await
|
||||||
|
.expect("Failed to open database")
|
||||||
|
.transaction(stores)
|
||||||
|
.run(transaction)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_ms_timestamp() -> u32 {
|
pub fn get_ms_timestamp() -> u32 {
|
||||||
Date::new_0().get_milliseconds()
|
Date::new_0().get_milliseconds()
|
||||||
}
|
}
|
||||||
|
@ -20,20 +20,22 @@ use crate::{api, routing::Handler as RouteHandler};
|
|||||||
#[instrument]
|
#[instrument]
|
||||||
#[component]
|
#[component]
|
||||||
pub fn UI<G: Html>(cx: Scope) -> View<G> {
|
pub fn UI<G: Html>(cx: Scope) -> View<G> {
|
||||||
|
let view = create_signal(cx, View::empty());
|
||||||
api::HttpStore::provide_context(cx, "/api".to_owned());
|
api::HttpStore::provide_context(cx, "/api".to_owned());
|
||||||
let store = api::HttpStore::get_from_context(cx).as_ref().clone();
|
let store = api::HttpStore::get_from_context(cx).as_ref().clone();
|
||||||
info!("Starting UI");
|
info!("Starting UI");
|
||||||
let local_store = api::LocalStore::new();
|
|
||||||
let app_state = if let Some(app_state) = local_store.fetch_app_state() {
|
|
||||||
app_state
|
|
||||||
} else {
|
|
||||||
crate::app_state::AppState::new()
|
|
||||||
};
|
|
||||||
debug!(?app_state, "Loaded app state from local storage");
|
|
||||||
let sh = crate::app_state::get_state_handler(cx, app_state, store);
|
|
||||||
let view = create_signal(cx, View::empty());
|
|
||||||
spawn_local_scoped(cx, {
|
spawn_local_scoped(cx, {
|
||||||
async move {
|
async move {
|
||||||
|
let local_store = api::LocalStore::new();
|
||||||
|
// TODO(jwall): At some point we can drop this potentially?
|
||||||
|
local_store.migrate().await;
|
||||||
|
let app_state = if let Some(app_state) = local_store.fetch_app_state().await {
|
||||||
|
app_state
|
||||||
|
} else {
|
||||||
|
crate::app_state::AppState::new()
|
||||||
|
};
|
||||||
|
debug!(?app_state, "Loaded app state from local storage");
|
||||||
|
let sh = crate::app_state::get_state_handler(cx, app_state, store);
|
||||||
sh.dispatch(cx, Message::LoadState(None));
|
sh.dispatch(cx, Message::LoadState(None));
|
||||||
view.set(view! { cx,
|
view.set(view! { cx,
|
||||||
RouteHandler(sh=sh)
|
RouteHandler(sh=sh)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user