mirror of
https://github.com/hulthe/inkopslista.git
synced 2026-09-18 18:03:39 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3dd373403 |
@@ -3,5 +3,3 @@ target/
|
||||
**/*.rs.bk
|
||||
|
||||
*.sqlite
|
||||
|
||||
dabase
|
||||
Generated
+1141
-1629
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,7 +1,7 @@
|
||||
##################
|
||||
### BASE STAGE ###
|
||||
##################
|
||||
FROM rust:1.94.0 AS base
|
||||
FROM rust:1.92.0 AS base
|
||||
|
||||
RUN rustup target add wasm32-unknown-unknown
|
||||
RUN rustup target add x86_64-unknown-linux-musl
|
||||
|
||||
+38
-39
@@ -3,6 +3,44 @@ name = "inkopslista"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.12.26", features = ["json", "rustls-tls"], default-features = false }
|
||||
inkopslista-lib = { path = "../lib" }
|
||||
tokio = { version = "1.48.0", default-features = false, features = ["rt", "macros", "sync"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.148"
|
||||
anyhow = "1.0.100"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
tokio = { version = "1.48.0", default-features = false, features = ["fs", "rt-multi-thread"] }
|
||||
xdg = "3.0.0"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
tokio = { version = "1.48.0", default-features = false, features = ["rt-multi-thread"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen = "0.2.106"
|
||||
wasm-bindgen-futures = "0.4.56"
|
||||
web-sys = "0.3.83"
|
||||
|
||||
[dependencies.slint]
|
||||
version = "1.14.1"
|
||||
default-features = false
|
||||
features = [
|
||||
"live-preview",
|
||||
"backend-winit-wayland",
|
||||
"renderer-femtovg",
|
||||
"compat-1-2",
|
||||
"backend-android-activity-06",
|
||||
]
|
||||
|
||||
[build-dependencies]
|
||||
slint-build = "1.14.1"
|
||||
|
||||
# See https://github.com/rust-mobile/cargo-apk?tab=readme-ov-file#manifest
|
||||
[package.metadata.android]
|
||||
package = "sh.nubo.Inkopslista"
|
||||
@@ -25,42 +63,3 @@ name = "android.permission.INTERNET"
|
||||
|
||||
[[package.metadata.android.uses_permission]]
|
||||
name = "android.permission.ACCESS_NETWORK_STATE"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.12.26", features = ["json", "rustls-tls"], default-features = false }
|
||||
inkopslista-lib = { path = "../lib" }
|
||||
tokio = { version = "1.48.0", default-features = false, features = ["rt", "macros", "sync"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.148"
|
||||
anyhow = "1.0.100"
|
||||
|
||||
[dependencies.slint]
|
||||
version = "1.17.1"
|
||||
default-features = false
|
||||
features = [
|
||||
"live-preview",
|
||||
"backend-winit-wayland",
|
||||
"renderer-femtovg",
|
||||
"compat-1-2",
|
||||
"backend-android-activity-06",
|
||||
]
|
||||
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
tokio = { version = "1.48.0", default-features = false, features = ["fs", "rt-multi-thread"] }
|
||||
xdg = "3.0.0"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
tokio = { version = "1.48.0", default-features = false, features = ["rt-multi-thread"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen = "0.2.106"
|
||||
wasm-bindgen-futures = "0.4.56"
|
||||
web-sys = "0.3.83"
|
||||
|
||||
[build-dependencies]
|
||||
slint-build = "1.17.1"
|
||||
|
||||
+1
-11
@@ -1,14 +1,4 @@
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("target arch env");
|
||||
let style = match target_arch.as_str() {
|
||||
// For web, rely on dark mod detection based on browser settings.
|
||||
"wasm32" => "cosmic",
|
||||
|
||||
// For any other target, force dark mode.
|
||||
_ => "cosmic-dark",
|
||||
};
|
||||
let config = slint_build::CompilerConfiguration::new().with_style(style.into());
|
||||
let config = slint_build::CompilerConfiguration::new().with_style("cosmic-dark".into());
|
||||
slint_build::compile_with_config("ui/app.slint", config).expect("Slint build failed");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
cd "$(dirname 0)"
|
||||
CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-../../target}"
|
||||
|
||||
# TODO: cross compilation with cross --target=aarch64-unknown-linux-gnu
|
||||
cargo b -r
|
||||
|
||||
cp "$CARGO_TARGET_DIR/release/inkopslista" inköpslista
|
||||
|
||||
# TODO: cross compilation with --arch=aarch64
|
||||
flatpak-builder -v --force-clean --user --install-deps-from=flathub --repo=repo --install builddir sh.nubo.Inkopslista.yml
|
||||
@@ -1,6 +1,6 @@
|
||||
[Desktop Entry]
|
||||
Name=Inköpslista
|
||||
Exec=inkopslista %U
|
||||
Exec=inköpslista %U
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Icon=sh.nubo.Inkopslista
|
||||
|
||||
@@ -2,34 +2,43 @@ id: sh.nubo.Inkopslista
|
||||
runtime: org.freedesktop.Platform
|
||||
runtime-version: '25.08'
|
||||
sdk: org.freedesktop.Sdk
|
||||
sdk-extensions:
|
||||
- org.freedesktop.Sdk.Extension.rust-stable
|
||||
|
||||
build-options:
|
||||
append-path: /usr/lib/sdk/rust-stable/bin
|
||||
build-args:
|
||||
- --share=network
|
||||
|
||||
command: 'inkopslista'
|
||||
command: 'inköpslista'
|
||||
|
||||
modules:
|
||||
- name: inköpslista
|
||||
buildsystem: simple
|
||||
sources:
|
||||
- type: dir
|
||||
path: ../..
|
||||
# - type: script
|
||||
# dest-filename: gui
|
||||
# commands:
|
||||
# - echo "hello there"
|
||||
- type: file
|
||||
path: ./sh.nubo.Inkopslista.desktop
|
||||
- type: file
|
||||
path: ./inköpslista
|
||||
- type: file
|
||||
path: ../images/icon.svg
|
||||
- type: file
|
||||
path: ../images/icon_x512.png
|
||||
- type: file
|
||||
path: ../images/icon_x192.png
|
||||
- type: file
|
||||
path: ../images/icon_x144.png
|
||||
- type: file
|
||||
path: ../images/icon_x128.png
|
||||
- type: file
|
||||
path: ../images/icon_x72.png
|
||||
build-commands:
|
||||
- cargo build --release --locked
|
||||
- install -Dm755 target/release/inkopslista $FLATPAK_DEST/bin/inkopslista
|
||||
- install -Dm755 inköpslista $FLATPAK_DEST/bin/inköpslista
|
||||
- install -Dm644 sh.nubo.Inkopslista.desktop $FLATPAK_DEST/share/applications/sh.nubo.Inkopslista.desktop
|
||||
|
||||
- install -Dm644 gui/flatpak/sh.nubo.Inkopslista.desktop $FLATPAK_DEST/share/applications/sh.nubo.Inkopslista.desktop
|
||||
|
||||
- install -Dm644 gui/images/icon.svg $FLATPAK_DEST/share/icons/hicolor/scalable/apps/sh.nubo.Inkopslista.svg
|
||||
- install -Dm644 gui/images/icon_x512.png $FLATPAK_DEST/share/icons/hicolor/512x512/apps/sh.nubo.Inkopslista.png
|
||||
- install -Dm644 gui/images/icon_x192.png $FLATPAK_DEST/share/icons/hicolor/192x192/apps/sh.nubo.Inkopslista.png
|
||||
- install -Dm644 gui/images/icon_x144.png $FLATPAK_DEST/share/icons/hicolor/144x144/apps/sh.nubo.Inkopslista.png
|
||||
- install -Dm644 gui/images/icon_x128.png $FLATPAK_DEST/share/icons/hicolor/128x128/apps/sh.nubo.Inkopslista.png
|
||||
- install -Dm644 gui/images/icon_x72.png $FLATPAK_DEST/share/icons/hicolor/72x72/apps/sh.nubo.Inkopslista.png
|
||||
- install -Dm644 icon.svg $FLATPAK_DEST/share/icons/hicolor/scalable/apps/sh.nubo.Inkopslista.svg
|
||||
- install -Dm644 icon_x512.png $FLATPAK_DEST/share/icons/hicolor/512x512/apps/sh.nubo.Inkopslista.png
|
||||
- install -Dm644 icon_x192.png $FLATPAK_DEST/share/icons/hicolor/192x192/apps/sh.nubo.Inkopslista.png
|
||||
- install -Dm644 icon_x144.png $FLATPAK_DEST/share/icons/hicolor/144x144/apps/sh.nubo.Inkopslista.png
|
||||
- install -Dm644 icon_x128.png $FLATPAK_DEST/share/icons/hicolor/128x128/apps/sh.nubo.Inkopslista.png
|
||||
- install -Dm644 icon_x72.png $FLATPAK_DEST/share/icons/hicolor/72x72/apps/sh.nubo.Inkopslista.png
|
||||
|
||||
finish-args:
|
||||
# Wayland access
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Arrow / Arrow_Down_Up">
|
||||
<path id="Vector" d="M11 16L8 19M8 19L5 16M8 19V5M13 8L16 5M16 5L19 8M16 5V19" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 350 B |
+5
-6
@@ -18,24 +18,23 @@ pub use fs::*;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod fs {
|
||||
use crate::runtime::spawn;
|
||||
|
||||
use super::Config;
|
||||
use anyhow::{Context, Ok};
|
||||
use serde_json;
|
||||
|
||||
const CONFIG_FILE_NAME: &str = "config.json";
|
||||
const XDG_PREFIX: &str = "inköpslista";
|
||||
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 };
|
||||
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")?;
|
||||
|
||||
spawn(async {
|
||||
tokio::spawn(async {
|
||||
let res = tokio::fs::write(path, conf)
|
||||
.await
|
||||
.context("unable to write config file");
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use slint::ComponentHandle;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::{runtime::spawn, ui};
|
||||
|
||||
/// A thing for spawning I/O-tasks and updating the GUI `ConnectionStatus` with the result.
|
||||
#[derive(Clone)]
|
||||
pub struct Io {
|
||||
state: Arc<State>,
|
||||
}
|
||||
|
||||
struct State {
|
||||
status: mpsc::Sender<ui::ConnectionStatus>,
|
||||
}
|
||||
|
||||
impl Io {
|
||||
pub fn new(app: &ui::App) -> Self {
|
||||
let (tx, mut rx) = mpsc::channel(10);
|
||||
let app_weak = app.as_weak();
|
||||
|
||||
spawn(async move {
|
||||
loop {
|
||||
let Some(status) = rx.recv().await else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _ = app_weak.upgrade_in_event_loop(move |app| {
|
||||
app.global::<ui::State>().set_connection_status(status);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Io {
|
||||
state: Arc::new(State { status: tx }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn a fallible async function.
|
||||
///
|
||||
/// Update the `ConnectionStatus` in the GUI depending on whether the result is an error.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn spawn<Fut>(&self, f: impl FnOnce() -> Fut + Send + 'static)
|
||||
where
|
||||
Fut: Future<Output = anyhow::Result<()>>,
|
||||
Fut: Send + 'static,
|
||||
{
|
||||
let io = self.clone();
|
||||
spawn(async move {
|
||||
let _ = io.state.status.send(ui::ConnectionStatus::Syncing).await;
|
||||
let result = f().await;
|
||||
let was_error = result.is_err();
|
||||
if let Err(e) = result {
|
||||
eprintln!("{e:?}");
|
||||
}
|
||||
let _ = io
|
||||
.state
|
||||
.status
|
||||
.send(if was_error {
|
||||
ui::ConnectionStatus::Error
|
||||
} else {
|
||||
ui::ConnectionStatus::Idle
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
/// Spawn a fallible async function.
|
||||
///
|
||||
/// Update the `ConnectionStatus` in the GUI depending on whether the result is an error.
|
||||
// FIXME: this function is duplicated from the one above, but without the `Send` requirement on the future.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn spawn<Fut>(&self, f: impl FnOnce() -> Fut + Send + 'static)
|
||||
where
|
||||
Fut: Future<Output = anyhow::Result<()>>,
|
||||
Fut: 'static,
|
||||
{
|
||||
let io = self.clone();
|
||||
spawn(async move {
|
||||
let _ = io.state.status.send(ui::ConnectionStatus::Syncing).await;
|
||||
let result = f().await;
|
||||
let was_error = result.is_err();
|
||||
if let Err(e) = result {
|
||||
eprintln!("{e:?}");
|
||||
}
|
||||
let _ = io
|
||||
.state
|
||||
.status
|
||||
.send(if was_error {
|
||||
ui::ConnectionStatus::Error
|
||||
} else {
|
||||
ui::ConnectionStatus::Idle
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
+81
-91
@@ -2,7 +2,6 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
mod config;
|
||||
mod io;
|
||||
mod runtime;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
@@ -18,14 +17,11 @@ use std::{
|
||||
|
||||
use inkopslista_lib::{ButtonItem, Item, ItemData};
|
||||
use reqwest::Url;
|
||||
use slint::{ComponentHandle, Model, ModelRc, ToSharedString, VecModel};
|
||||
use slint::{Model, ModelRc, SharedString, ToSharedString, VecModel};
|
||||
|
||||
use crate::{config::Config, io::Io};
|
||||
use crate::{config::Config, runtime::spawn};
|
||||
|
||||
/// Exported slint types
|
||||
mod ui {
|
||||
slint::include_modules!();
|
||||
}
|
||||
slint::include_modules!();
|
||||
|
||||
/// GET "/api/list" from the server
|
||||
async fn get_list(conf: &Mutex<Config>) -> Result<Vec<Item>, anyhow::Error> {
|
||||
@@ -64,32 +60,12 @@ async fn delete_item(item: &str, conf: &Mutex<Config>) -> Result<(), anyhow::Err
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn put_category(
|
||||
itemname: &str,
|
||||
category: &str,
|
||||
conf: &Mutex<Config>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
let url: Url = format!(
|
||||
"{address}/api/favourites/{itemname}/category",
|
||||
address = conf.lock().unwrap().address
|
||||
)
|
||||
.parse()?;
|
||||
let _response = client
|
||||
.put(url)
|
||||
.body(category.to_string())
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert shopping list to slint types
|
||||
fn update_slint_list(new_list: Vec<Item>, list: &VecModel<ui::SItem>) {
|
||||
fn update_slint_list(new_list: Vec<Item>, list: &VecModel<SItem>) {
|
||||
list.clear();
|
||||
new_list
|
||||
.iter()
|
||||
.map(|item| ui::SItem {
|
||||
.map(|item| SItem {
|
||||
name: item.name.to_shared_string(),
|
||||
checked: item.data.checked,
|
||||
amount: item.data.amount as i32,
|
||||
@@ -97,43 +73,52 @@ fn update_slint_list(new_list: Vec<Item>, list: &VecModel<ui::SItem>) {
|
||||
.for_each(|item| list.push(item));
|
||||
}
|
||||
|
||||
fn update_slint_favs(new_favs: Vec<ButtonItem>, favs: &VecModel<ui::SButtonItem>) {
|
||||
fn update_slint_favs(new_favs: Vec<ButtonItem>, favs: &VecModel<SButtonItem>) {
|
||||
favs.clear();
|
||||
new_favs
|
||||
.into_iter()
|
||||
.map(|item| ui::SButtonItem {
|
||||
.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<Mutex<Config>>, app_weak: &slint::Weak<ui::App>, io: &Io) {
|
||||
fn refresh_list(conf: &Arc<Mutex<Config>>, app_weak: &slint::Weak<App>) {
|
||||
let conf = conf.clone();
|
||||
let app_weak = app_weak.clone();
|
||||
io.spawn(async move || {
|
||||
let new_list = get_list(&conf).await?;
|
||||
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<ui::SItem> = app.global::<ui::State>().get_list();
|
||||
let list: &VecModel<ui::SItem> =
|
||||
list.as_any().downcast_ref().expect("list is a VecModel");
|
||||
update_slint_list(new_list, list);
|
||||
let list: ModelRc<SItem> = app.global::<State>().get_list();
|
||||
let list: &VecModel<SItem> = list.as_any().downcast_ref().expect("list is a VecModel");
|
||||
update_slint_list(new_list, &list);
|
||||
});
|
||||
let new_favs = get_favorites(&conf).await?;
|
||||
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<ui::SButtonItem> = app.global::<ui::State>().get_favorites();
|
||||
let favs: &VecModel<ui::SButtonItem> =
|
||||
let favs: ModelRc<SButtonItem> = app.global::<State>().get_favorites();
|
||||
let favs: &VecModel<SButtonItem> =
|
||||
favs.as_any().downcast_ref().expect("favs is a vec model");
|
||||
update_slint_favs(new_favs, favs);
|
||||
});
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), anyhow::Error> {
|
||||
let app = ui::App::new()?;
|
||||
let io_ = Io::new(&app);
|
||||
let state = app.global::<ui::State>();
|
||||
let app = App::new()?;
|
||||
let state = app.global::<State>();
|
||||
let list_shared = Rc::new(VecModel::default());
|
||||
let fav_shared = Rc::new(VecModel::default());
|
||||
state.set_list(ModelRc::new(list_shared.clone()));
|
||||
@@ -163,24 +148,22 @@ pub fn run() -> Result<(), anyhow::Error> {
|
||||
let conf = conf_shared.clone();
|
||||
let app_weak = app.as_weak();
|
||||
// load shopping list from server on start
|
||||
refresh_list(&conf, &app_weak, &io_);
|
||||
let io = io_.clone();
|
||||
refresh_list(&conf, &app_weak);
|
||||
state.on_refresh_list(move || {
|
||||
refresh_list(&conf, &app_weak, &io);
|
||||
refresh_list(&conf, &app_weak);
|
||||
});
|
||||
|
||||
let conf = conf_shared.clone();
|
||||
let list = list_shared.clone();
|
||||
let io = io_.clone();
|
||||
state.on_add_to_list(move |item| {
|
||||
if let Some(index) = list.iter().position(|item2| item2.name == item) {
|
||||
let mut item2 = list.row_data(index).unwrap();
|
||||
item2.amount += 1;
|
||||
item2.amount = item2.amount + 1;
|
||||
let item3 = item2.clone();
|
||||
list.set_row_data(index, item2);
|
||||
let conf = conf.clone();
|
||||
io.spawn(async move || {
|
||||
put_list(
|
||||
spawn(async move {
|
||||
let res = put_list(
|
||||
&item,
|
||||
&ItemData {
|
||||
checked: item3.checked,
|
||||
@@ -188,19 +171,22 @@ pub fn run() -> Result<(), anyhow::Error> {
|
||||
},
|
||||
&conf,
|
||||
)
|
||||
.await
|
||||
.await;
|
||||
if let Err(err) = res {
|
||||
eprintln!("{err:?}");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
list.push(ui::SItem {
|
||||
list.push(SItem {
|
||||
checked: false,
|
||||
name: item.clone(),
|
||||
amount: 1,
|
||||
});
|
||||
let conf = conf.clone();
|
||||
io.spawn(async move || {
|
||||
spawn(async move {
|
||||
// TODO: add number to increment
|
||||
put_list(
|
||||
let res = put_list(
|
||||
&item,
|
||||
&ItemData {
|
||||
checked: false,
|
||||
@@ -208,27 +194,30 @@ pub fn run() -> Result<(), anyhow::Error> {
|
||||
},
|
||||
&conf,
|
||||
)
|
||||
.await
|
||||
.await;
|
||||
if let Err(err) = res {
|
||||
eprintln!("{err:?}");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let conf = conf_shared.clone();
|
||||
let list = list_shared.clone();
|
||||
let io = io_.clone();
|
||||
state.on_check_item(move |item| {
|
||||
let positem = list
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, item2)| item2.name == item);
|
||||
let Some((pos, mut item)) = positem else {
|
||||
return;
|
||||
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();
|
||||
|
||||
io.spawn(async move || {
|
||||
put_list(
|
||||
spawn(async move {
|
||||
let res = put_list(
|
||||
&item.name,
|
||||
&ItemData {
|
||||
checked: item.checked,
|
||||
@@ -236,13 +225,15 @@ pub fn run() -> Result<(), anyhow::Error> {
|
||||
},
|
||||
&conf,
|
||||
)
|
||||
.await
|
||||
.await;
|
||||
if let Err(err) = res {
|
||||
eprintln!("{err:?}");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let conf = conf_shared.clone();
|
||||
let list = list_shared.clone();
|
||||
let io = io_.clone();
|
||||
state.on_delete_checked(move || {
|
||||
let poss: Vec<_> = list
|
||||
.iter()
|
||||
@@ -256,30 +247,27 @@ pub fn run() -> Result<(), anyhow::Error> {
|
||||
|
||||
let conf = conf.clone();
|
||||
|
||||
io.spawn(async move || {
|
||||
let mut result = Ok(());
|
||||
spawn(async move {
|
||||
for (_, name) in poss {
|
||||
let r = delete_item(&name, &conf).await;
|
||||
if r.is_err() {
|
||||
result = r;
|
||||
let res = delete_item(&name, &conf).await;
|
||||
if let Err(err) = res {
|
||||
eprintln!("{err:?}");
|
||||
}
|
||||
}
|
||||
result
|
||||
});
|
||||
});
|
||||
|
||||
let conf = conf_shared.clone();
|
||||
let list = list_shared.clone();
|
||||
let io = io_.clone();
|
||||
state.on_inc_amount(move |item| {
|
||||
if let Some(index) = list.iter().position(|item2| item2.name == item) {
|
||||
let mut item2 = list.row_data(index).unwrap();
|
||||
item2.amount += 1;
|
||||
item2.amount = item2.amount + 1;
|
||||
let item3 = item2.clone();
|
||||
list.set_row_data(index, item2);
|
||||
let conf = conf.clone();
|
||||
io.spawn(async move || {
|
||||
put_list(
|
||||
spawn(async move {
|
||||
let res = put_list(
|
||||
&item,
|
||||
&ItemData {
|
||||
checked: item3.checked,
|
||||
@@ -287,28 +275,36 @@ pub fn run() -> Result<(), anyhow::Error> {
|
||||
},
|
||||
&conf,
|
||||
)
|
||||
.await
|
||||
.await;
|
||||
if let Err(err) = res {
|
||||
eprintln!("{err:?}");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
let conf = conf_shared.clone();
|
||||
let list = list_shared.clone();
|
||||
let io = io_.clone();
|
||||
state.on_dec_amount(move |item| {
|
||||
if let Some(index) = list.iter().position(|item2| item2.name == item) {
|
||||
let mut item2 = list.row_data(index).unwrap();
|
||||
item2.amount -= 1;
|
||||
item2.amount = item2.amount - 1;
|
||||
let item3 = item2.clone();
|
||||
list.set_row_data(index, item2);
|
||||
let conf = conf.clone();
|
||||
if item3.amount < 1 {
|
||||
list.remove(index);
|
||||
io.spawn(async move || delete_item(&item3.name, &conf).await);
|
||||
spawn(async move {
|
||||
let res = delete_item(&item3.name, &conf).await;
|
||||
if let Err(err) = res {
|
||||
eprintln!("{err:?}");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
io.spawn(async move || {
|
||||
spawn(async move {
|
||||
// TODO: add number to increment
|
||||
put_list(
|
||||
let res = put_list(
|
||||
&item,
|
||||
&ItemData {
|
||||
checked: item3.checked,
|
||||
@@ -316,22 +312,16 @@ pub fn run() -> Result<(), anyhow::Error> {
|
||||
},
|
||||
&conf,
|
||||
)
|
||||
.await
|
||||
.await;
|
||||
if let Err(err) = res {
|
||||
eprintln!("{err:?}");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let conf = conf_shared.clone();
|
||||
let io = io_.clone();
|
||||
|
||||
state.on_set_cat(move |itemname, cat| {
|
||||
let conf = conf.clone();
|
||||
|
||||
io.spawn(async move || put_category(&itemname, &cat, &conf).await);
|
||||
return;
|
||||
});
|
||||
|
||||
state.on_set_server_address(move |address| {
|
||||
conf_shared.lock().unwrap().address = address.to_string();
|
||||
#[cfg(target_os = "linux")]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
+15
-50
@@ -9,15 +9,15 @@ import {
|
||||
GridBox,
|
||||
HorizontalBox,
|
||||
ComboBox,
|
||||
Palette, Spinner, SpinBox,
|
||||
} from "std-widgets.slint";
|
||||
import { SettingsView } from "settings.slint";
|
||||
import { ConnectionStatus, State, SItem, SButtonItem } from "state.slint";
|
||||
export { ConnectionStatus, State, SItem }
|
||||
import { State, SItem, SButtonItem } from "state.slint";
|
||||
export { State, SItem }
|
||||
|
||||
|
||||
component GoodListItem inherits Rectangle {
|
||||
in-out property <SItem> item;
|
||||
background: Palette.alternate-background;
|
||||
background: #ffffff10;
|
||||
border-radius: 8pt;
|
||||
border-width: 10pt;
|
||||
HorizontalLayout {
|
||||
@@ -112,7 +112,7 @@ component PlusMinus inherits HorizontalLayout {
|
||||
|
||||
component GoodListItemAdd inherits Rectangle {
|
||||
in-out property <SItem> item;
|
||||
background: Palette.alternate-background;
|
||||
background: #ffffff10;
|
||||
border-radius: 8pt;
|
||||
border-width: 10pt;
|
||||
HorizontalLayout {
|
||||
@@ -179,22 +179,21 @@ component GoodsListAdd {
|
||||
}
|
||||
|
||||
component GoodButtons inherits ScrollView {
|
||||
in property <length> button-min-width: 100pt;
|
||||
in property <length> button-height: 60pt;
|
||||
in property <length> button-spacing: 10pt;
|
||||
in property <int> button-width: 100;
|
||||
in property <int> button-height: 60;
|
||||
in property <int> button-spacing: 10;
|
||||
|
||||
property <int> count-x: floor(root.width / (button-min-width + button-spacing));
|
||||
property <int> count-x: floor(root.width / 1pt / (button-width + button-spacing));
|
||||
property <int> count-y: ceil(State.favorites.length / count-x);
|
||||
property <length> button-width: ((self.width + button-spacing ) / count-x) - button-spacing;
|
||||
|
||||
viewport-height: (count-y * (button-height + button-spacing));
|
||||
viewport-height: (count-y * (button-height + button-spacing)) * 1pt;
|
||||
mouse-drag-pan-enabled: true;
|
||||
|
||||
for item[idx] in State.favorites: Button {
|
||||
x: floor(mod(idx, count-x)) * (root.button-width + root.button-spacing);
|
||||
y: floor(idx / count-x) * (root.button-height + root.button-spacing);
|
||||
width: root.button-width;
|
||||
height: root.button-height;
|
||||
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.name;
|
||||
clicked => {
|
||||
State.add-to-list(item.name);
|
||||
@@ -202,30 +201,7 @@ component GoodButtons inherits ScrollView {
|
||||
}
|
||||
}
|
||||
|
||||
component ConnStatusIndicator inherits Rectangle {
|
||||
width: 18px;
|
||||
Image {
|
||||
states [
|
||||
idle when State.connection-status == ConnectionStatus.Idle: {
|
||||
opacity: 0%;
|
||||
}
|
||||
syncing when State.connection-status == ConnectionStatus.Syncing: {
|
||||
colorize: green;
|
||||
opacity: 75% + 25% * sin(360deg * animation-tick() / 1s);
|
||||
}
|
||||
error when State.connection-status == ConnectionStatus.Error: {
|
||||
colorize: red;
|
||||
opacity: 100%;
|
||||
}
|
||||
]
|
||||
source: @image-url("../images/arrow-down-up.svg");
|
||||
width: 24px;
|
||||
height: self.width;
|
||||
}
|
||||
}
|
||||
|
||||
component RefreshButton inherits HorizontalLayout {
|
||||
spacing: 8px;
|
||||
alignment: center;
|
||||
Button {
|
||||
text: "Refresh";
|
||||
@@ -234,7 +210,6 @@ component RefreshButton inherits HorizontalLayout {
|
||||
State.refresh-list();
|
||||
}
|
||||
}
|
||||
ConnStatusIndicator { }
|
||||
}
|
||||
|
||||
component AddView inherits VerticalLayout {
|
||||
@@ -243,7 +218,7 @@ component AddView inherits VerticalLayout {
|
||||
padding: 8px;
|
||||
spacing: 8px;
|
||||
|
||||
RefreshButton {}
|
||||
RefreshButton { }
|
||||
|
||||
goods := GoodsListAdd {
|
||||
items <=> list;
|
||||
@@ -306,22 +281,16 @@ component ShopView inherits VerticalLayout {
|
||||
|
||||
component CatItem inherits HorizontalBox {
|
||||
in-out property <SButtonItem> item;
|
||||
|
||||
Text {
|
||||
text: item.name;
|
||||
vertical-alignment: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
model: ["kyl", "skafferi", "frys", "övrigt"];
|
||||
current-value: item.category;
|
||||
width: 60%;
|
||||
selected(category) => { State.set-cat(item.name, category) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
component CatView inherits VerticalLayout {
|
||||
in-out property <[SButtonItem]> cats;
|
||||
|
||||
@@ -332,12 +301,8 @@ component CatView inherits VerticalLayout {
|
||||
|
||||
export component App inherits Window {
|
||||
|
||||
title: "Inköpslista";
|
||||
icon: @image-url("../images/icon.svg");
|
||||
|
||||
preferred-height: 700px;
|
||||
preferred-width: 480px;
|
||||
|
||||
TabWidget {
|
||||
Tab {
|
||||
title: "Add";
|
||||
|
||||
+1
-10
@@ -6,14 +6,7 @@ export struct SItem {
|
||||
|
||||
export struct SButtonItem{
|
||||
name: string,
|
||||
category: string,
|
||||
}
|
||||
|
||||
export enum ConnectionStatus {
|
||||
Idle,
|
||||
Syncing,
|
||||
Error,
|
||||
}
|
||||
category: string}
|
||||
|
||||
export global State {
|
||||
in-out property <[SItem]> list: [
|
||||
@@ -27,7 +20,6 @@ export global State {
|
||||
callback delete-checked();
|
||||
callback inc-amount(string);
|
||||
callback dec-amount(string);
|
||||
callback set-cat(string, string);
|
||||
in-out property <string> server-address;
|
||||
in property <[SButtonItem]> favorites: [
|
||||
{ name: "mjölk", category: "kyl" },
|
||||
@@ -38,5 +30,4 @@ export global State {
|
||||
set-server-address(address) => {
|
||||
server-address = address;
|
||||
}
|
||||
in-out property <ConnectionStatus> connection-status: ConnectionStatus.Syncing;
|
||||
}
|
||||
|
||||
+1
-2
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "inkopslista-srv"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
rocket = { version = "0.5.1", features = ["json"] }
|
||||
@@ -9,4 +9,3 @@ serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
inkopslista-lib = { path = "../lib" }
|
||||
sqlite = "0.37.0"
|
||||
clap = { version = "4.6.6", features = ["derive", "env"] }
|
||||
|
||||
+11
-21
@@ -1,16 +1,16 @@
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
|
||||
use clap::Parser;
|
||||
use std::{env, path::Path};
|
||||
|
||||
use inkopslista_lib::{ButtonItem, Item, ItemData};
|
||||
use rocket::{
|
||||
State,
|
||||
fs::{FileServer, NamedFile},
|
||||
serde::json::Json,
|
||||
shield::Shield,
|
||||
State,
|
||||
};
|
||||
use sqlite::ConnectionThreadSafe;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// The directory where the web files for the gui is stored.
|
||||
fn www_path() -> &'static Path {
|
||||
@@ -49,14 +49,12 @@ fn get_list(db: &State<ConnectionThreadSafe>) -> Json<Vec<Item>> {
|
||||
|
||||
#[get("/api/favorites")]
|
||||
fn get_favorites(db: &State<ConnectionThreadSafe>) -> Json<Vec<ButtonItem>> {
|
||||
let opt = Opt::parse();
|
||||
|
||||
let query = format!("WITH favorites as (SELECT itemname, COUNT(date_added) day_count
|
||||
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 > {num_days})
|
||||
SELECT favorites.itemname, category FROM favorites LEFT OUTER JOIN categories ON favorites.itemname = categories.itemname", num_days = opt.num_days);
|
||||
HAVING day_count > 0)
|
||||
SELECT favorites.itemname, category FROM favorites LEFT OUTER JOIN categories ON favorites.itemname = categories.itemname";
|
||||
let list: Vec<ButtonItem> = db
|
||||
.prepare(query)
|
||||
.unwrap()
|
||||
@@ -75,8 +73,8 @@ fn get_favorites(db: &State<ConnectionThreadSafe>) -> Json<Vec<ButtonItem>> {
|
||||
Json(list)
|
||||
}
|
||||
|
||||
#[put("/api/favourites/<itemname>/category", data = "<category>")]
|
||||
fn put_category(db: &State<ConnectionThreadSafe>, itemname: &str, category: &str) {
|
||||
#[put("/api/favourites/<itemname>", data = "<category>")]
|
||||
fn set_categories(db: &State<ConnectionThreadSafe>, 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();
|
||||
@@ -113,18 +111,10 @@ fn delete_item(item: &str, db: &State<ConnectionThreadSafe>) {
|
||||
while let Ok(sqlite::State::Row) = statement.next() {}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Opt {
|
||||
#[clap(long, env = "DB_PATH")]
|
||||
db_path: PathBuf,
|
||||
#[clap(long, env = "NUM_DAYS", default_value = "5")]
|
||||
num_days: u32,
|
||||
}
|
||||
|
||||
#[launch]
|
||||
fn rocket() -> _ {
|
||||
let opt = Opt::parse();
|
||||
let db = sqlite::Connection::open_thread_safe(opt.db_path).unwrap();
|
||||
let db_path = env::var("DB_PATH").unwrap();
|
||||
let db = sqlite::Connection::open_thread_safe(db_path).unwrap();
|
||||
db.execute(include_str!("db/init.sql"))
|
||||
.expect("unable to initialize database");
|
||||
rocket::build()
|
||||
@@ -142,7 +132,7 @@ fn rocket() -> _ {
|
||||
put_list,
|
||||
delete_item,
|
||||
get_favorites,
|
||||
put_category
|
||||
set_categories
|
||||
],
|
||||
)
|
||||
.mount("/", FileServer::from(www_path()))
|
||||
|
||||
Reference in New Issue
Block a user