made server address configurable

This commit is contained in:
Ide
2025-12-28 13:59:00 +01:00
parent f413f27bf0
commit a97cd03eb3
7 changed files with 175 additions and 37 deletions
Generated
+22 -6
View File
@@ -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"
+4
View File
@@ -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"
+53
View File
@@ -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<Config, anyhow::Error> {
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)
}
+51 -16
View File
@@ -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<dyn Error>;
type MagicAnyError = anyhow::Error;
/// GET "/api/list" from the server
async fn get_list() -> Result<Vec<Item>, MagicAnyError> {
async fn get_list(conf: &Mutex<Config>) -> Result<Vec<Item>, MagicAnyError> {
// TODO: don't hardcode url
let list = reqwest::get("http://localhost:8000/api/list")
.await?
.json::<Vec<Item>>()
.await?;
let base = Url::parse(&conf.lock().unwrap().address)?;
let url = base.join("/api/list")?;
let list = reqwest::get(url).await?.json::<Vec<Item>>().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<Config>) -> 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<Config>) -> 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::<State>();
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()?;
+8 -15
View File
@@ -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 <SItem> 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 {
+17
View File
@@ -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;
}
}
+20
View File
@@ -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 <string> server-address;
callback set-server-address(string);
set-server-address(address) => {
server-address = address;
}
}