From b3dd37340372448b042370136227657c1e4e3db3 Mon Sep 17 00:00:00 2001 From: Ida Dahl Date: Fri, 12 Jun 2026 19:24:39 +0200 Subject: [PATCH] idk what I did last --- gui/src/lib.rs | 37 +++++++++++++++++++++++++++++-- gui/ui/app.slint | 40 ++++++++++++++++++++++++++++----- gui/ui/state.slint | 9 ++++++++ lib/src/lib.rs | 8 +++++-- srv/src/db/init.sql | 12 +++++++++- srv/src/main.rs | 54 +++++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 147 insertions(+), 13 deletions(-) diff --git a/gui/src/lib.rs b/gui/src/lib.rs index e8c0d87..e420bc9 100644 --- a/gui/src/lib.rs +++ b/gui/src/lib.rs @@ -15,9 +15,9 @@ use std::{ sync::{Arc, Mutex}, }; -use inkopslista_lib::{Item, ItemData}; +use inkopslista_lib::{ButtonItem, Item, ItemData}; use reqwest::Url; -use slint::{Model, ModelRc, ToSharedString, VecModel}; +use slint::{Model, ModelRc, SharedString, ToSharedString, VecModel}; use crate::{config::Config, runtime::spawn}; @@ -31,6 +31,13 @@ async fn get_list(conf: &Mutex) -> Result, anyhow::Error> { Ok(list) } +async fn get_favorites(conf: &Mutex) -> Result, anyhow::Error> { + let base = Url::parse(&conf.lock().unwrap().address)?; + let url = base.join("/api/favorites")?; + let favs = reqwest::get(url).await?.json::>().await?; + Ok(favs) +} + /// 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(); @@ -66,6 +73,17 @@ fn update_slint_list(new_list: Vec, list: &VecModel) { .for_each(|item| list.push(item)); } +fn update_slint_favs(new_favs: Vec, favs: &VecModel) { + favs.clear(); + new_favs + .into_iter() + .map(|item| SButtonItem { + name: item.name.into(), + category: item.category.unwrap_or_default().to_shared_string(), + }) + .for_each(|item| favs.push(item)); +} + fn refresh_list(conf: &Arc>, app_weak: &slint::Weak) { let conf = conf.clone(); let app_weak = app_weak.clone(); @@ -82,6 +100,19 @@ fn refresh_list(conf: &Arc>, app_weak: &slint::Weak) { let list: &VecModel = list.as_any().downcast_ref().expect("list is a VecModel"); update_slint_list(new_list, &list); }); + let new_favs = match get_favorites(&conf).await { + Ok(new_favs) => new_favs, + Err(e) => { + eprintln!("{e:?}"); + return; + } + }; + let _ = app_weak.upgrade_in_event_loop(|app| { + let favs: ModelRc = app.global::().get_favorites(); + let favs: &VecModel = + favs.as_any().downcast_ref().expect("favs is a vec model"); + update_slint_favs(new_favs, favs); + }); }); } @@ -89,7 +120,9 @@ pub fn run() -> Result<(), anyhow::Error> { let app = App::new()?; let state = app.global::(); let list_shared = Rc::new(VecModel::default()); + let fav_shared = Rc::new(VecModel::default()); state.set_list(ModelRc::new(list_shared.clone())); + state.set_favorites(ModelRc::new(fav_shared.clone())); #[cfg(target_os = "linux")] let conf = runtime::RT diff --git a/gui/ui/app.slint b/gui/ui/app.slint index 1a72d7d..83bdf66 100644 --- a/gui/ui/app.slint +++ b/gui/ui/app.slint @@ -7,9 +7,11 @@ import { CheckBox, ScrollView, GridBox, + HorizontalBox, + ComboBox, } from "std-widgets.slint"; import { SettingsView } from "settings.slint"; -import { State, SItem } from "state.slint"; +import { State, SItem, SButtonItem } from "state.slint"; export { State, SItem } @@ -177,25 +179,24 @@ component GoodsListAdd { } component GoodButtons inherits ScrollView { - in property <[string]> favorites: ["Mjölk", "Ost", "Ägg", "Bullens", "Oboy", "Mjöl", "Havregryn", "KrosTom"]; in property button-width: 100; in property button-height: 60; in property button-spacing: 10; property count-x: floor(root.width / 1pt / (button-width + button-spacing)); - property count-y: ceil(favorites.length / count-x); + property count-y: ceil(State.favorites.length / count-x); viewport-height: (count-y * (button-height + button-spacing)) * 1pt; mouse-drag-pan-enabled: true; - for item[idx] in favorites: Button { + for item[idx] in State.favorites: Button { x: floor(mod(idx, count-x)) * (root.button-width + root.button-spacing) * 1pt; y: floor(idx / count-x) * (root.button-height + root.button-spacing) * 1pt; width: root.button-width * 1pt; height: root.button-height * 1pt; - text: item; + text: item.name; clicked => { - State.add-to-list(item); + State.add-to-list(item.name); } } } @@ -278,6 +279,26 @@ component ShopView inherits VerticalLayout { } } +component CatItem inherits HorizontalBox { + in-out property item; + Text { + text: item.name; + } + + ComboBox { + model: ["kyl", "skafferi", "frys", "övrigt"]; + current-value: item.category; + } +} + +component CatView inherits VerticalLayout { + in-out property <[SButtonItem]> cats; + + for item in cats: CatItem { + item: item; + } +} + export component App inherits Window { preferred-height: 700px; @@ -297,6 +318,13 @@ export component App inherits Window { } } + Tab { + title: "Categories"; + CatView { + cats <=> State.favorites; + } + } + Tab { title: "Settings"; SettingsView { } diff --git a/gui/ui/state.slint b/gui/ui/state.slint index 082ae9d..75cdc84 100644 --- a/gui/ui/state.slint +++ b/gui/ui/state.slint @@ -4,6 +4,10 @@ export struct SItem { amount: int, } +export struct SButtonItem{ + name: string, + category: string} + export global State { in-out property <[SItem]> list: [ { name: "Test 1", checked: true }, @@ -17,6 +21,11 @@ export global State { callback inc-amount(string); callback dec-amount(string); in-out property server-address; + in property <[SButtonItem]> favorites: [ + { name: "mjölk", category: "kyl" }, + { name: "ost", category: "kyl" }, + ]; + callback set-server-address(string); set-server-address(address) => { server-address = address; diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 5724275..67920a8 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -7,9 +7,13 @@ pub struct Item { } #[derive(Serialize, Deserialize, Default, Debug, Clone)] - pub struct ItemData { pub checked: bool, pub amount: i64, - // TODO: add number +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone)] +pub struct ButtonItem { + pub name: String, + pub category: Option, } diff --git a/srv/src/db/init.sql b/srv/src/db/init.sql index 3111164..da78852 100644 --- a/srv/src/db/init.sql +++ b/srv/src/db/init.sql @@ -3,4 +3,14 @@ CREATE TABLE itemname TEXT NOT NULL PRIMARY KEY, checked INT NOT NULL, -- actually a boolean amount INT NOT NULL - ) STRICT; \ No newline at end of file + ) STRICT; + +CREATE TABLE + IF NOT EXISTS adds ( + itemname TEXT NOT NULL, + date_added INT NOT NULL, + PRIMARY KEY (itemname, date_added) + ) STRICT; + +CREATE TABLE + IF NOT EXISTS categories (itemname TEXT NOT NULL PRIMARY KEY, category TEXT) STRICT; \ No newline at end of file diff --git a/srv/src/main.rs b/srv/src/main.rs index e4c4ccf..ea8eb4b 100644 --- a/srv/src/main.rs +++ b/srv/src/main.rs @@ -3,7 +3,7 @@ extern crate rocket; use std::{env, path::Path}; -use inkopslista_lib::{Item, ItemData}; +use inkopslista_lib::{ButtonItem, Item, ItemData}; use rocket::{ fs::{FileServer, NamedFile}, serde::json::Json, @@ -47,6 +47,42 @@ fn get_list(db: &State) -> Json> { Json(list) } +#[get("/api/favorites")] +fn get_favorites(db: &State) -> Json> { + let query = "WITH favorites as (SELECT itemname, COUNT(date_added) day_count + FROM adds + WHERE (date(julianday('now')) - date(date_added, 'unixepoch')) < 184 + GROUP BY itemname + HAVING day_count > 0) + SELECT favorites.itemname, category FROM favorites LEFT OUTER JOIN categories ON favorites.itemname = categories.itemname"; + let list: Vec = db + .prepare(query) + .unwrap() + .into_iter() + .map(|row| { + let row = row.unwrap(); + ButtonItem { + name: row.read::<&str, _>("itemname").to_string(), + category: row + .try_read::<&str, _>("category") + .ok() + .map(ToString::to_string), + } + }) + .collect(); + Json(list) +} + +#[put("/api/favourites/", data = "")] +fn set_categories(db: &State, itemname: &str, category: &str) { + let query = "INSERT INTO categories VALUES (?, ?) ON CONFLICT DO UPDATE SET category = ?"; + let mut statement = db.prepare(query).unwrap(); + statement.bind((1, itemname)).unwrap(); + statement.bind((2, category)).unwrap(); + statement.bind((3, category)).unwrap(); + while let Ok(sqlite::State::Row) = statement.next() {} +} + #[put("/api/list/", data = "")] fn put_list(item: &str, data: Json, db: &State) { // TODO: errorhandling @@ -60,6 +96,10 @@ fn put_list(item: &str, data: Json, db: &State) statement.bind((4, data.checked as i64)).unwrap(); statement.bind((5, data.amount)).unwrap(); while let Ok(sqlite::State::Row) = statement.next() {} + let query = "INSERT INTO adds VALUES(?, strftime('%s','now')) ON CONFLICT DO NOTHING"; + let mut statement = db.prepare(query).unwrap(); + statement.bind((1, item)).unwrap(); + while let Ok(sqlite::State::Row) = statement.next() {} } #[delete("/api/list/")] @@ -84,6 +124,16 @@ fn rocket() -> _ { .disable::(), ) .manage(db) - .mount("/", routes![index, get_list, put_list, delete_item]) + .mount( + "/", + routes![ + index, + get_list, + put_list, + delete_item, + get_favorites, + set_categories + ], + ) .mount("/", FileServer::from(www_path())) }