diff --git a/Cargo.lock b/Cargo.lock index 329f280..796245c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2364,11 +2364,15 @@ dependencies = [ name = "inkopslista" version = "0.0.0" dependencies = [ + "anyhow", "inkopslista-lib", "reqwest", + "serde", + "serde_json", "slint", "slint-build", "tokio", + "xdg", ] [[package]] @@ -3872,7 +3876,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls", - "socket2 0.5.10", + "socket2 0.6.1", "thiserror 2.0.17", "tokio", "tracing", @@ -3909,9 +3913,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.1", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -4531,15 +4535,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -6601,6 +6605,12 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" +[[package]] +name = "xdg" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" + [[package]] name = "xkbcommon" version = "0.9.0" @@ -6846,6 +6856,12 @@ dependencies = [ "syn", ] +[[package]] +name = "zmij" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d6085d62852e35540689d1f97ad663e3971fc19cf5eceab364d62c646ea167" + [[package]] name = "zune-core" version = "0.4.12" diff --git a/inköpslista-gui/Cargo.toml b/inköpslista-gui/Cargo.toml index 6e27181..2d58ad8 100644 --- a/inköpslista-gui/Cargo.toml +++ b/inköpslista-gui/Cargo.toml @@ -7,6 +7,10 @@ edition = "2024" reqwest = { version = "0.12.26", features = ["json", "rustls-tls"], default-features = false } inkopslista-lib = { path = "../inkopslista-lib" } tokio = { version = "1.48.0", features = ["full"] } +serde = { version = "1.0.228", features = ["derive"] } +xdg = "3.0.0" +serde_json = "1.0.148" +anyhow = "1.0.100" [dependencies.slint] version = "1.14.1" diff --git a/inköpslista-gui/src/config.rs b/inköpslista-gui/src/config.rs new file mode 100644 index 0000000..227aaf6 --- /dev/null +++ b/inköpslista-gui/src/config.rs @@ -0,0 +1,53 @@ +use anyhow::{Context, Ok}; +use serde::{Deserialize, Serialize}; +use serde_json; +use xdg::BaseDirectories; + +#[derive(Serialize, Deserialize)] + +pub struct Config { + pub address: String, +} + +impl Default for Config { + fn default() -> Self { + Self { + address: "http://localhost:8000".to_string(), + } + } +} +const CONFIG_FILE_NAME: &'static str = "config.json"; +const XDG_PREFIX: &'static str = "inköpslista"; + +pub fn save_config(address: String) -> Result<(), anyhow::Error> { + let xdg_dirs = xdg::BaseDirectories::with_prefix(XDG_PREFIX); + let conf = Config { address: address }; + let conf = serde_json::to_string(&conf).context("unable to serialize config")?; + + let path = xdg_dirs + .place_config_file(CONFIG_FILE_NAME) + .context("unable to create config directory")?; + + tokio::spawn(async { + let res = tokio::fs::write(path, conf) + .await + .context("unable to write config file"); + if let Err(err) = res { + eprintln!("{err:?}"); + } + }); + Ok(()) +} + +pub async fn get_config() -> Result { + let xdg_dirs = xdg::BaseDirectories::with_prefix(XDG_PREFIX); + let file = xdg_dirs + .get_config_file(CONFIG_FILE_NAME) + .context("no config file")?; + let content = tokio::fs::read_to_string(file) + .await + .context("failed to read config file")?; + + let conf: Config = serde_json::from_str(&content).context("Unable to deserealize contect")?; + Ok(conf) +} diff --git a/inköpslista-gui/src/main.rs b/inköpslista-gui/src/main.rs index f27ba0e..38247c2 100644 --- a/inköpslista-gui/src/main.rs +++ b/inköpslista-gui/src/main.rs @@ -1,31 +1,36 @@ // 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, rc::Rc}; +mod config; +use std::{ + error::Error, + rc::Rc, + sync::{Arc, Mutex}, +}; use inkopslista_lib::{Item, ItemData}; use reqwest::Url; use slint::{Model, ModelRc, ToSharedString, VecModel}; +use crate::config::{Config, get_config, save_config}; + slint::include_modules!(); // TODO: use anyhow -type MagicAnyError = Box; +type MagicAnyError = anyhow::Error; /// GET "/api/list" from the server -async fn get_list() -> Result, MagicAnyError> { +async fn get_list(conf: &Mutex) -> Result, MagicAnyError> { // TODO: don't hardcode url - let list = reqwest::get("http://localhost:8000/api/list") - .await? - .json::>() - .await?; + 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) -> Result<(), MagicAnyError> { +async fn put_list(item: &str, data: &ItemData, conf: &Mutex) -> Result<(), MagicAnyError> { let client = reqwest::Client::new(); - let base = Url::parse("http://localhost:8000/api/list/")?; + let base = Url::parse(&conf.lock().unwrap().address)?.join("/api/list/")?; let url = base.join(item)?; let _response = client .put(url) @@ -36,9 +41,9 @@ async fn put_list(item: &str, data: &ItemData) -> Result<(), MagicAnyError> { Ok(()) } -async fn delete_item(item: &str) -> Result<(), MagicAnyError> { +async fn delete_item(item: &str, conf: &Mutex) -> Result<(), MagicAnyError> { let client = reqwest::Client::new(); - let base = Url::parse("http://localhost:8000/api/list/")?; + 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(()) @@ -62,15 +67,23 @@ async fn main() -> Result<(), MagicAnyError> { let state = ui.global::(); let list = Rc::new(VecModel::default()); state.set_list(ModelRc::new(list.clone())); - + let conf = get_config() + .await + .inspect_err(|err| eprintln!("{err:?}")) + .unwrap_or_default(); + state.set_server_address(conf.address.to_shared_string()); + let conf = Arc::new(Mutex::new(conf)); // HACK: load shopping list from server on start - match get_list().await { + + let conf2 = conf.clone(); + match get_list(&conf2).await { Ok(new_list) => update_slint_list(new_list, &list), Err(e) => { eprintln!("{e:?}"); } } let list2 = list.clone(); + let conf2 = conf.clone(); state.on_add_to_list(move |item| { if list2.iter().any(|item2| item2.name == item) { return; @@ -80,15 +93,17 @@ async fn main() -> Result<(), MagicAnyError> { checked: false, name: item.clone(), }); + let conf2 = conf2.clone(); tokio::spawn(async move { // TODO: add number to increment - let res = put_list(&item, &ItemData { checked: false }).await; + let res = put_list(&item, &ItemData { checked: false }, &conf2).await; if let Err(err) = res { eprintln!("{err:?}"); } }); }); + let conf2 = conf.clone(); let list2 = list.clone(); state.on_check_item(move |item| { let positem = list2 @@ -101,6 +116,7 @@ async fn main() -> Result<(), MagicAnyError> { }; item.checked = !item.checked; list2.set_row_data(pos, item.clone()); + let conf2 = conf2.clone(); tokio::spawn(async move { let res = put_list( @@ -108,6 +124,7 @@ async fn main() -> Result<(), MagicAnyError> { &ItemData { checked: item.checked, }, + &conf2, ) .await; if let Err(err) = res { @@ -115,6 +132,8 @@ async fn main() -> Result<(), MagicAnyError> { } }); }); + + let conf2 = conf.clone(); let list2 = list.clone(); state.on_delete_checked(move || { let poss: Vec<_> = list2 @@ -126,15 +145,31 @@ async fn main() -> Result<(), MagicAnyError> { for &(pos, _) in poss.iter().rev() { list2.remove(pos); } + + let conf2 = conf2.clone(); + tokio::spawn(async move { for (_, name) in poss { - let res = delete_item(&name).await; + let res = delete_item(&name, &conf2).await; if let Err(err) = res { eprintln!("{err:?}"); } } }); }); + state.on_set_server_address(move |address| { + let _ = save_config(address.to_string()); + conf.lock().unwrap().address = address.to_string(); + + // create type for config file ✅ + // derives serde ✅ + // add library that gives us folder to save file in xdg typ ✅ + // write funciton that uses library to save file to folder place_config_file() ✅ + // call function on callback through tokio + // write function to load file get_config_file(path) finns i xdg ✅ + // call function on start ✅ + // use config on get put delete server stuffs. (instead of the static string) ✅ + }); ui.run()?; diff --git a/inköpslista-gui/ui/app.slint b/inköpslista-gui/ui/app.slint index 6f4b823..c939cdb 100644 --- a/inköpslista-gui/ui/app.slint +++ b/inköpslista-gui/ui/app.slint @@ -10,22 +10,10 @@ import { TextEdit, GridBox, } from "std-widgets.slint"; +import { SettingsView } from "settings.slint"; +import { State, SItem } from "state.slint"; +export { State, SItem } -export struct SItem { - name: string, - 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; @@ -141,6 +129,11 @@ export component App inherits Window { preferred-height: 700px; preferred-width: 480px; TabWidget { + Tab { + title: "Settings"; + SettingsView { } + } + Tab { title: "Add"; AddView { diff --git a/inköpslista-gui/ui/settings.slint b/inköpslista-gui/ui/settings.slint new file mode 100644 index 0000000..a10686e --- /dev/null +++ b/inköpslista-gui/ui/settings.slint @@ -0,0 +1,17 @@ +import { VerticalBox, LineEdit } from "std-widgets.slint"; +import { State } from "state.slint"; + +export component SettingsView inherits VerticalBox { + alignment: start; + Text { + text: "server address:"; + font-size: 25px; + } + + LineEdit { + edited(text) => { + State.set-server-address(text); + } + text: State.server-address; + } +} diff --git a/inköpslista-gui/ui/state.slint b/inköpslista-gui/ui/state.slint new file mode 100644 index 0000000..2cc1440 --- /dev/null +++ b/inköpslista-gui/ui/state.slint @@ -0,0 +1,20 @@ +export struct SItem { + name: string, + 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(); + in-out property server-address; + callback set-server-address(string); + set-server-address(address) => { + server-address = address; + } +}