From f413f27bf03718a861af2556959676aba84e6b6e Mon Sep 17 00:00:00 2001 From: Ida Dahl Date: Sat, 27 Dec 2025 22:58:07 +0100 Subject: [PATCH] sync list updates with server --- Cargo.lock | 22 +++---- inkopslista-lib/src/lib.rs | 1 + inköpslista-gui/Cargo.toml | 2 +- inköpslista-gui/src/main.rs | 107 ++++++++++++++++++++++++++++++++--- inköpslista-gui/ui/app.slint | 43 +++++++++----- inköpslista-srv/src/main.rs | 4 +- 6 files changed, 144 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a245d88..329f280 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2360,6 +2360,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inkopslista" +version = "0.0.0" +dependencies = [ + "inkopslista-lib", + "reqwest", + "slint", + "slint-build", + "tokio", +] + [[package]] name = "inkopslista-lib" version = "0.1.0" @@ -2377,17 +2388,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "inköpslista" -version = "0.0.0" -dependencies = [ - "inkopslista-lib", - "reqwest", - "slint", - "slint-build", - "tokio", -] - [[package]] name = "inlinable_string" version = "0.1.15" diff --git a/inkopslista-lib/src/lib.rs b/inkopslista-lib/src/lib.rs index 8c546f9..d4bfc09 100644 --- a/inkopslista-lib/src/lib.rs +++ b/inkopslista-lib/src/lib.rs @@ -10,4 +10,5 @@ pub struct Item { pub struct ItemData { pub checked: bool, + // TODO: add number } diff --git a/inköpslista-gui/Cargo.toml b/inköpslista-gui/Cargo.toml index 583cf49..6e27181 100644 --- a/inköpslista-gui/Cargo.toml +++ b/inköpslista-gui/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "inköpslista" +name = "inkopslista" version = "0.0.0" edition = "2024" diff --git a/inköpslista-gui/src/main.rs b/inköpslista-gui/src/main.rs index cfa7412..f27ba0e 100644 --- a/inköpslista-gui/src/main.rs +++ b/inköpslista-gui/src/main.rs @@ -1,10 +1,11 @@ // Prevent console window in addition to Slint window in Windows release builds when, e.g., starting the app via file manager. Ignored on other platforms. #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use std::error::Error; +use std::{error::Error, rc::Rc}; -use inkopslista_lib::Item; -use slint::{ModelRc, ToSharedString, VecModel}; +use inkopslista_lib::{Item, ItemData}; +use reqwest::Url; +use slint::{Model, ModelRc, ToSharedString, VecModel}; slint::include_modules!(); @@ -21,29 +22,119 @@ async fn get_list() -> Result, MagicAnyError> { Ok(list) } +/// Add or update an item in the list +async fn put_list(item: &str, data: &ItemData) -> Result<(), MagicAnyError> { + let client = reqwest::Client::new(); + let base = Url::parse("http://localhost:8000/api/list/")?; + let url = base.join(item)?; + let _response = client + .put(url) + .json(data) + .send() + .await? + .error_for_status()?; + Ok(()) +} + +async fn delete_item(item: &str) -> Result<(), MagicAnyError> { + let client = reqwest::Client::new(); + let base = Url::parse("http://localhost:8000/api/list/")?; + let url = base.join(item)?; + let _response = client.delete(url).send().await?.error_for_status()?; + Ok(()) +} + /// Convert shopping list to slint types -fn list_slint_convert(list: Vec) -> ModelRc { - let list = list +fn update_slint_list(new_list: Vec, list: &VecModel) { + list.clear(); + new_list .iter() .map(|item| SItem { name: item.name.to_shared_string(), checked: item.data.checked, }) - .collect::>(); - ModelRc::new(list) + .for_each(|item| list.push(item)); } #[tokio::main] async fn main() -> Result<(), MagicAnyError> { let ui = App::new()?; + let state = ui.global::(); + let list = Rc::new(VecModel::default()); + state.set_list(ModelRc::new(list.clone())); // HACK: load shopping list from server on start match get_list().await { - Ok(list) => ui.set_list(list_slint_convert(list)), + Ok(new_list) => update_slint_list(new_list, &list), Err(e) => { eprintln!("{e:?}"); } } + let list2 = list.clone(); + state.on_add_to_list(move |item| { + if list2.iter().any(|item2| item2.name == item) { + return; + // TODO: implement increment when number is added + } + list2.push(SItem { + checked: false, + name: item.clone(), + }); + tokio::spawn(async move { + // TODO: add number to increment + let res = put_list(&item, &ItemData { checked: false }).await; + if let Err(err) = res { + eprintln!("{err:?}"); + } + }); + }); + + let list2 = list.clone(); + state.on_check_item(move |item| { + let positem = list2 + .iter() + .enumerate() + .find(|(_, item2)| item2.name == item); + let (pos, mut item) = match positem { + Some(positem) => positem, + None => return, + }; + item.checked = !item.checked; + list2.set_row_data(pos, item.clone()); + + tokio::spawn(async move { + let res = put_list( + &item.name, + &ItemData { + checked: item.checked, + }, + ) + .await; + if let Err(err) = res { + eprintln!("{err:?}"); + } + }); + }); + let list2 = list.clone(); + state.on_delete_checked(move || { + let poss: Vec<_> = list2 + .iter() + .enumerate() + .filter(|(_, item)| item.checked) + .map(|(pos, item)| (pos, item.name)) + .collect(); + for &(pos, _) in poss.iter().rev() { + list2.remove(pos); + } + tokio::spawn(async move { + for (_, name) in poss { + let res = delete_item(&name).await; + if let Err(err) = res { + eprintln!("{err:?}"); + } + } + }); + }); ui.run()?; diff --git a/inköpslista-gui/ui/app.slint b/inköpslista-gui/ui/app.slint index 00a3af1..6f4b823 100644 --- a/inköpslista-gui/ui/app.slint +++ b/inköpslista-gui/ui/app.slint @@ -16,6 +16,17 @@ export struct SItem { checked: bool, } +export global State { + in-out property <[SItem]> list: [ + { name: "Test 1", checked: true }, + { name: "Test 2", checked: false }, + { name: "Test 3", checked: false }, + ]; + callback add-to-list(string); + callback check-item(string); + callback delete-checked(); +} + component GoodListItem inherits Rectangle { in-out property item; background: #ffffff10; @@ -24,8 +35,9 @@ component GoodListItem inherits Rectangle { HorizontalBox { check := CheckBox { checked: root.item.checked; - toggled => { - // TODO + toggled() => { + State.check-item(item.name); + debug("CheckBox checked: ", self.checked); } } @@ -38,7 +50,8 @@ component GoodListItem inherits Rectangle { input := TouchArea { clicked => { - root.item.checked = !root.item.checked; + //root.item.checked = !root.item.checked; + State.check-item(item.name); } } } @@ -76,6 +89,9 @@ component GoodButtons inherits ScrollView { width: root.button-width * 1pt; height: root.button-height * 1pt; text: item; + clicked => { + State.add-to-list(item); + } } } @@ -96,9 +112,9 @@ component AddView inherits VerticalBox { Button { width: self.height; text: "+"; - // clicked => { - // goods.items[goods.items.length] = input.text; - // } + clicked => { + State.add-to-list(input.text); + } } } @@ -114,27 +130,28 @@ component ShopView inherits VerticalBox { Button { text: "I am done hehe"; + clicked => { + State.delete-checked(); + } } } export component App inherits Window { - in property <[SItem]> list: [ - { name: "Test 1", checked: true }, - { name: "Test 2", checked: false }, - { name: "Test 3", checked: false }, - ]; + + preferred-height: 700px; + preferred-width: 480px; TabWidget { Tab { title: "Add"; AddView { - list <=> list; + list <=> State.list; } } Tab { title: "Shop"; ShopView { - list <=> list; + list <=> State.list; } } } diff --git a/inköpslista-srv/src/main.rs b/inköpslista-srv/src/main.rs index 197710c..0da8699 100644 --- a/inköpslista-srv/src/main.rs +++ b/inköpslista-srv/src/main.rs @@ -42,7 +42,7 @@ fn put_list(item: &str, data: Json, mock_db: &State>) { } #[delete("/api/list/")] -fn delete_trip(item: &str, mock_db: &State>) { +fn delete_item(item: &str, mock_db: &State>) { let mut mock_db = mock_db.lock().unwrap(); if let Some(index) = mock_db.list.iter().position(|x| x.name == item) { mock_db.list.remove(index); @@ -53,7 +53,7 @@ fn delete_trip(item: &str, mock_db: &State>) { fn rocket() -> _ { rocket::build() .manage(Mutex::new(MockDb::default())) - .mount("/", routes![index, get_list, put_list, delete_trip]) + .mount("/", routes![index, get_list, put_list, delete_item]) } #[derive(Serialize, Deserialize, Default, Debug)]