// 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")] mod config; mod runtime; #[cfg(target_os = "android")] mod android; #[cfg(target_arch = "wasm32")] mod wasm; use std::{ rc::Rc, sync::{Arc, Mutex}, }; use inkopslista_lib::{Item, ItemData}; use reqwest::Url; use slint::{Model, ModelRc, ToSharedString, VecModel}; use crate::{config::Config, runtime::spawn}; slint::include_modules!(); /// GET "/api/list" from the server async fn get_list(conf: &Mutex) -> Result, anyhow::Error> { let base = Url::parse(&conf.lock().unwrap().address)?; let url = base.join("/api/list")?; let list = reqwest::get(url).await?.json::>().await?; Ok(list) } /// Add or update an item in the list async fn put_list(item: &str, data: &ItemData, conf: &Mutex) -> Result<(), anyhow::Error> { let client = reqwest::Client::new(); let base = Url::parse(&conf.lock().unwrap().address)?.join("/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, conf: &Mutex) -> Result<(), anyhow::Error> { let client = reqwest::Client::new(); let base = Url::parse(&conf.lock().unwrap().address)?.join("/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 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, }) .for_each(|item| list.push(item)); } fn refresh_list(conf: &Arc>, app_weak: &slint::Weak) { let conf = conf.clone(); let app_weak = app_weak.clone(); spawn(async move { let new_list = match get_list(&conf).await { Ok(new_list) => new_list, Err(e) => { eprintln!("{e:?}"); return; } }; let _ = app_weak.upgrade_in_event_loop(|app| { let list: ModelRc = app.global::().get_list(); let list: &VecModel = list.as_any().downcast_ref().expect("list is a VecModel"); update_slint_list(new_list, &list); }); }); } pub fn run() -> Result<(), anyhow::Error> { let app = App::new()?; let state = app.global::(); let list_shared = Rc::new(VecModel::default()); state.set_list(ModelRc::new(list_shared.clone())); #[cfg(target_os = "linux")] let conf = runtime::RT .block_on(config::get_config()) .inspect_err(|err| eprintln!("{err:?}")) .unwrap_or_default(); #[cfg(target_arch = "wasm32")] let conf = Config::default(); #[cfg(target_os = "android")] let conf = Config::default(); state.set_server_address(conf.address.to_shared_string()); let conf_shared = Arc::new(Mutex::new(conf)); let conf = conf_shared.clone(); let app_weak = app.as_weak(); // load shopping list from server on start refresh_list(&conf, &app_weak); state.on_refresh_list(move || { refresh_list(&conf, &app_weak); }); let conf = conf_shared.clone(); let list = list_shared.clone(); state.on_add_to_list(move |item| { if list.iter().any(|item2| item2.name == item) { return; // TODO: implement increment when number is added } list.push(SItem { checked: false, name: item.clone(), }); let conf = conf.clone(); spawn(async move { // TODO: add number to increment let res = put_list( &item, &ItemData { checked: false, amount: 1, }, &conf, ) .await; if let Err(err) = res { eprintln!("{err:?}"); } }); }); let conf = conf_shared.clone(); let list = list_shared.clone(); state.on_check_item(move |item| { let positem = list .iter() .enumerate() .find(|(_, item2)| item2.name == item); let (pos, mut item) = match positem { Some(positem) => positem, None => return, }; item.checked = !item.checked; list.set_row_data(pos, item.clone()); let conf = conf.clone(); spawn(async move { let res = put_list( &item.name, &ItemData { checked: item.checked, amount: 1, }, &conf, ) .await; if let Err(err) = res { eprintln!("{err:?}"); } }); }); let conf = conf_shared.clone(); let list = list_shared.clone(); state.on_delete_checked(move || { let poss: Vec<_> = list .iter() .enumerate() .filter(|(_, item)| item.checked) .map(|(pos, item)| (pos, item.name)) .collect(); for &(pos, _) in poss.iter().rev() { list.remove(pos); } let conf = conf.clone(); spawn(async move { for (_, name) in poss { let res = delete_item(&name, &conf).await; if let Err(err) = res { eprintln!("{err:?}"); } } }); }); state.on_set_server_address(move |address| { conf_shared.lock().unwrap().address = address.to_string(); #[cfg(target_os = "linux")] let _ = config::save_config(address.to_string()); }); app.run()?; Ok(()) }