aaaa: commit worktree
Features: - meson - (almost) all strings are localized - relm4_icons no longer used - default.nix updated but not tested - updated dependencies - unused for now: paket-utils, paket/locker - build-aux/checks.sh now enforced - fixups in libpaket/tracking parsing - libpaket: RegTokenDecodeError instead of generic DecodeError - moving dependency versions to workspace Cargo.toml - default.nix adjusted - README.md added - dependency: relm4 version pinned to include fixes
1543
Cargo.lock
generated
70
Cargo.toml
|
@ -3,7 +3,7 @@ resolver = "2"
|
||||||
|
|
||||||
members = [
|
members = [
|
||||||
"libpaket",
|
"libpaket",
|
||||||
"paket",
|
"paket-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
@ -13,12 +13,64 @@ license = "AGPL-3.0-only"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
secrecy = { version = "0.10", features = ["serde"] }
|
adw = { package = "libadwaita", version = "0.7", features = ["v1_6"] }
|
||||||
|
aes-gcm = { version = "0.10.3"}
|
||||||
|
aperture = "0.9"
|
||||||
|
base64 = "0.22"
|
||||||
|
ed25519-dalek = { version = "2.1.0"}
|
||||||
|
futures = "0.3"
|
||||||
|
gettext = { package = "gettext-rs", version = "0.7", features = ["gettext-system"] }
|
||||||
|
glib = "0"
|
||||||
|
glycin = { version = "2.0", features = ["gdk4"] }
|
||||||
|
gtk = { package = "gtk4", version = "0.9", features = ["v4_16"] }
|
||||||
|
hmac = { version = "0.12.1"}
|
||||||
|
num_enum = { version = "0.7"}
|
||||||
|
oo7 = { version = "0.3" }
|
||||||
|
rand = "0.8"
|
||||||
|
random-string = "1.1.0"
|
||||||
|
regex = "1"
|
||||||
|
relm4 = { version = ">=0.9.1", features = [ "libadwaita", "macros", ] }
|
||||||
reqwest = { version = "0.12", features = ["json", "cookies", "gzip", "http2"] }
|
reqwest = { version = "0.12", features = ["json", "cookies", "gzip", "http2"] }
|
||||||
serde = { version = "1.0.195", features = ["derive"] }
|
secrecy = { version = "0.10", features = ["serde"] }
|
||||||
serde_json = "1.0.111"
|
serde = { version = "1", features = ["derive"] }
|
||||||
uuid = { version = "1.7.0", features = ["v4"] }
|
serde_ignored = "0.1"
|
||||||
relm4 = { git = "https://github.com/Relm4/Relm4.git", features = [
|
serde_json = "1"
|
||||||
"libadwaita",
|
serde_newtype = "0.1"
|
||||||
"macros",
|
serde_repr = { version = "0.1.18" }
|
||||||
] }
|
sha2 = "0.10.8"
|
||||||
|
thiserror = "1.0"
|
||||||
|
tracker = "0.2"
|
||||||
|
url = "2.5.0"
|
||||||
|
urlencoding = "2.1.3"
|
||||||
|
uuid = { version = "1.7", features = ["v4"] }
|
||||||
|
webkit = { package = "webkit6", version = "0.4" }
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "paket"
|
||||||
|
authors.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
default-run = "paket"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
adw = { workspace = true }
|
||||||
|
aperture = { workspace = true }
|
||||||
|
futures = { workspace = true }
|
||||||
|
gettext = { workspace = true }
|
||||||
|
glycin = { workspace = true }
|
||||||
|
gtk = { workspace = true }
|
||||||
|
libpaket = { path = "libpaket" }
|
||||||
|
oo7 = { workspace = true }
|
||||||
|
regex = { workspace = true }
|
||||||
|
relm4 = { workspace = true }
|
||||||
|
reqwest = { workspace = true }
|
||||||
|
secrecy = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
tracker = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
|
webkit = { workspace = true }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
glib-build-tools = "0.20"
|
||||||
|
|
29
README.md
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# paket
|
||||||
|
|
||||||
|
## Files and influences from other projects
|
||||||
|
|
||||||
|
- `i18n.rs` and `build-aux/{checks,dist-vendor}.sh` are directly copied from amberol
|
||||||
|
- `src/lib.rs` has code from amberol for initalizing gettext
|
||||||
|
- `paket-utils/src/lib.rs` `system_get_accept_languages` is based on libsoup3 and GNOME Web
|
||||||
|
- the Relm4 book, libadwaita docs and so much more
|
||||||
|
|
||||||
|
This project is standing upon of the shoulders of giants. I'm sorry if missed something here. I want to give proper attribution, please open an issue. Especially if I violated a copyright license by publishing it.
|
||||||
|
|
||||||
|
## How to run
|
||||||
|
|
||||||
|
### If you're a dev
|
||||||
|
|
||||||
|
Use `cargo run`, meson devenv is not supported (yet, merge requests are welcome).
|
||||||
|
|
||||||
|
### If you're a package maintainer
|
||||||
|
|
||||||
|
I don't recommend publishing this software in conventional stable releases, I want people to use the latest version due to the volatile nature of a reverse engineering project. An exception is NixOS, even though the quality of many packages are lacking there, people can easily mixin `nixpkgs-unstable` to receive the latest version.
|
||||||
|
|
||||||
|
|
||||||
|
If you want to package it:
|
||||||
|
|
||||||
|
- wait for a release
|
||||||
|
- make sure dependencies are met, `cargo auditable` and the ones in `./meson.build`
|
||||||
|
- do to usual meson dance (`setup`, `compile`, `install`). you're distro has probably an abstraction for meson packages, use that if possible.
|
||||||
|
- e.g. Alpine Linux has `abuild-meson` as an wrapper to `meson setup` to apply distro defaults for a release
|
||||||
|
- if you're distro doesn't have support for meson, then don't package this. please. i will write code that breaks running on you're system if you screw up packaging. [this comment reflects my opinion to 100%.](https://github.com/bottlesdevs/Bottles/issues/2345#issuecomment-1737334499)
|
2
assets/copy-symbolic.svg
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 0 3 c 0 -1.644531 1.355469 -3 3 -3 h 5 c 1.644531 0 3 1.355469 3 3 c 0 0.550781 -0.449219 1 -1 1 s -1 -0.449219 -1 -1 c 0 -0.570312 -0.429688 -1 -1 -1 h -5 c -0.570312 0 -1 0.429688 -1 1 v 5 c 0 0.570312 0.429688 1 1 1 c 0.550781 0 1 0.449219 1 1 s -0.449219 1 -1 1 c -1.644531 0 -3 -1.355469 -3 -3 z m 5 5 c 0 -1.644531 1.355469 -3 3 -3 h 5 c 1.644531 0 3 1.355469 3 3 v 5 c 0 1.644531 -1.355469 3 -3 3 h -5 c -1.644531 0 -3 -1.355469 -3 -3 z m 2 0 v 5 c 0 0.570312 0.429688 1 1 1 h 5 c 0.570312 0 1 -0.429688 1 -1 v -5 c 0 -0.570312 -0.429688 -1 -1 -1 h -5 c -0.570312 0 -1 0.429688 -1 1 z m 0 0" fill="#222222"/></svg>
|
After Width: | Height: | Size: 759 B |
2
assets/four-arrows-pointing-outward-symbolic.svg
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 1 6 c -0.550781 0 -1 -0.449219 -1 -1 v -5 h 5 c 0.550781 0 1 0.449219 1 1 s -0.449219 1 -1 1 h -1.585938 l 2.292969 2.292969 c 0.390625 0.390625 0.390625 1.023437 0 1.414062 c -0.1875 0.1875 -0.441406 0.292969 -0.707031 0.292969 s -0.519531 -0.105469 -0.707031 -0.292969 l -2.292969 -2.292969 v 1.585938 c 0 0.550781 -0.449219 1 -1 1 z m 0 0"/><path d="m 15 6 c 0.550781 0 1 -0.449219 1 -1 v -5 h -5 c -0.550781 0 -1 0.449219 -1 1 s 0.449219 1 1 1 h 1.585938 l -2.292969 2.292969 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 c 0.1875 0.1875 0.441406 0.292969 0.707031 0.292969 s 0.519531 -0.105469 0.707031 -0.292969 l 2.292969 -2.292969 v 1.585938 c 0 0.550781 0.449219 1 1 1 z m 0 0"/><path d="m 1 10 c -0.550781 0 -1 0.449219 -1 1 v 5 h 5 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 h -1.585938 l 2.292969 -2.292969 c 0.390625 -0.390625 0.390625 -1.023437 0 -1.414062 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 s -0.519531 0.105469 -0.707031 0.292969 l -2.292969 2.292969 v -1.585938 c 0 -0.550781 -0.449219 -1 -1 -1 z m 0 0"/><path d="m 15 10 c 0.550781 0 1 0.449219 1 1 v 5 h -5 c -0.550781 0 -1 -0.449219 -1 -1 s 0.449219 -1 1 -1 h 1.585938 l -2.292969 -2.292969 c -0.390625 -0.390625 -0.390625 -1.023437 0 -1.414062 c 0.1875 -0.1875 0.441406 -0.292969 0.707031 -0.292969 s 0.519531 0.105469 0.707031 0.292969 l 2.292969 2.292969 v -1.585938 c 0 -0.550781 0.449219 -1 1 -1 z m 0 0"/></g></svg>
|
After Width: | Height: | Size: 1.5 KiB |
4
assets/loupe-large-symbolic.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m 6.5 0 c -3.578125 0 -6.5 2.921875 -6.5 6.5 s 2.921875 6.5 6.5 6.5 c 1.429688 0 2.753906 -0.46875 3.828125 -1.257812 l 2.945313 2.945312 c 0.957031 0.9375 2.363281 -0.5 1.40625 -1.4375 l -2.929688 -2.929688 c 0.785156 -1.074218 1.25 -2.394531 1.25 -3.820312 c 0 -3.578125 -2.921875 -6.5 -6.5 -6.5 z m 0 2 c 2.496094 0 4.5 2.003906 4.5 4.5 s -2.003906 4.5 -4.5 4.5 s -4.5 -2.003906 -4.5 -4.5 s 2.003906 -4.5 4.5 -4.5 z m 0 0" fill="#2e3436"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 590 B |
2
assets/mail-symbolic.svg
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 2.1875 2 c -1.207031 0 -2.1875 0.800781 -2.1875 2 v 7.976562 c 0 1.203126 0.980469 2 2.1875 2 h 11.625 c 1.207031 0 2.1875 -1.015624 2.1875 -2.21875 v -7.539062 c 0 -1.199219 -0.980469 -2.21875 -2.1875 -2.21875 z m -0.1875 2 h 12 v 8 h -12 z m 0 0"/><path d="m 14.691406 2.605469 l -6.699218 5.261719 l -5.691407 -4.269532 l -0.601562 0.800782 l 6.308593 4.730468 l 7.300782 -5.738281 z m 0 0"/></g></svg>
|
After Width: | Height: | Size: 561 B |
2
assets/minus-symbolic.svg
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 3 7 h 10 v 2 h -10 z m 0 0" fill="#222222"/></svg>
|
After Width: | Height: | Size: 188 B |
8
assets/package-x-generic-symbolic.svg
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g fill="#2e3434">
|
||||||
|
<path d="m 4.023438 0.996094 c -1.660157 0 -3.027344 1.367187 -3.027344 3.027344 v 7.910156 c 0 1.660156 1.367187 3.027344 3.027344 3.027344 h 7.949218 c 1.660156 0 3.027344 -1.367188 3.027344 -3.027344 v -7.910156 c 0 -1.660157 -1.367188 -3.027344 -3.027344 -3.027344 z m 0 2 h 7.949218 c 0.585938 0 1.027344 0.441406 1.027344 1.027344 v 7.910156 c 0 0.585937 -0.441406 1.027344 -1.027344 1.027344 h -7.949218 c -0.585938 0 -1.027344 -0.441407 -1.027344 -1.027344 v -7.910156 c 0 -0.585938 0.441406 -1.027344 1.027344 -1.027344 z m 0 0"/>
|
||||||
|
<path d="m 4.023438 1 c -1.660157 0 -3.027344 1.367188 -3.027344 3.027344 v 0.945312 s 1.390625 2.03125 3.027344 2.03125 h 7.949218 c 1.46875 0 3.027344 -2.03125 3.027344 -2.03125 v -0.945312 c 0 -1.660156 -1.367188 -3.027344 -3.027344 -3.027344 z m 0 2 h 7.949218 c 0.585938 0 1.027344 0.441406 1.027344 1.027344 v 0.945312 c 0 0.585938 -0.441406 1.027344 -1.027344 1.027344 h -7.949218 c -0.585938 0 -1.027344 -0.441406 -1.027344 -1.027344 v -0.945312 c 0 -0.585938 0.441406 -1.027344 1.027344 -1.027344 z m 0 0"/>
|
||||||
|
<path d="m 7 0.996094 h 2 v 10.03125 l -2 -0.988282 z m 0 0"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
15
assets/parcel-locker-symbolic.svg
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" sodipodi:docname="parcel-locker-symbolic.svg" inkscape:version="1.4 (e7c3feb100, 2024-10-09)">
|
||||||
|
<sodipodi:namedview pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="48.507525" inkscape:cx="10.627217" inkscape:cy="7.1638369" inkscape:window-width="1920" inkscape:window-height="1139" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="svg5" showguides="true" showgrid="true">
|
||||||
|
<inkscape:grid units="px" originx="0" originy="0" spacingx="1" spacingy="1" empcolor="#0099e5" empopacity="0.30196078" empspacing="5" enabled="true" visible="true"/>
|
||||||
|
<sodipodi:guide position="9.3294461,11.655977" inkscape:locked="false"/>
|
||||||
|
<sodipodi:guide position="4.1636297,8.9542866" inkscape:locked="false"/>
|
||||||
|
<sodipodi:guide position="7.9247905,10.250752" inkscape:locked="false"/>
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<g fill="none" stroke="#000000">
|
||||||
|
<path d="m 0.56825749 7.9556049 l 14.81021051 0.0355161 z" stroke-width="0.315"/>
|
||||||
|
<path d="m 6.7463557 1.7691283 v 12.3770547 z" stroke-width="0.444664"/>
|
||||||
|
<path d="m 8.221279 4.416161 h 4.294881 v 2.238365 h -4.294881 z" stroke-width="0.478604"/>
|
||||||
|
<rect height="13.556851" rx="2.5" stroke-width="2.42" width="13.703" x="1.22449" y="1.195335"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
39
assets/person-symbolic.svg
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
height="16px"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
width="16px"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="person-symbolic.svg"
|
||||||
|
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="56.3125"
|
||||||
|
inkscape:cx="8.008879"
|
||||||
|
inkscape:cy="8.0532741"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1139"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<path
|
||||||
|
d="m 8 1 c -1.65625 0 -3 1.34375 -3 3 s 1.34375 3 3 3 s 3 -1.34375 3 -3 s -1.34375 -3 -3 -3 z m -1.5 7 c -2.492188 0 -4.5 2.007812 -4.5 4.5 v 1.5 c 0 1 1 1 1 1 h 10 s 1 0 1 -1 v -1.5 c 0 -2.492188 -2.007812 -4.5 -4.5 -4.5 z m 0 0"
|
||||||
|
fill="#222222"
|
||||||
|
id="path1"
|
||||||
|
style="fill:none;stroke:#000000;stroke-opacity:1;stroke-width:1.5;stroke-dasharray:none" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
2
assets/plus-symbolic.svg
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 7 3 v 4 h -4 v 2 h 4 v 4 h 2 v -4 h 4 v -2 h -4 v -4 z m 0 0" fill="#222222"/></svg>
|
After Width: | Height: | Size: 222 B |
2
assets/qr-code-scanner-symbolic.svg
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 2 0 s -0.457031 -0.015625 -0.949219 0.230469 c -0.488281 0.246093 -1.050781 0.9375 -1.050781 1.769531 v 3 h 2 v -3 h 3 v -2 z m 9 0 v 2 h 3 v 3 h 2 v -3 c 0 -0.832031 -0.5625 -1.523438 -1.050781 -1.769531 c -0.492188 -0.246094 -0.949219 -0.230469 -0.949219 -0.230469 z m -8 3 v 2 h 2 v -2 z m 2 2 v 2 h 2 v -2 z m 3 -2 v 5 h 5 v -5 z m 0 5 h -5 v 5 h 5 z m 1 -4 h 3 v 3 h -3 z m 1 1 v 1 h 1 v -1 z m -6 4 h 3 v 3 h -3 z m 5 0 v 3 h 1 v -1 h 1 v -1 h 1 v -1 z m 3 1 v 1 h 1 v -1 z m 0 1 h -1 v 1 h 1 z m 0 1 v 1 h 1 v -1 z m -1 0 h -1 v 1 h 1 z m -6 -2 v 1 h 1 v -1 z m -5 1 v 3 c 0 0.832031 0.5625 1.523438 1.050781 1.769531 c 0.492188 0.246094 0.949219 0.230469 0.949219 0.230469 h 3 v -2 h -3 v -3 z m 14 0 v 3 h -3 v 2 h 3 s 0.457031 0.015625 0.949219 -0.230469 c 0.488281 -0.246093 1.050781 -0.9375 1.050781 -1.769531 v -3 z m 0 0" fill="#222222"/></svg>
|
After Width: | Height: | Size: 996 B |
459
build-aux/checks.sh
Executable file
|
@ -0,0 +1,459 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# SPDX-FileCopyrightText: 2021 Alejandro Domínguez
|
||||||
|
# SPDX-FileCopyrightText: 2022 Kévin Commaile
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
export LC_ALL=C
|
||||||
|
|
||||||
|
# Usage info
|
||||||
|
show_help() {
|
||||||
|
cat << EOF
|
||||||
|
Run conformity checks on the current Rust project.
|
||||||
|
|
||||||
|
If a dependency is not found, helps the user to install it.
|
||||||
|
|
||||||
|
USAGE: ${0##*/} [OPTIONS]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
-f, --force-install Install missing dependencies without asking
|
||||||
|
-v, --verbose Use verbose output
|
||||||
|
-h, --help Display this help and exit
|
||||||
|
|
||||||
|
ERROR CODES:
|
||||||
|
1 Check failed
|
||||||
|
2 Missing dependency
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Style helpers
|
||||||
|
act="\e[1;32m"
|
||||||
|
err="\e[1;31m"
|
||||||
|
pos="\e[32m"
|
||||||
|
neg="\e[31m"
|
||||||
|
res="\e[0m"
|
||||||
|
|
||||||
|
# Common styled strings
|
||||||
|
Installing="${act}Installing${res}"
|
||||||
|
Checking=" ${act}Checking${res}"
|
||||||
|
Failed=" ${err}Failed${res}"
|
||||||
|
error="${err}error:${res}"
|
||||||
|
invalid="${neg}Invalid input${res}"
|
||||||
|
ok="${pos}ok${res}"
|
||||||
|
fail="${neg}fail${res}"
|
||||||
|
|
||||||
|
# Initialize variables
|
||||||
|
force_install=0
|
||||||
|
verbose=0
|
||||||
|
|
||||||
|
# Helper functions
|
||||||
|
# Sort to_sort in natural order.
|
||||||
|
sort() {
|
||||||
|
local size=${#to_sort[@]}
|
||||||
|
local swapped=0;
|
||||||
|
|
||||||
|
for (( i = 0; i < $size-1; i++ ))
|
||||||
|
do
|
||||||
|
swapped=0
|
||||||
|
for ((j = 0; j < $size-1-$i; j++ ))
|
||||||
|
do
|
||||||
|
if [[ "${to_sort[$j]}" > "${to_sort[$j+1]}" ]]
|
||||||
|
then
|
||||||
|
temp="${to_sort[$j]}";
|
||||||
|
to_sort[$j]="${to_sort[$j+1]}";
|
||||||
|
to_sort[$j+1]="$temp";
|
||||||
|
swapped=1;
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $swapped -eq 0 ]]; then
|
||||||
|
break;
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove common entries in to_diff1 and to_diff2.
|
||||||
|
diff() {
|
||||||
|
for i in ${!to_diff1[@]}; do
|
||||||
|
for j in ${!to_diff2[@]}; do
|
||||||
|
if [[ "${to_diff1[$i]}" == "${to_diff2[$j]}" ]]; then
|
||||||
|
unset to_diff1[$i]
|
||||||
|
unset to_diff2[$j]
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if rustup is available.
|
||||||
|
# Argument:
|
||||||
|
# '-i' to install if missing.
|
||||||
|
check_rustup() {
|
||||||
|
if ! which rustup &> /dev/null; then
|
||||||
|
if [[ "$1" == '-i' ]]; then
|
||||||
|
echo -e "$Installing rustup…"
|
||||||
|
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain nightly
|
||||||
|
export PATH=$PATH:$HOME/.cargo/bin
|
||||||
|
if ! which rustup &> /dev/null; then
|
||||||
|
echo -e "$Failed to install rustup"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install cargo via rustup.
|
||||||
|
install_cargo() {
|
||||||
|
check_rustup -i
|
||||||
|
if ! which cargo >/dev/null 2>&1; then
|
||||||
|
echo -e "$Failed to install cargo"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if cargo is available. If not, ask to install it.
|
||||||
|
check_cargo() {
|
||||||
|
if ! which cargo >/dev/null 2>&1; then
|
||||||
|
echo "Unable to find cargo"
|
||||||
|
|
||||||
|
if [[ $force_install -eq 1 ]]; then
|
||||||
|
install_cargo
|
||||||
|
elif [ ! -t 1 ]; then
|
||||||
|
exit 2
|
||||||
|
elif check_rustup; then
|
||||||
|
echo -e "$error rustup is installed but the cargo command isn't available"
|
||||||
|
exit 2
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "y: Install cargo via rustup"
|
||||||
|
echo "N: Don't install cargo and abort checks"
|
||||||
|
echo ""
|
||||||
|
while true; do
|
||||||
|
echo -n "Install cargo? [y/N]: "; read yn < /dev/tty
|
||||||
|
case $yn in
|
||||||
|
[Yy]* )
|
||||||
|
install_cargo
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
[Nn]* | "" )
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
* )
|
||||||
|
echo $invalid
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $verbose -eq 1 ]]; then
|
||||||
|
echo ""
|
||||||
|
rustc -Vv && cargo +nightly -Vv
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install rustfmt with rustup.
|
||||||
|
install_rustfmt() {
|
||||||
|
check_rustup -i
|
||||||
|
|
||||||
|
echo -e "$Installing rustfmt…"
|
||||||
|
rustup component add --toolchain nightly rustfmt
|
||||||
|
if ! cargo +nightly fmt --version >/dev/null 2>&1; then
|
||||||
|
echo -e "$Failed to install rustfmt"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run rustfmt to enforce code style.
|
||||||
|
run_rustfmt() {
|
||||||
|
if ! cargo fmt --version >/dev/null 2>&1; then
|
||||||
|
if [[ $force_install -eq 1 ]]; then
|
||||||
|
install_rustfmt
|
||||||
|
elif [ ! -t 1 ]; then
|
||||||
|
echo "Unable to check the project's code style, because rustfmt could not be run"
|
||||||
|
exit 2
|
||||||
|
else
|
||||||
|
echo "Rustfmt is needed to check the project's code style, but it isn’t available"
|
||||||
|
echo ""
|
||||||
|
echo "y: Install rustfmt via rustup"
|
||||||
|
echo "N: Don't install rustfmt and abort checks"
|
||||||
|
echo ""
|
||||||
|
while true; do
|
||||||
|
echo -n "Install rustfmt? [y/N]: "; read yn < /dev/tty
|
||||||
|
case $yn in
|
||||||
|
[Yy]* )
|
||||||
|
install_rustfmt
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
[Nn]* | "" )
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
* )
|
||||||
|
echo $invalid
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "$Checking code style…"
|
||||||
|
|
||||||
|
if [[ $verbose -eq 1 ]]; then
|
||||||
|
echo ""
|
||||||
|
cargo fmt --version
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! cargo fmt --all -- --check; then
|
||||||
|
echo -e " Checking code style result: $fail"
|
||||||
|
echo "Please fix the above issues, either manually or by running: cargo fmt --all"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e " Checking code style result: $ok"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Install typos with cargo.
|
||||||
|
install_typos() {
|
||||||
|
echo -e "$Installing typos…"
|
||||||
|
cargo install typos-cli
|
||||||
|
if ! typos --version >/dev/null 2>&1; then
|
||||||
|
echo -e "$Failed to install typos"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run typos to check for spelling mistakes.
|
||||||
|
run_typos() {
|
||||||
|
if ! typos --version >/dev/null 2>&1; then
|
||||||
|
if [[ $force_install -eq 1 ]]; then
|
||||||
|
install_typos
|
||||||
|
elif [ ! -t 1 ]; then
|
||||||
|
echo "Unable to check spelling mistakes, because typos could not be run"
|
||||||
|
exit 2
|
||||||
|
else
|
||||||
|
echo "Typos is needed to check spelling mistakes, but it isn’t available"
|
||||||
|
echo ""
|
||||||
|
echo "y: Install typos via cargo"
|
||||||
|
echo "N: Don't install typos and abort checks"
|
||||||
|
echo ""
|
||||||
|
while true; do
|
||||||
|
echo -n "Install typos? [y/N]: "; read yn < /dev/tty
|
||||||
|
case $yn in
|
||||||
|
[Yy]* )
|
||||||
|
install_typos
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
[Nn]* | "" )
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
* )
|
||||||
|
echo $invalid
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "$Checking spelling mistakes…"
|
||||||
|
|
||||||
|
if [[ $verbose -eq 1 ]]; then
|
||||||
|
echo ""
|
||||||
|
typos --version
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! typos --color always; then
|
||||||
|
echo -e " Checking spelling mistakes result: $fail"
|
||||||
|
echo "Please fix the above issues, either manually or by running: typos -w"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e " Checking spelling mistakes result: $ok"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if files in POTFILES are correct.
|
||||||
|
#
|
||||||
|
# This checks, in that order:
|
||||||
|
# - All files exist
|
||||||
|
# - All files with translatable strings are present and only those
|
||||||
|
# - Files are sorted alphabetically
|
||||||
|
#
|
||||||
|
# This assumes the following:
|
||||||
|
# - POTFILES.in is located at 'po/POTFILES.in'
|
||||||
|
# - UI (Glade) files are located in 'src/gtk/' and use 'translatable="yes"'
|
||||||
|
# - Rust files are located in 'src' and use 'i18n' methods or macros
|
||||||
|
check_potfiles() {
|
||||||
|
echo -e "$Checking po/POTFILES.in…"
|
||||||
|
|
||||||
|
local ret=0
|
||||||
|
|
||||||
|
# Check that files in POTFILES exist.
|
||||||
|
while read -r line; do
|
||||||
|
if [[ -n $line && ${line::1} != '#' ]]; then
|
||||||
|
if [[ ! -f $line ]]; then
|
||||||
|
echo -e "$error File '$line' in POTFILES does not exist"
|
||||||
|
ret=1
|
||||||
|
fi
|
||||||
|
if [[ ${line:(-3):3} == '.ui' ]]; then
|
||||||
|
ui_potfiles+=($line)
|
||||||
|
elif [[ ${line:(-3):3} == '.rs' ]]; then
|
||||||
|
rs_potfiles+=($line)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < po/POTFILES.in
|
||||||
|
|
||||||
|
if [[ ret -eq 1 ]]; then
|
||||||
|
echo -e " Checking po/POTFILES.in result: $fail"
|
||||||
|
echo "Please fix the above issues"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get UI files with 'translatable="yes"'.
|
||||||
|
#ui_files=(`grep -lIr 'translatable="yes"' src/gtk/*.ui`)
|
||||||
|
|
||||||
|
# Get Rust files with regex 'i18n[!]?\('.
|
||||||
|
rs_files=(`grep -lIrE 'i18n[!]?\(' --exclude=src/i18n.rs src/*`)
|
||||||
|
|
||||||
|
# Remove common files
|
||||||
|
#to_diff1=("${ui_potfiles[@]}")
|
||||||
|
#to_diff2=("${ui_files[@]}")
|
||||||
|
#diff
|
||||||
|
#ui_potfiles=("${to_diff1[@]}")
|
||||||
|
#ui_files=("${to_diff2[@]}")
|
||||||
|
|
||||||
|
to_diff1=("${rs_potfiles[@]}")
|
||||||
|
to_diff2=("${rs_files[@]}")
|
||||||
|
diff
|
||||||
|
rs_potfiles=("${to_diff1[@]}")
|
||||||
|
rs_files=("${to_diff2[@]}")
|
||||||
|
|
||||||
|
potfiles_count=$((${#ui_potfiles[@]} + ${#rs_potfiles[@]}))
|
||||||
|
if [[ $potfiles_count -eq 1 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "$error Found 1 file in POTFILES without translatable strings:"
|
||||||
|
ret=1
|
||||||
|
elif [[ $potfiles_count -ne 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "$error Found $potfiles_count files in POTFILES without translatable strings:"
|
||||||
|
ret=1
|
||||||
|
fi
|
||||||
|
for file in ${ui_potfiles[@]}; do
|
||||||
|
echo $file
|
||||||
|
done
|
||||||
|
for file in ${rs_potfiles[@]}; do
|
||||||
|
echo $file
|
||||||
|
done
|
||||||
|
|
||||||
|
let files_count=$((${#ui_files[@]} + ${#rs_files[@]}))
|
||||||
|
if [[ $files_count -eq 1 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "$error Found 1 file with translatable strings not present in POTFILES:"
|
||||||
|
ret=1
|
||||||
|
elif [[ $files_count -ne 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "$error Found $files_count with translatable strings not present in POTFILES:"
|
||||||
|
ret=1
|
||||||
|
fi
|
||||||
|
for file in ${ui_files[@]}; do
|
||||||
|
echo $file
|
||||||
|
done
|
||||||
|
for file in ${rs_files[@]}; do
|
||||||
|
echo $file
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ret -eq 1 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo -e " Checking po/POTFILES.in result: $fail"
|
||||||
|
echo "Please fix the above issues"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check sorted alphabetically
|
||||||
|
to_sort=("${potfiles[@]}")
|
||||||
|
sort
|
||||||
|
for i in ${!potfiles[@]}; do
|
||||||
|
if [[ "${potfiles[$i]}" != "${to_sort[$i]}" ]]; then
|
||||||
|
echo -e "$error Found file '${potfiles[$i]}' before '${to_sort[$i]}' in POTFILES"
|
||||||
|
ret=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ret -eq 1 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo -e " Checking po/POTFILES.in result: $fail"
|
||||||
|
echo "Please fix the above issues"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e " Checking po/POTFILES.in result: $ok"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if files in src/paket.gresource.xml are sorted alphabetically.
|
||||||
|
check_resources() {
|
||||||
|
echo -e "$Checking paket.gresource.xml…"
|
||||||
|
|
||||||
|
local ret=0
|
||||||
|
|
||||||
|
# Get files.
|
||||||
|
regex="<file .*>(.*)</file>"
|
||||||
|
while read -r line; do
|
||||||
|
if [[ $line =~ $regex ]]; then
|
||||||
|
files+=("${BASH_REMATCH[1]}")
|
||||||
|
fi
|
||||||
|
done < paket.gresource.xml
|
||||||
|
|
||||||
|
# Check sorted alphabetically
|
||||||
|
to_sort=("${files[@]}")
|
||||||
|
sort
|
||||||
|
for i in ${!files[@]}; do
|
||||||
|
if [[ "${files[$i]}" != "${to_sort[$i]}" ]]; then
|
||||||
|
echo -e "$error Found file '${files[$i]#src/}' before '${to_sort[$i]#src/}' in paket.gresource.xml"
|
||||||
|
ret=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ret -eq 1 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo -e " Checking paket.gresource.xml result: $fail"
|
||||||
|
echo "Please fix the above issues"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e " Checking paket.gresource.xml result: $ok"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check arguments
|
||||||
|
while [[ "$1" ]]; do case $1 in
|
||||||
|
-f | --force-install )
|
||||||
|
force_install=1
|
||||||
|
;;
|
||||||
|
-v | --verbose )
|
||||||
|
verbose=1
|
||||||
|
;;
|
||||||
|
-h | --help )
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
show_help >&2
|
||||||
|
exit 1
|
||||||
|
esac; shift; done
|
||||||
|
|
||||||
|
# Run
|
||||||
|
check_cargo
|
||||||
|
echo ""
|
||||||
|
run_rustfmt
|
||||||
|
echo ""
|
||||||
|
# TODO: typos
|
||||||
|
#run_typos
|
||||||
|
#echo ""
|
||||||
|
check_potfiles
|
||||||
|
echo ""
|
||||||
|
check_resources
|
||||||
|
echo ""
|
13
build-aux/dist-vendor.sh
Executable file
|
@ -0,0 +1,13 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# SPDX-FileCopyrightText: 2019 Christopher Davis <brainblasted@disroot.org>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
export SOURCE_ROOT="$1"
|
||||||
|
export DIST="$2"
|
||||||
|
|
||||||
|
cd "$SOURCE_ROOT"
|
||||||
|
mkdir "$DIST"/.cargo
|
||||||
|
cargo vendor | sed 's/^directory = ".*"/directory = "vendor"/g' > $DIST/.cargo/config
|
||||||
|
# Move vendor into dist tarball directory
|
||||||
|
mv vendor "$DIST"
|
115
build.rs
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
use std::{env::var, path::Path};
|
||||||
|
|
||||||
|
enum BuildEnv {
|
||||||
|
Meson,
|
||||||
|
Direct,
|
||||||
|
}
|
||||||
|
struct BuildContext {
|
||||||
|
profile: String,
|
||||||
|
application_id: String,
|
||||||
|
pkgdata_dir: String,
|
||||||
|
locale_dir: String,
|
||||||
|
gettext_package: String,
|
||||||
|
version: String,
|
||||||
|
config_init_code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_changed_prefix(key: &str) {
|
||||||
|
println!("cargo::rerun-if-env-changed=PAKET_{}", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_changed(key: &str) {
|
||||||
|
println!("cargo::rerun-if-env-changed={}", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_get_prefix(key: &str) -> String {
|
||||||
|
env_get(format!("PAKET_{}", key).as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_get(key: &str) -> String {
|
||||||
|
var(key).expect(format!("{} not found, but required by PAKET_BUILD_BY", key).as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let env = var("PAKET_BUILY_BY")
|
||||||
|
.map_or(Some(BuildEnv::Direct), |string| {
|
||||||
|
if string.eq_ignore_ascii_case("meson") {
|
||||||
|
Some(BuildEnv::Meson)
|
||||||
|
} else if string.eq_ignore_ascii_case("direct") || string.eq_ignore_ascii_case("cargo")
|
||||||
|
{
|
||||||
|
Some(BuildEnv::Direct)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect("PAKET_BUILT_BY doesn't contain a valid build target");
|
||||||
|
// shouldn't happen theortically
|
||||||
|
env_changed_prefix("BUILT_BY");
|
||||||
|
|
||||||
|
println!("cargo::rerun-if-changed=build.rs");
|
||||||
|
|
||||||
|
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||||
|
let context = match env {
|
||||||
|
BuildEnv::Meson => {
|
||||||
|
env_changed("PROFILE");
|
||||||
|
env_changed_prefix("APPLICATION_ID");
|
||||||
|
env_changed_prefix("PKGDATADIR");
|
||||||
|
env_changed_prefix("LOCALEDIR");
|
||||||
|
env_changed_prefix("GETTEXT_PACKAGE");
|
||||||
|
env_changed_prefix("VERSION");
|
||||||
|
|
||||||
|
BuildContext {
|
||||||
|
profile: env_get("PROFILE"),
|
||||||
|
application_id: env_get_prefix("APPLICATION_ID"),
|
||||||
|
pkgdata_dir: env_get_prefix("PKGDATADIR"),
|
||||||
|
locale_dir: env_get_prefix("LOCALEDIR"),
|
||||||
|
gettext_package: env_get_prefix("GETTEXT_PACKAGE"),
|
||||||
|
version: env_get_prefix("VERSION"),
|
||||||
|
config_init_code: "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BuildEnv::Direct => {
|
||||||
|
println!("cargo::rerun-if-changed=assets");
|
||||||
|
|
||||||
|
glib_build_tools::compile_resources(&["."], "paket.gresource.xml", "paket.gresource");
|
||||||
|
|
||||||
|
BuildContext {
|
||||||
|
profile: env_get("PROFILE"),
|
||||||
|
application_id: "de.j4ne.Paket".to_string(),
|
||||||
|
version: env_get("CARGO_PKG_VERSION"),
|
||||||
|
pkgdata_dir: format!("{}/pkgdata", env_get("OUT_DIR")),
|
||||||
|
locale_dir: format!("{}/locale", env_get("OUT_DIR")),
|
||||||
|
gettext_package: "paket".to_string(),
|
||||||
|
config_init_code: format!(
|
||||||
|
"gtk::gio::resources_register_include!(\"paket.gresource\").unwrap();"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let dest_path = Path::new(&out_dir).join("config.rs");
|
||||||
|
std::fs::write(
|
||||||
|
&dest_path,
|
||||||
|
format!(
|
||||||
|
"
|
||||||
|
pub static VERSION: &str = \"{version}\";
|
||||||
|
pub static GETTEXT_PACKAGE: &str = \"{gettext_package}\";
|
||||||
|
pub static LOCALEDIR: &str = \"{locale_dir}\";
|
||||||
|
pub static PKGDATADIR: &str = \"{pkgdata_dir}\";
|
||||||
|
pub static APPLICATION_ID: &str = \"{application_id}\";
|
||||||
|
pub static PROFILE: &str = \"{profile}\";
|
||||||
|
|
||||||
|
pub fn init() {{
|
||||||
|
{code_init}
|
||||||
|
}}",
|
||||||
|
version = context.version,
|
||||||
|
gettext_package = context.gettext_package,
|
||||||
|
locale_dir = context.locale_dir,
|
||||||
|
pkgdata_dir = context.pkgdata_dir,
|
||||||
|
application_id = context.application_id,
|
||||||
|
profile = context.profile,
|
||||||
|
code_init = context.config_init_code,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
53
default.nix
|
@ -1,33 +1,32 @@
|
||||||
{ pkgs ? import <nixpkgs> {} }: let
|
{ pkgs ? import <nixpkgs> {} }: let
|
||||||
package = {
|
package = {
|
||||||
rustPlatform,
|
|
||||||
nix-gitignore,
|
|
||||||
pkg-config,
|
|
||||||
openssl,
|
|
||||||
glib,
|
|
||||||
gdk-pixbuf,
|
|
||||||
graphene,
|
|
||||||
cairo,
|
cairo,
|
||||||
pango,
|
gdk-pixbuf,
|
||||||
gtk4,
|
|
||||||
libsoup_3,
|
|
||||||
libadwaita,
|
|
||||||
webkitgtk_6_0,
|
|
||||||
libseccomp,
|
|
||||||
wrapGAppsHook4,
|
|
||||||
glib-networking,
|
glib-networking,
|
||||||
|
glib,
|
||||||
|
graphene,
|
||||||
|
gst_all_1,
|
||||||
|
gtk4,
|
||||||
|
libadwaita,
|
||||||
|
libcamera,
|
||||||
|
libseccomp,
|
||||||
|
libsoup_3,
|
||||||
|
nix-gitignore,
|
||||||
|
openssl,
|
||||||
|
pango,
|
||||||
|
pipewire,
|
||||||
|
pkg-config,
|
||||||
|
rustPlatform,
|
||||||
|
webkitgtk_6_0,
|
||||||
|
wrapGAppsHook4,
|
||||||
}: rustPlatform.buildRustPackage {
|
}: rustPlatform.buildRustPackage {
|
||||||
pname = "paket";
|
pname = "paket";
|
||||||
version = "unstable-2024-09-28";
|
version = "unstable-2025-01-22";
|
||||||
|
|
||||||
src = nix-gitignore.gitignoreSource [] ./.;
|
src = nix-gitignore.gitignoreSource [] ./.;
|
||||||
|
|
||||||
cargoLock = {
|
cargoLock = {
|
||||||
lockFile = ./Cargo.lock;
|
lockFile = ./Cargo.lock;
|
||||||
outputHashes = {
|
|
||||||
"relm4-0.9.0" = "sha256-iFxi2ZWdzWtui85IOfMIfyuPDbQO69u5VLk0a9ebatM=";
|
|
||||||
"relm4-icons-0.9.0" = "sha256-UUo1wIvJL2MryUFICnmVq6LoPuNaZ9nKcNGCCF8cx+k=";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
|
@ -37,6 +36,13 @@
|
||||||
wrapGAppsHook4
|
wrapGAppsHook4
|
||||||
];
|
];
|
||||||
|
|
||||||
|
preFixup = ''
|
||||||
|
gappsWrapperArgs+=(
|
||||||
|
# vp8enc preset
|
||||||
|
--prefix GST_PRESET_PATH : "${gst_all_1.gst-plugins-good}/share/gstreamer-1.0/presets"
|
||||||
|
)
|
||||||
|
'';
|
||||||
|
|
||||||
buildInputs = [
|
buildInputs = [
|
||||||
# Building
|
# Building
|
||||||
openssl
|
openssl
|
||||||
|
@ -48,6 +54,15 @@
|
||||||
libadwaita
|
libadwaita
|
||||||
webkitgtk_6_0 # for JSC
|
webkitgtk_6_0 # for JSC
|
||||||
|
|
||||||
|
# scanner
|
||||||
|
libcamera # for the gstreamer plugin
|
||||||
|
pipewire # for device provider
|
||||||
|
gst_all_1.gst-plugins-bad
|
||||||
|
gst_all_1.gst-plugins-base
|
||||||
|
gst_all_1.gst-plugins-good
|
||||||
|
gst_all_1.gst-plugins-rs # for gtk4paintablesink
|
||||||
|
gst_all_1.gstreamer
|
||||||
|
|
||||||
# Linking
|
# Linking
|
||||||
libseccomp
|
libseccomp
|
||||||
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
app_id = "de.j4ne.Paket"
|
|
||||||
|
|
||||||
icons = ["plus", "minus", "package-x-generic", "mail", "loupe-large", "person", "copy", "qr-code-scanner"]
|
|
|
@ -6,32 +6,32 @@ license.workspace = true
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aes-gcm = { version = "0.10.3", optional = true }
|
aes-gcm = { workspace = true, optional = true }
|
||||||
ed25519-dalek = { version = "2.1.0", optional = true }
|
ed25519-dalek = { workspace = true, optional = true }
|
||||||
hmac = { version = "0.12.1", optional = true }
|
hmac = { workspace = true, optional = true }
|
||||||
num_enum = { version = "0.7", optional = true }
|
num_enum = { workspace = true, optional = true }
|
||||||
|
|
||||||
# TODO: Consolidate?
|
# TODO: Consolidate?
|
||||||
rand = "0.8.5"
|
rand = { workspace = true }
|
||||||
random-string = "1.1.0"
|
random-string = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
secrecy = { workspace = true}
|
secrecy = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
serde_repr = { version = "0.1.18", optional = true }
|
serde_repr = { workspace = true, optional = true }
|
||||||
serde_ignored = "0.1"
|
serde_ignored = { workspace = true }
|
||||||
url = "2.5.0"
|
url = { workspace = true }
|
||||||
base64 = "0.22"
|
base64 = { workspace = true }
|
||||||
|
|
||||||
# TODO: consider splitting login.rs refresh_token and authorization_token
|
# TODO: consider splitting login.rs refresh_token and authorization_token
|
||||||
# (sha2 and urlencoding only used with authorization_token)
|
# (sha2 and urlencoding only used with authorization_token)
|
||||||
# sha2 also used in briefankuendigung and packstation_register_regtoken
|
# sha2 also used in briefankuendigung and packstation_register_regtoken
|
||||||
sha2 = "0.10.8"
|
sha2 = { workspace = true }
|
||||||
urlencoding = "2.1.3"
|
urlencoding = { workspace = true }
|
||||||
uuid = { workspace = true, features = ["serde"], optional = true }
|
uuid = { workspace = true, features = ["serde"], optional = true }
|
||||||
|
|
||||||
serde_newtype = "0.1.1"
|
serde_newtype = { workspace = true }
|
||||||
thiserror = "1.0.56"
|
thiserror = { workspace = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [
|
default = [
|
||||||
|
@ -42,6 +42,8 @@ default = [
|
||||||
|
|
||||||
unstable = []
|
unstable = []
|
||||||
|
|
||||||
|
private_tests = []
|
||||||
|
|
||||||
advices = [
|
advices = [
|
||||||
#"dep:sha2",
|
#"dep:sha2",
|
||||||
"dep:uuid",
|
"dep:uuid",
|
||||||
|
@ -72,7 +74,6 @@ locker_register_base = [
|
||||||
"locker_base",
|
"locker_base",
|
||||||
"dep:hmac",
|
"dep:hmac",
|
||||||
#"dep:sha2",
|
#"dep:sha2",
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
locker_register_regtoken = [
|
locker_register_regtoken = [
|
||||||
|
|
|
@ -22,6 +22,7 @@ It's recommended that consumers of this crate are always tracking the latest ver
|
||||||
In the examples error-handling is ignored for simplicity. You don’t want to do that.
|
In the examples error-handling is ignored for simplicity. You don’t want to do that.
|
||||||
|
|
||||||
### Getting mail notifications (Briefankündigung)
|
### Getting mail notifications (Briefankündigung)
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Requires a logged-in user.
|
// Requires a logged-in user.
|
||||||
let token: libpaket::login::DHLIdToken;
|
let token: libpaket::login::DHLIdToken;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
mod www;
|
|
||||||
mod briefankuendigung;
|
mod briefankuendigung;
|
||||||
|
mod www;
|
||||||
|
|
||||||
|
pub use briefankuendigung::*;
|
||||||
pub use www::*;
|
pub use www::*;
|
||||||
pub use briefankuendigung::*;
|
|
|
@ -32,27 +32,28 @@ newtype! {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AdvicesResponse {
|
pub struct AdvicesResponse {
|
||||||
// access_token_url, basic_auth, grant_token is null if no advices are available
|
// access_token_url, basic_auth, grant_token is null if no advices are available
|
||||||
|
pub(super) delay_text: Option<String>,
|
||||||
pub(super) access_token_url: Option<AdviceAccessTokenUrl>,
|
pub(super) access_token_url: Option<AdviceAccessTokenUrl>,
|
||||||
pub(super) basic_auth: Option<String>,
|
pub(super) basic_auth: Option<String>,
|
||||||
current_advice: Option<AdvicesList>,
|
current_advice: Vec<AdvicesList>,
|
||||||
pub(super) grant_token: Option<String>,
|
pub(super) grant_token: Option<String>,
|
||||||
old_advices: Vec<AdvicesList>,
|
old_advices: Vec<AdvicesList>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AdvicesResponse {
|
impl AdvicesResponse {
|
||||||
pub fn has_any_advices(&self) -> bool {
|
pub fn has_any_advices(&self) -> bool {
|
||||||
self.has_current_advice() || self.has_old_advices()
|
self.has_current_advices() || self.has_old_advices()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_current_advice(&self) -> bool {
|
pub fn has_current_advices(&self) -> bool {
|
||||||
self.current_advice.is_some()
|
self.current_advice.len() > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_old_advices(&self) -> bool {
|
pub fn has_old_advices(&self) -> bool {
|
||||||
self.old_advices.len() > 0
|
self.old_advices.len() > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_current_advice(&self) -> Option<&AdvicesList> {
|
pub fn get_current_advices(&self) -> &Vec<AdvicesList> {
|
||||||
self.current_advice.as_ref()
|
self.current_advice.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,21 +67,33 @@ fn endpoint_advices() -> url::Url {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::www::WebClient {
|
impl crate::www::WebClient {
|
||||||
// FIXME: more error parsing
|
// FIXME: more error parsing
|
||||||
pub async fn advices(&self, dhli: &crate::login::DHLIdToken) -> crate::LibraryResult<AdvicesResponse> {
|
pub async fn advices(
|
||||||
let res = request!(self.web_client, endpoint_advices,
|
&self,
|
||||||
header("Cookie", CookieHeaderValueBuilder::new().add_dhli(dhli).add_dhlcs(dhli).build_string())
|
dhli: &crate::login::DHLIdToken,
|
||||||
|
) -> crate::LibraryResult<AdvicesResponse> {
|
||||||
|
let res = request!(
|
||||||
|
self.web_client,
|
||||||
|
endpoint_advices,
|
||||||
|
header(
|
||||||
|
"Cookie",
|
||||||
|
CookieHeaderValueBuilder::new()
|
||||||
|
.add_dhli(dhli)
|
||||||
|
.add_dhlcs(dhli)
|
||||||
|
.build_string()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
let res = parse_json_response!(res, AdvicesResponse);
|
let res = parse_json_response!(res, AdvicesResponse);
|
||||||
|
|
||||||
if res.access_token_url.is_some() {
|
if res.access_token_url.is_some() {
|
||||||
if res.access_token_url.as_ref().unwrap().as_str() != crate::advices::endpoint_access_tokens() {
|
if res.access_token_url.as_ref().unwrap().as_str()
|
||||||
|
!= crate::advices::endpoint_access_tokens()
|
||||||
|
{
|
||||||
return Err(crate::LibraryError::APIChange);
|
return Err(crate::LibraryError::APIChange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
pub fn app_version() -> &'static str {
|
pub fn app_version() -> &'static str {
|
||||||
"9.9.1.95 (a72fec7be)"
|
"9.9.1.95 (a72fec7be)"
|
||||||
}
|
}
|
||||||
|
@ -8,7 +7,11 @@ pub fn linux_android_version() -> &'static str {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn webview_user_agent() -> String {
|
pub fn webview_user_agent() -> String {
|
||||||
format!("{} [DHL Paket Android App/{}]", web_user_agent(), app_version())
|
format!(
|
||||||
|
"{} [DHL Paket Android App/{}]",
|
||||||
|
web_user_agent(),
|
||||||
|
app_version()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn device_string() -> &'static str {
|
pub fn device_string() -> &'static str {
|
||||||
|
@ -26,4 +29,4 @@ pub fn okhttp_user_agent() -> String {
|
||||||
pub fn web_user_agent() -> String {
|
pub fn web_user_agent() -> String {
|
||||||
format!("Mozilla/5.0 ({}; {}; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/117.0.0.0 Mobile Safari/537.36",
|
format!("Mozilla/5.0 ({}; {}; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/117.0.0.0 Mobile Safari/537.36",
|
||||||
linux_android_version(), device_string())
|
linux_android_version(), device_string())
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,9 @@ pub mod tracking;
|
||||||
#[cfg(feature = "locker_ble")]
|
#[cfg(feature = "locker_ble")]
|
||||||
pub use locker::LockerClient;
|
pub use locker::LockerClient;
|
||||||
|
|
||||||
/*#[cfg(test)]
|
#[cfg(feature = "private_tests")]
|
||||||
pub(crate) mod private;*/
|
#[cfg(test)]
|
||||||
|
pub(crate) mod private;
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
@ -64,7 +65,7 @@ impl From<reqwest::Error> for LibraryError {
|
||||||
impl From<common::APIError> for LibraryError {
|
impl From<common::APIError> for LibraryError {
|
||||||
fn from(value: common::APIError) -> Self {
|
fn from(value: common::APIError) -> Self {
|
||||||
match value.error {
|
match value.error {
|
||||||
common::APIErrorType::InvalidGrant => Self::Unauthorized
|
common::APIErrorType::InvalidGrant => Self::Unauthorized,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
use super::utils::{PrimitiveBuilder, PrimitiveReader};
|
||||||
use num_enum::TryFromPrimitive;
|
use num_enum::TryFromPrimitive;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use super::utils::{PrimitiveBuilder, PrimitiveReader};
|
|
||||||
|
|
||||||
use crate::{LibraryError, LibraryResult};
|
use crate::{LibraryError, LibraryResult};
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ const REQUESTS: [CommandType; 10] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
struct ResponseInitSession {
|
struct ResponseInitSession {
|
||||||
raw_command: Command
|
raw_command: Command,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Response {
|
enum Response {
|
||||||
|
@ -57,7 +57,10 @@ impl TryFrom<Command> for Response {
|
||||||
|
|
||||||
fn try_from(value: Command) -> Result<Self, Self::Error> {
|
fn try_from(value: Command) -> Result<Self, Self::Error> {
|
||||||
if REQUESTS.binary_search(&value.r#type).is_ok() {
|
if REQUESTS.binary_search(&value.r#type).is_ok() {
|
||||||
return Err(LibraryError::InvalidArgument("TryFrom<Command> for Response: CommandType is a Request (expected Response)".to_string()))
|
return Err(LibraryError::InvalidArgument(
|
||||||
|
"TryFrom<Command> for Response: CommandType is a Request (expected Response)"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
@ -74,12 +77,11 @@ impl TryFrom<Command> for Response {
|
||||||
|
|
||||||
// Checksum function
|
// Checksum function
|
||||||
|
|
||||||
|
|
||||||
pub struct Command {
|
pub struct Command {
|
||||||
r#type: CommandType,
|
r#type: CommandType,
|
||||||
payload: Vec<u8>,
|
payload: Vec<u8>,
|
||||||
init_vector: Vec<u8>,
|
init_vector: Vec<u8>,
|
||||||
metadata: Vec<u8>
|
metadata: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
|
@ -105,21 +107,26 @@ impl Command {
|
||||||
|
|
||||||
pub fn parse(bin: Vec<u8>) -> LibraryResult<Self> {
|
pub fn parse(bin: Vec<u8>) -> LibraryResult<Self> {
|
||||||
// command byte + message length + 3 empty message arguments (array with size 0) + 2 checksum bytes
|
// command byte + message length + 3 empty message arguments (array with size 0) + 2 checksum bytes
|
||||||
let to_few_bytes = LibraryError::InvalidArgument("Command::parse: Invalid vec.len() (to few bytes)".to_string());
|
let to_few_bytes = LibraryError::InvalidArgument(
|
||||||
|
"Command::parse: Invalid vec.len() (to few bytes)".to_string(),
|
||||||
|
);
|
||||||
if bin.len() < 1 + 4 + (4 * 3) + 2 {
|
if bin.len() < 1 + 4 + (4 * 3) + 2 {
|
||||||
return Err(to_few_bytes)
|
return Err(to_few_bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let checksum = {
|
let checksum = {
|
||||||
PrimitiveReader {
|
PrimitiveReader {
|
||||||
offset: bin.len() - 2,
|
offset: bin.len() - 2,
|
||||||
vec: bin.as_slice(),
|
vec: bin.as_slice(),
|
||||||
}.read_u16()
|
}
|
||||||
|
.read_u16()
|
||||||
};
|
};
|
||||||
|
|
||||||
if checksum != Self::checksum(&bin[0..bin.len() - 2]) {
|
if checksum != Self::checksum(&bin[0..bin.len() - 2]) {
|
||||||
return Err(LibraryError::InvalidArgument("Command::parse: Invalid checksum".to_string()));
|
return Err(LibraryError::InvalidArgument(
|
||||||
|
"Command::parse: Invalid checksum".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,14 +137,16 @@ impl Command {
|
||||||
|
|
||||||
let r#type = reader.read_u8();
|
let r#type = reader.read_u8();
|
||||||
let Ok(r#type) = CommandType::try_from_primitive(r#type) else {
|
let Ok(r#type) = CommandType::try_from_primitive(r#type) else {
|
||||||
return Err(LibraryError::DecodeError("unable determine CommandType".to_string()));
|
return Err(LibraryError::DecodeError(
|
||||||
|
"unable determine CommandType".to_string(),
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
let size_of_message = reader.read_u32() as usize;
|
let size_of_message = reader.read_u32() as usize;
|
||||||
if reader.left_to_process() < size_of_message {
|
if reader.left_to_process() < size_of_message {
|
||||||
return Err(to_few_bytes);
|
return Err(to_few_bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
let payload: Vec<u8> = reader.read_arr_from_len();
|
let payload: Vec<u8> = reader.read_arr_from_len();
|
||||||
let init_vector = reader.read_arr_from_len();
|
let init_vector = reader.read_arr_from_len();
|
||||||
let metadata = reader.read_arr_from_len();
|
let metadata = reader.read_arr_from_len();
|
||||||
|
@ -152,21 +161,21 @@ impl Command {
|
||||||
|
|
||||||
pub fn finish(self) -> Vec<u8> {
|
pub fn finish(self) -> Vec<u8> {
|
||||||
let vec1 = PrimitiveBuilder::new()
|
let vec1 = PrimitiveBuilder::new()
|
||||||
.write_array_with_len(&self.payload)
|
.write_array_with_len(&self.payload)
|
||||||
.write_array_with_len(&self.init_vector)
|
.write_array_with_len(&self.init_vector)
|
||||||
.write_array_with_len(&self.metadata)
|
.write_array_with_len(&self.metadata)
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
let vec2 = PrimitiveBuilder::new()
|
let vec2 = PrimitiveBuilder::new()
|
||||||
.write_u8(self.r#type as u8)
|
.write_u8(self.r#type as u8)
|
||||||
.write_u32(vec1.len() as u32 + 2)
|
.write_u32(vec1.len() as u32 + 2)
|
||||||
.write_array(&vec1)
|
.write_array(&vec1)
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
PrimitiveBuilder::new()
|
PrimitiveBuilder::new()
|
||||||
.write_array(&vec2)
|
.write_array(&vec2)
|
||||||
.write_u16(Self::checksum(vec2.as_slice()))
|
.write_u16(Self::checksum(vec2.as_slice()))
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_only_decimal(str: &String) {
|
fn assert_only_decimal(str: &String) {
|
||||||
|
@ -183,15 +192,15 @@ impl Command {
|
||||||
|
|
||||||
// de.dhl.paket does some kinky string conversion
|
// de.dhl.paket does some kinky string conversion
|
||||||
|
|
||||||
let vec = Vec::<u8>::new().into_iter()
|
let vec = Vec::<u8>::new()
|
||||||
.chain(fields.0.to_be_bytes().into_iter())
|
.into_iter()
|
||||||
.chain(fields.1.to_be_bytes().into_iter())
|
.chain(fields.0.to_be_bytes().into_iter())
|
||||||
.chain(fields.2.to_be_bytes().into_iter())
|
.chain(fields.1.to_be_bytes().into_iter())
|
||||||
.chain(fields.3.to_vec().into_iter())
|
.chain(fields.2.to_be_bytes().into_iter())
|
||||||
.collect::<Vec<u8>>();
|
.chain(fields.3.to_vec().into_iter())
|
||||||
|
.collect::<Vec<u8>>();
|
||||||
|
|
||||||
let new_vec = PrimitiveBuilder::new()
|
let new_vec = PrimitiveBuilder::new().write_array_with_len(&vec).finish();
|
||||||
.write_array_with_len(&vec).finish();
|
|
||||||
|
|
||||||
Ok(Command {
|
Ok(Command {
|
||||||
r#type: CommandType::InitSessionRequest,
|
r#type: CommandType::InitSessionRequest,
|
||||||
|
@ -202,9 +211,12 @@ impl Command {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*#[cfg(test)]
|
#[cfg(feature = "private_tests")]
|
||||||
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::private::sample_packstation::{INIT_SESSION_REQUEST, INIT_SESSION_RESPONSE, INIT_SESSION_SERVICE_UUID};
|
use crate::private::sample_packstation::{
|
||||||
|
INIT_SESSION_REQUEST, INIT_SESSION_RESPONSE, INIT_SESSION_SERVICE_UUID,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{Command, CommandType};
|
use super::{Command, CommandType};
|
||||||
|
|
||||||
|
@ -215,7 +227,10 @@ mod test {
|
||||||
corrected.push(b as u8);
|
corrected.push(b as u8);
|
||||||
}
|
}
|
||||||
|
|
||||||
let command = Command::init_session_request(uuid::uuid!(INIT_SESSION_SERVICE_UUID).try_into().unwrap()).unwrap();
|
let command = Command::init_session_request(
|
||||||
|
uuid::uuid!(INIT_SESSION_SERVICE_UUID).try_into().unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(command.finish().as_slice(), corrected.as_slice())
|
assert_eq!(command.finish().as_slice(), corrected.as_slice())
|
||||||
}
|
}
|
||||||
|
@ -229,5 +244,4 @@ mod test {
|
||||||
|
|
||||||
let command = Command::parse(corrected);
|
let command = Command::parse(corrected);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}*/
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ impl Seed {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let mut bytes: Vec<u8> = vec![0; 32];
|
let mut bytes: Vec<u8> = vec![0; 32];
|
||||||
rng.fill_bytes(bytes.as_mut_slice());
|
rng.fill_bytes(bytes.as_mut_slice());
|
||||||
Seed (bytes)
|
Seed(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_bytes(&self) -> &[u8; 32] {
|
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||||
|
@ -61,6 +61,15 @@ impl CustomerKeySeed {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_partial(postnumber: &String, seed: Vec<u8>, uuid: &Uuid) -> Self {
|
||||||
|
CustomerKeySeed {
|
||||||
|
postnumber: postnumber.clone(),
|
||||||
|
seed: SecretBox::new(Box::new(Seed::from(seed))),
|
||||||
|
uuid: uuid.clone(),
|
||||||
|
device_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn from(postnumber: &String, seed: Vec<u8>, uuid: &Uuid, device_id: String) -> Self {
|
pub fn from(postnumber: &String, seed: Vec<u8>, uuid: &Uuid, device_id: String) -> Self {
|
||||||
CustomerKeySeed {
|
CustomerKeySeed {
|
||||||
postnumber: postnumber.clone(),
|
postnumber: postnumber.clone(),
|
||||||
|
|
|
@ -68,7 +68,6 @@ pub enum APIRegisterError {
|
||||||
UserCanNotYetRegister = 21,
|
UserCanNotYetRegister = 21,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(super) struct RegistrationCommonDevice {
|
pub(super) struct RegistrationCommonDevice {
|
||||||
|
|
|
@ -4,8 +4,11 @@ use hmac::{digest::CtOutput, Mac, SimpleHmac};
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
|
|
||||||
use crate::common::APIResult;
|
use crate::common::APIResult;
|
||||||
use crate::locker::register::{DeviceMetadata, DeviceRegistrationResponse, RegToken, RegistrationCommonDevice, RegistrationOption, RegistrationPayload};
|
|
||||||
use crate::locker::crypto::CustomerKeySeed;
|
use crate::locker::crypto::CustomerKeySeed;
|
||||||
|
use crate::locker::register::{
|
||||||
|
DeviceMetadata, DeviceRegistrationResponse, RegToken, RegistrationCommonDevice,
|
||||||
|
RegistrationOption, RegistrationPayload,
|
||||||
|
};
|
||||||
use crate::login::DHLIdToken;
|
use crate::login::DHLIdToken;
|
||||||
use crate::LibraryResult;
|
use crate::LibraryResult;
|
||||||
|
|
||||||
|
@ -28,16 +31,18 @@ impl RegistrationRegToken {
|
||||||
reg_token: &RegToken,
|
reg_token: &RegToken,
|
||||||
device_metadata: DeviceMetadata,
|
device_metadata: DeviceMetadata,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let challange_bin = general_purpose::STANDARD.decode(begin_registration.challenge.as_str()).unwrap();
|
let challange_bin = general_purpose::STANDARD
|
||||||
|
.decode(begin_registration.challenge.as_str())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let mut mac = reg_token.hmac();
|
let mut mac = reg_token.hmac();
|
||||||
mac.update(challange_bin.as_slice());
|
mac.update(challange_bin.as_slice());
|
||||||
|
|
||||||
let verifier_bin: CtOutput<SimpleHmac<Sha256>> = mac.finalize();
|
let verifier_bin: CtOutput<SimpleHmac<Sha256>> = mac.finalize();
|
||||||
let verifier_bin = verifier_bin.into_bytes();
|
let verifier_bin = verifier_bin.into_bytes();
|
||||||
let verifier = general_purpose::STANDARD.encode(&verifier_bin);
|
let verifier = general_purpose::STANDARD.encode(&verifier_bin);
|
||||||
let verifier_signature = customer_key_seed.sign(&verifier_bin);
|
let verifier_signature = customer_key_seed.sign(&verifier_bin);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
challenge: begin_registration.challenge,
|
challenge: begin_registration.challenge,
|
||||||
customer_password: reg_token.customer_password(),
|
customer_password: reg_token.customer_password(),
|
||||||
|
@ -52,7 +57,6 @@ impl RegistrationRegToken {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::StammdatenClient {
|
impl crate::StammdatenClient {
|
||||||
|
|
||||||
pub async fn register_by_regtoken(
|
pub async fn register_by_regtoken(
|
||||||
&self,
|
&self,
|
||||||
dhli: &DHLIdToken,
|
dhli: &DHLIdToken,
|
||||||
|
@ -61,7 +65,6 @@ impl crate::StammdatenClient {
|
||||||
device_metadata: DeviceMetadata,
|
device_metadata: DeviceMetadata,
|
||||||
reg_token: &crate::locker::register::RegToken,
|
reg_token: &crate::locker::register::RegToken,
|
||||||
) -> LibraryResult<DeviceRegistrationResponse> {
|
) -> LibraryResult<DeviceRegistrationResponse> {
|
||||||
|
|
||||||
let mut valid = false;
|
let mut valid = false;
|
||||||
for option in registration_payload.registration_options.iter() {
|
for option in registration_payload.registration_options.iter() {
|
||||||
if *option == RegistrationOption::ByRegToken {
|
if *option == RegistrationOption::ByRegToken {
|
||||||
|
@ -74,7 +77,7 @@ impl crate::StammdatenClient {
|
||||||
customer_key_seed,
|
customer_key_seed,
|
||||||
registration_payload,
|
registration_payload,
|
||||||
reg_token,
|
reg_token,
|
||||||
device_metadata
|
device_metadata,
|
||||||
);
|
);
|
||||||
|
|
||||||
let body = serde_json::to_string(&body).unwrap();
|
let body = serde_json::to_string(&body).unwrap();
|
||||||
|
@ -103,16 +106,15 @@ impl crate::StammdatenClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn endpoint_devices_register_by_regtoken() -> &'static str {
|
fn endpoint_devices_register_by_regtoken() -> &'static str {
|
||||||
"https://www.dhl.de/int-stammdaten/public/devices/registerByRegToken"
|
"https://www.dhl.de/int-stammdaten/public/devices/registerByRegToken"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "private_tests")]
|
||||||
/*#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod registration_api {
|
mod registration_api {
|
||||||
use std::str::FromStr as _;
|
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
|
use std::str::FromStr as _;
|
||||||
|
|
||||||
use crate::private::init as private;
|
use crate::private::init as private;
|
||||||
|
|
||||||
|
@ -120,33 +122,39 @@ mod registration_api {
|
||||||
fn regtoken_customer_password() {
|
fn regtoken_customer_password() {
|
||||||
let regtoken = super::RegToken::parse_from_qrcode_uri(&private::IN_REGTOKEN).unwrap();
|
let regtoken = super::RegToken::parse_from_qrcode_uri(&private::IN_REGTOKEN).unwrap();
|
||||||
assert!(regtoken.customer_password() == private::OUT_CUSTOMER_PASSWORD);
|
assert!(regtoken.customer_password() == private::OUT_CUSTOMER_PASSWORD);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn register_via_regtoken(){
|
fn register_via_regtoken() {
|
||||||
let regtoken = super::RegToken::parse_from_qrcode_uri(&private::IN_REGTOKEN).unwrap();
|
let regtoken = super::RegToken::parse_from_qrcode_uri(&private::IN_REGTOKEN).unwrap();
|
||||||
|
|
||||||
let customer_key_seed = crate::locker::crypto::CustomerKeySeed::from(&private::IN_POSTNUMBER.to_string(),
|
let customer_key_seed = crate::locker::crypto::CustomerKeySeed::from_partial(
|
||||||
base64::engine::general_purpose::STANDARD.decode(&private::IN_SEED_BASE64).unwrap(),
|
&private::IN_POSTNUMBER.to_string(),
|
||||||
&uuid::Uuid::from_str(&private::IN_KEY_ID).unwrap());
|
base64::engine::general_purpose::STANDARD
|
||||||
|
.decode(&private::IN_SEED_BASE64)
|
||||||
|
.unwrap(),
|
||||||
|
&uuid::Uuid::from_str(&private::IN_KEY_ID).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
let registration_payload = super::RegistrationPayload {
|
let registration_payload = super::RegistrationPayload {
|
||||||
challenge: private::IN_CHALLENGE.to_string(),
|
challenge: private::IN_CHALLENGE.to_string(),
|
||||||
registration_options: Vec::new(),
|
registration_options: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let reg_reg_token = super::RegistrationRegToken::new(
|
let reg_reg_token = super::RegistrationRegToken::new(
|
||||||
&customer_key_seed,
|
&customer_key_seed,
|
||||||
registration_payload,
|
registration_payload,
|
||||||
®token,
|
®token,
|
||||||
super::DeviceMetadata { manufacturer_model: "".to_string(), manufacturer_name: "".to_string(), manufacturer_operating_system: "".to_string(), name: "".to_string() }
|
super::DeviceMetadata {
|
||||||
|
manufacturer_model: "".to_string(),
|
||||||
|
manufacturer_name: "".to_string(),
|
||||||
|
manufacturer_operating_system: "".to_string(),
|
||||||
|
name: "".to_string(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(reg_reg_token.verifier == private::OUT_VERIFIER);
|
assert!(reg_reg_token.verifier == private::OUT_VERIFIER);
|
||||||
assert!(reg_reg_token.verifier_signature == private::OUT_VERIFIERSIGNATURE);
|
assert!(reg_reg_token.verifier_signature == private::OUT_VERIFIERSIGNATURE);
|
||||||
assert!(reg_reg_token.device.public_key == private::OUT_DEVICE_PUBKEY);
|
assert!(reg_reg_token.device.public_key == private::OUT_DEVICE_PUBKEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
*/
|
|
|
@ -2,7 +2,27 @@ use base64::{engine::general_purpose, Engine as _};
|
||||||
use hmac::{Mac, SimpleHmac};
|
use hmac::{Mac, SimpleHmac};
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
|
|
||||||
use crate::{LibraryResult, LibraryError};
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::LibraryError;
|
||||||
|
|
||||||
|
#[derive(Error, Debug, PartialEq)]
|
||||||
|
pub enum RegTokenDecodeError {
|
||||||
|
#[error("RegTokenUri from QR-Code too short")]
|
||||||
|
LengthTooShort,
|
||||||
|
#[error("RegTokenUri from QR-Code has invalid magic")]
|
||||||
|
InvalidMagic,
|
||||||
|
#[error("RegTokenUri from QR-Code not decodeable (base64)")]
|
||||||
|
NotBase64,
|
||||||
|
#[error("RegToken longer than expected")]
|
||||||
|
LengthTooLong,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RegTokenDecodeError> for LibraryError {
|
||||||
|
fn from(value: RegTokenDecodeError) -> Self {
|
||||||
|
Self::DecodeError(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct RegToken(Vec<u8>);
|
pub struct RegToken(Vec<u8>);
|
||||||
impl secrecy::zeroize::Zeroize for RegToken {
|
impl secrecy::zeroize::Zeroize for RegToken {
|
||||||
|
@ -12,23 +32,23 @@ impl secrecy::zeroize::Zeroize for RegToken {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegToken {
|
impl RegToken {
|
||||||
pub fn parse_from_qrcode_uri(uri: &str) -> LibraryResult<RegToken> {
|
pub fn parse_from_qrcode_uri(uri: &str) -> Result<RegToken, RegTokenDecodeError> {
|
||||||
if uri.len() <= 23 {
|
if uri.len() <= 23 {
|
||||||
return Err(LibraryError::DecodeError("RegTokenUri too short".to_string()));
|
return Err(RegTokenDecodeError::LengthTooShort);
|
||||||
}
|
}
|
||||||
if !uri.starts_with("urn:dhl.de:regtoken:v1:") {
|
if !uri.starts_with("urn:dhl.de:regtoken:v1:") {
|
||||||
return Err(LibraryError::DecodeError("RegTokenUri has invalid magic".to_string()));
|
return Err(RegTokenDecodeError::InvalidMagic);
|
||||||
}
|
}
|
||||||
|
|
||||||
let token = &uri[23..];
|
let token = &uri[23..];
|
||||||
let token = general_purpose::STANDARD.decode(token);
|
let token = general_purpose::STANDARD.decode(token);
|
||||||
|
|
||||||
let Ok(mut token) = token else {
|
let Ok(mut token) = token else {
|
||||||
return Err(LibraryError::DecodeError("RegTokenUri not decodeable (base64)".to_string()));
|
return Err(RegTokenDecodeError::NotBase64);
|
||||||
};
|
};
|
||||||
|
|
||||||
if token.len() > 64 {
|
if token.len() > 64 {
|
||||||
return Err(LibraryError::DecodeError("RegToken longer than expected".to_string()));
|
return Err(RegTokenDecodeError::LengthTooLong);
|
||||||
}
|
}
|
||||||
|
|
||||||
if token.len() < 32 {
|
if token.len() < 32 {
|
||||||
|
|
|
@ -3,7 +3,13 @@ use crate::{LibraryError, LibraryResult};
|
||||||
|
|
||||||
// 601e7028-0565-
|
// 601e7028-0565-
|
||||||
pub static LOCKER_SERVICE_UUID_PREFIX: (u32, u16) = (0x601e7028, 0x0565);
|
pub static LOCKER_SERVICE_UUID_PREFIX: (u32, u16) = (0x601e7028, 0x0565);
|
||||||
pub static LOCKER_SERVICE_UUID: uuid::Uuid = uuid::Builder::from_fields(LOCKER_SERVICE_UUID_PREFIX.0, LOCKER_SERVICE_UUID_PREFIX.1, 0, &[0;8]).into_uuid();
|
pub static LOCKER_SERVICE_UUID: uuid::Uuid = uuid::Builder::from_fields(
|
||||||
|
LOCKER_SERVICE_UUID_PREFIX.0,
|
||||||
|
LOCKER_SERVICE_UUID_PREFIX.1,
|
||||||
|
0,
|
||||||
|
&[0; 8],
|
||||||
|
)
|
||||||
|
.into_uuid();
|
||||||
|
|
||||||
pub enum LockerVendor {
|
pub enum LockerVendor {
|
||||||
Keba,
|
Keba,
|
||||||
|
@ -38,10 +44,7 @@ pub struct LockerDevice {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LockerDevice {
|
impl LockerDevice {
|
||||||
pub fn new(
|
pub fn new(service_uuid: LockerServiceUUID, service_data: &Vec<u8>) -> LibraryResult<Self> {
|
||||||
service_uuid: LockerServiceUUID,
|
|
||||||
service_data: &Vec<u8>,
|
|
||||||
) -> LibraryResult<Self> {
|
|
||||||
mini_assert_inval!(service_data.len() == 14);
|
mini_assert_inval!(service_data.len() == 14);
|
||||||
|
|
||||||
let mut reader = PrimitiveReader {
|
let mut reader = PrimitiveReader {
|
||||||
|
|
|
@ -40,9 +40,7 @@ pub mod token {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn revoke_form(
|
pub fn revoke_form(token: String) -> Vec<(String, String)> {
|
||||||
token: String
|
|
||||||
) -> Vec<(String, String)> {
|
|
||||||
vec![
|
vec![
|
||||||
("token".into(), token),
|
("token".into(), token),
|
||||||
("client_id".into(), client_id().into()),
|
("client_id".into(), client_id().into()),
|
||||||
|
@ -81,10 +79,13 @@ pub mod token {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod logout {
|
pub mod logout {
|
||||||
use crate::{constants::web_user_agent, login::{
|
use crate::{
|
||||||
constants::{client_id, redirect_uri_logout},
|
constants::web_user_agent,
|
||||||
DHLIdToken,
|
login::{
|
||||||
}};
|
constants::{client_id, redirect_uri_logout},
|
||||||
|
DHLIdToken,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
pub fn form(id_token: &DHLIdToken) -> Vec<(String, String)> {
|
pub fn form(id_token: &DHLIdToken) -> Vec<(String, String)> {
|
||||||
vec![
|
vec![
|
||||||
|
|
|
@ -52,7 +52,8 @@ impl<T> OpenIDToken<T> {
|
||||||
|
|
||||||
// requires valid system time
|
// requires valid system time
|
||||||
pub fn is_expired(&self) -> bool {
|
pub fn is_expired(&self) -> bool {
|
||||||
let duration: Result<std::time::Duration, std::time::SystemTimeError> = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH);
|
let duration: Result<std::time::Duration, std::time::SystemTimeError> =
|
||||||
|
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH);
|
||||||
|
|
||||||
let Ok(duration) = duration else {
|
let Ok(duration) = duration else {
|
||||||
return true;
|
return true;
|
||||||
|
@ -104,14 +105,14 @@ where
|
||||||
where
|
where
|
||||||
E: serde::de::Error,
|
E: serde::de::Error,
|
||||||
{
|
{
|
||||||
|
|
||||||
let splits = s.split_terminator(".").collect::<Vec<&str>>();
|
let splits = s.split_terminator(".").collect::<Vec<&str>>();
|
||||||
let claims = serde_json::from_slice::<Claims<T>>(
|
let claims = serde_json::from_slice::<Claims<T>>(
|
||||||
general_purpose::URL_SAFE_NO_PAD
|
general_purpose::URL_SAFE_NO_PAD
|
||||||
.decode(splits[1])
|
.decode(splits[1])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_slice(),
|
.as_slice(),
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
Ok(OpenIDToken {
|
Ok(OpenIDToken {
|
||||||
token: s.to_string(),
|
token: s.to_string(),
|
||||||
|
|
|
@ -38,4 +38,4 @@ impl CodeVerfier {
|
||||||
pub fn code_verfier(&self) -> String {
|
pub fn code_verfier(&self) -> String {
|
||||||
self.client_secret.clone()
|
self.client_secret.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,6 +142,5 @@ pub struct CustomerDataFull {
|
||||||
|
|
||||||
pub requested_services: Option<Vec<CustomerDataService>>,
|
pub requested_services: Option<Vec<CustomerDataService>>,
|
||||||
//pub customer_actions: Option,
|
//pub customer_actions: Option,
|
||||||
|
|
||||||
pub address: CustomerAddress,
|
pub address: CustomerAddress,
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,7 @@ struct SendungEmpfaenger {
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct SendungsInfo {
|
struct SendungsInfo {
|
||||||
gesuchte_sendungsnummer: String,
|
gesuchte_sendungsnummer: Option<String>,
|
||||||
sendungsrichtung: String,
|
sendungsrichtung: String,
|
||||||
sendungsname: Option<String>,
|
sendungsname: Option<String>,
|
||||||
sendungsliste: Option<String>,
|
sendungsliste: Option<String>,
|
||||||
|
@ -169,16 +169,22 @@ struct Nachhaltigkeitsstatus {
|
||||||
klimafreundlicher_empfang: bool,
|
klimafreundlicher_empfang: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Sendungsnummern {
|
||||||
|
sendungsnummer: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct SendungDetails {
|
struct SendungDetails {
|
||||||
quelle: SendungsQuelle, // PAKET
|
quelle: Option<SendungsQuelle>, // PAKET
|
||||||
express_sendung: Option<bool>,
|
express_sendung: Option<bool>,
|
||||||
is_shipper_plz: Option<bool>,
|
is_shipper_plz: Option<bool>,
|
||||||
ist_zugestellt: Option<bool>,
|
ist_zugestellt: Option<bool>,
|
||||||
retoure: Option<bool>,
|
retoure: Option<bool>,
|
||||||
ruecksendung: Option<bool>,
|
ruecksendung: Option<bool>,
|
||||||
sendungsverlauf: SendungsVerlauf,
|
sendungsverlauf: Option<SendungsVerlauf>,
|
||||||
show_quality_level_hint: Option<bool>,
|
show_quality_level_hint: Option<bool>,
|
||||||
two_man_handling: Option<bool>,
|
two_man_handling: Option<bool>,
|
||||||
unplausibel: Option<bool>,
|
unplausibel: Option<bool>,
|
||||||
|
@ -192,7 +198,7 @@ struct SendungDetails {
|
||||||
international: Option<bool>,
|
international: Option<bool>,
|
||||||
pan_empfaenger: Option<SendungEmpfaenger>,
|
pan_empfaenger: Option<SendungEmpfaenger>,
|
||||||
produkt_name: Option<String>,
|
produkt_name: Option<String>,
|
||||||
//sendungsnummern: (),
|
sendungsnummern: Option<Sendungsnummern>,
|
||||||
services: Option<SendungServiceStatusBenachrichtigung>,
|
services: Option<SendungServiceStatusBenachrichtigung>,
|
||||||
show_digital_notification_cta_hint: Option<bool>,
|
show_digital_notification_cta_hint: Option<bool>,
|
||||||
warenpost: Option<bool>,
|
warenpost: Option<bool>,
|
||||||
|
@ -321,7 +327,7 @@ pub struct Shipment {
|
||||||
pub needs_shipment_date: bool,
|
pub needs_shipment_date: bool,
|
||||||
pub needs_plz: bool,
|
pub needs_plz: bool,
|
||||||
|
|
||||||
pub quelle: SendungsQuelle,
|
pub quelle: Option<SendungsQuelle>,
|
||||||
|
|
||||||
// probably not optional
|
// probably not optional
|
||||||
pub international: Option<bool>,
|
pub international: Option<bool>,
|
||||||
|
@ -330,7 +336,7 @@ pub struct Shipment {
|
||||||
|
|
||||||
pub special: ShipmentSpecialDetails,
|
pub special: ShipmentSpecialDetails,
|
||||||
|
|
||||||
pub history: SendungsVerlauf,
|
pub history: Option<SendungsVerlauf>,
|
||||||
pub error: Option<ShipmentNotFoundError>,
|
pub error: Option<ShipmentNotFoundError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,8 +27,13 @@ impl CookieHeaderValueBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_key_value(mut self, key: String, value: String) -> Self {
|
pub fn add_key_value(mut self, key: String, value: String) -> Self {
|
||||||
self.list
|
self.list.push((key, value));
|
||||||
.push((key, value));
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_key_value_str(mut self, key: &str, value: &str) -> Self {
|
||||||
|
self.list.push((key.to_string(), value.to_string()));
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -188,6 +193,7 @@ macro_rules! parse_json_response {
|
||||||
($res: expr, $type: ty) => {{
|
($res: expr, $type: ty) => {{
|
||||||
let status = $res.status();
|
let status = $res.status();
|
||||||
let res: String = $res.text().await.unwrap();
|
let res: String = $res.text().await.unwrap();
|
||||||
|
println!("text: {}", res);
|
||||||
// Catch HTML Response early
|
// Catch HTML Response early
|
||||||
if status == 200 {
|
if status == 200 {
|
||||||
let res = res.clone();
|
let res = res.clone();
|
||||||
|
|
83
meson.build
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Emmanuele Bassi
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
project('paket', 'rust',
|
||||||
|
version: '2024.10',
|
||||||
|
license: ['GPL-3.0'],
|
||||||
|
meson_version: '>= 0.59.0',
|
||||||
|
default_options: [ 'warning_level=2', ],
|
||||||
|
)
|
||||||
|
|
||||||
|
dependency('gtk4', version: '>= 4.16')
|
||||||
|
dependency('libadwaita-1', version: '>= 1.6')
|
||||||
|
dependency('gstreamer-1.0', version: '>= 1.20')
|
||||||
|
dependency('gstreamer-video-1.0', version: '>= 1.20')
|
||||||
|
dependency('webkitgtk-6.0')
|
||||||
|
# Needed for camerabin
|
||||||
|
dependency('gstreamer-plugins-bad-1.0', version: '>= 1.20')
|
||||||
|
|
||||||
|
|
||||||
|
i18n = import('i18n')
|
||||||
|
gnome = import('gnome')
|
||||||
|
fs = import('fs')
|
||||||
|
|
||||||
|
cargo = find_program('cargo', required: true)
|
||||||
|
|
||||||
|
if get_option('profile') == 'development'
|
||||||
|
profile = '.Devel'
|
||||||
|
if fs.is_dir('.git')
|
||||||
|
vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD', check: true).stdout().strip()
|
||||||
|
if vcs_tag == ''
|
||||||
|
version_suffix = '-devel'
|
||||||
|
else
|
||||||
|
version_suffix = '-@0@'.format(vcs_tag)
|
||||||
|
endif
|
||||||
|
else
|
||||||
|
version_suffix = '-devel'
|
||||||
|
endif
|
||||||
|
else
|
||||||
|
profile = ''
|
||||||
|
version_suffix = ''
|
||||||
|
endif
|
||||||
|
|
||||||
|
application_id = 'de.j4ne.Paket@0@'.format(profile)
|
||||||
|
|
||||||
|
|
||||||
|
pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name()
|
||||||
|
|
||||||
|
gnome.compile_resources('paket',
|
||||||
|
'paket.gresource.xml',
|
||||||
|
gresource_bundle: true,
|
||||||
|
install: true,
|
||||||
|
install_dir: pkgdatadir,
|
||||||
|
)
|
||||||
|
|
||||||
|
subdir('src')
|
||||||
|
subdir('po')
|
||||||
|
|
||||||
|
meson.add_dist_script(
|
||||||
|
'build-aux/dist-vendor.sh',
|
||||||
|
meson.project_source_root(),
|
||||||
|
meson.project_build_root() / 'meson-dist' / '@0@-@1@'.format(meson.project_name(), meson.project_version()),
|
||||||
|
)
|
||||||
|
|
||||||
|
gnome.post_install(
|
||||||
|
glib_compile_schemas: true,
|
||||||
|
gtk_update_icon_cache: true,
|
||||||
|
update_desktop_database: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
summary({
|
||||||
|
'prefix': get_option('prefix'),
|
||||||
|
'libdir': get_option('libdir'),
|
||||||
|
'datadir': get_option('datadir'),
|
||||||
|
'bindir': get_option('bindir'),
|
||||||
|
},
|
||||||
|
section: 'Directories',
|
||||||
|
)
|
||||||
|
|
||||||
|
summary({
|
||||||
|
'Profile': get_option('profile'),
|
||||||
|
},
|
||||||
|
section: 'Build options',
|
||||||
|
)
|
12
meson_options.txt
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Emmanuele Bassi
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
option (
|
||||||
|
'profile',
|
||||||
|
type: 'combo',
|
||||||
|
choices: [
|
||||||
|
'default',
|
||||||
|
'development'
|
||||||
|
],
|
||||||
|
value: 'default',
|
||||||
|
)
|
9
paket-utils/Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
[package]
|
||||||
|
name = "paket-utils"
|
||||||
|
authors.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
glib = { workspace = true }
|
65
paket-utils/src/lib.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
// Based on https://gitlab.gnome.org/GNOME/libsoup/-/blob/master/libsoup/soup-misc.c?ref_type=heads#L11
|
||||||
|
// Which itself is based on epiphany-webkit (ephy_langs_append_languages())
|
||||||
|
|
||||||
|
use glib;
|
||||||
|
|
||||||
|
fn map_posix_lang_to_rfc2616<T: glib::IntoGStr>(posix_lang: T) -> Option<String> {
|
||||||
|
posix_lang.run_with_gstr(|lang| {
|
||||||
|
if false
|
||||||
|
/* Don't include charset variants, etc */
|
||||||
|
|| lang.contains('.')
|
||||||
|
|| lang.contains('@')
|
||||||
|
/* Ignore "C" locale, which g_get_language_names() always
|
||||||
|
* includes as a fallback. */
|
||||||
|
|| lang.starts_with('C')
|
||||||
|
{
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(lang.as_str().to_ascii_lowercase().replace('_', "-"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn system_get_accept_languages() -> Vec<String> {
|
||||||
|
let langs = glib::language_names();
|
||||||
|
|
||||||
|
let mut http_langs = langs
|
||||||
|
.iter()
|
||||||
|
.filter_map(map_posix_lang_to_rfc2616)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if http_langs.len() == 0 {
|
||||||
|
http_langs.push("de".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
if http_langs.len() == 1 {
|
||||||
|
http_langs
|
||||||
|
} else {
|
||||||
|
// Limit to 2 for privacy reasons for now
|
||||||
|
let len = std::cmp::min(langs.len(), 2);
|
||||||
|
let mut item = len;
|
||||||
|
|
||||||
|
http_langs
|
||||||
|
.iter_mut()
|
||||||
|
.map_while(|string| {
|
||||||
|
while item <= 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let item_cur = item;
|
||||||
|
item = item - 1;
|
||||||
|
|
||||||
|
if item_cur != len {
|
||||||
|
let quality_number = len as f64 / item_cur as f64;
|
||||||
|
let quality = if quality_number % 0.1 == 0. {
|
||||||
|
format!(";q=0.{:.1}", quality_number)
|
||||||
|
} else {
|
||||||
|
format!(";q=0.{:.2}", quality_number)
|
||||||
|
};
|
||||||
|
string.push_str(&quality);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(string)
|
||||||
|
})
|
||||||
|
.map(|item| item.to_owned())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
}
|
17
paket.gresource.xml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<gresources>
|
||||||
|
<gresource prefix="/de/j4ne/Paket/icons/scalable/actions/">
|
||||||
|
<file alias="copy-symbolic.svg">assets/copy-symbolic.svg</file>
|
||||||
|
<file alias="four-arrows-pointing-outward-symbolic.svg">assets/four-arrows-pointing-outward-symbolic.svg</file>
|
||||||
|
<file alias="loupe-large.svg">assets/loupe-large-symbolic.svg</file>
|
||||||
|
<file alias="mail-symbolic.svg">assets/mail-symbolic.svg</file>
|
||||||
|
<file alias="minus-symbolic.svg">assets/minus-symbolic.svg</file>
|
||||||
|
<file alias="package-x-generic-symbolic.svg">assets/package-x-generic-symbolic.svg</file>
|
||||||
|
<file alias="parcel-locker-symbolic.svg">assets/parcel-locker-symbolic.svg</file>
|
||||||
|
<file alias="person-symbolic.svg">assets/person-symbolic.svg</file>
|
||||||
|
<file alias="plus-symbolic.svg">assets/plus-symbolic.svg</file>
|
||||||
|
<file alias="qr-code-scanner-symbolic.svg">assets/qr-code-scanner-symbolic.svg</file>
|
||||||
|
</gresource>
|
||||||
|
<gresource prefix="/de/j4ne/Paket">
|
||||||
|
</gresource>
|
||||||
|
</gresources>
|
|
@ -1,25 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "paket"
|
|
||||||
authors.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
version.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
# using git version, for https://github.com/Relm4/Relm4/pull/677
|
|
||||||
relm4 = { workspace = true }
|
|
||||||
relm4-icons = { version = "0.9" }
|
|
||||||
tracker = "0.2"
|
|
||||||
adw = { package = "libadwaita", version = "0.7", features = ["v1_6"] }
|
|
||||||
aperture = "0.7"
|
|
||||||
webkit = { package = "webkit6", version = "0.4" }
|
|
||||||
libpaket = { path = "../libpaket" }
|
|
||||||
glycin = { version = "2.0.0-beta", features = ["gdk4"] }
|
|
||||||
oo7 = { version = "0.3" }
|
|
||||||
futures = "0.3"
|
|
||||||
gtk = { package = "gtk4", version = "0.9", features = ["v4_16"] }
|
|
||||||
reqwest = { workspace = true }
|
|
||||||
secrecy = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
|
||||||
uuid = { workspace = true }
|
|
|
@ -1 +0,0 @@
|
||||||
pub const APP_ID: &str = "de.j4ne.Paket";
|
|
|
@ -1,17 +0,0 @@
|
||||||
pub mod account;
|
|
||||||
pub mod advice;
|
|
||||||
pub mod advices;
|
|
||||||
pub mod constants;
|
|
||||||
pub mod keyring;
|
|
||||||
pub mod login;
|
|
||||||
pub mod packstation;
|
|
||||||
pub mod ready;
|
|
||||||
pub mod scanner;
|
|
||||||
pub mod tracking;
|
|
||||||
|
|
||||||
pub use login::LoginSharedState;
|
|
||||||
|
|
||||||
pub static LOGIN_BROKER: relm4::MessageBroker<login::LoginInput> = relm4::MessageBroker::new();
|
|
||||||
pub fn send_log_out() {
|
|
||||||
LOGIN_BROKER.send(login::LoginInput::LogOut);
|
|
||||||
}
|
|
10
po/POTFILES.in
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
src/account.rs
|
||||||
|
src/advices.rs
|
||||||
|
src/app.rs
|
||||||
|
src/keyring.rs
|
||||||
|
src/login.rs
|
||||||
|
src/packstation.rs
|
||||||
|
src/ready.rs
|
||||||
|
src/scanner.rs
|
||||||
|
src/tracking.rs
|
||||||
|
src/utils.rs
|
15
po/meson.build
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Emmanuele Bassi
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
i18n.gettext(
|
||||||
|
'paket',
|
||||||
|
args: [
|
||||||
|
'--keyword=i18n',
|
||||||
|
'--keyword=i18n_f',
|
||||||
|
'--keyword=i18n_k',
|
||||||
|
'--keyword=ni18n:1,2',
|
||||||
|
'--keyword=ni18n_f:1,2',
|
||||||
|
'--keyword=ni18n_k:1,2',
|
||||||
|
],
|
||||||
|
preset: 'glib',
|
||||||
|
)
|
|
@ -2,7 +2,7 @@ use adw::prelude::*;
|
||||||
use libpaket::{stammdaten::CustomerDataFull, LibraryError, LibraryResult};
|
use libpaket::{stammdaten::CustomerDataFull, LibraryError, LibraryResult};
|
||||||
use relm4::{Component, ComponentParts};
|
use relm4::{Component, ComponentParts};
|
||||||
|
|
||||||
use crate::{send_log_out, LoginSharedState};
|
use crate::{i18n::i18n, send_log_out, LoginSharedState};
|
||||||
|
|
||||||
#[tracker::track]
|
#[tracker::track]
|
||||||
pub struct AccountView {
|
pub struct AccountView {
|
||||||
|
@ -92,7 +92,7 @@ impl Component for AccountView {
|
||||||
|
|
||||||
append = >k::Button {
|
append = >k::Button {
|
||||||
add_css_class: relm4::css::DESTRUCTIVE_ACTION,
|
add_css_class: relm4::css::DESTRUCTIVE_ACTION,
|
||||||
set_label: "Log out",
|
set_label: &i18n("Log out"),
|
||||||
|
|
||||||
connect_clicked => AccountInput::LogOut,
|
connect_clicked => AccountInput::LogOut,
|
||||||
},
|
},
|
||||||
|
@ -101,7 +101,7 @@ impl Component for AccountView {
|
||||||
// Postnumber
|
// Postnumber
|
||||||
add = &adw::ActionRow {
|
add = &adw::ActionRow {
|
||||||
add_css_class: relm4::css::NUMERIC,
|
add_css_class: relm4::css::NUMERIC,
|
||||||
set_subtitle: "Postnummer",
|
set_subtitle: &i18n("Postnummer"),
|
||||||
|
|
||||||
#[track(model.changed_customer_data_full() && model.get_customer_data_full().is_some())]
|
#[track(model.changed_customer_data_full() && model.get_customer_data_full().is_some())]
|
||||||
set_title?: get_str_from_customer_data!(model, post_number).as_ref(),
|
set_title?: get_str_from_customer_data!(model, post_number).as_ref(),
|
||||||
|
@ -112,8 +112,8 @@ impl Component for AccountView {
|
||||||
|
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_child = &adw::ButtonContent {
|
set_child = &adw::ButtonContent {
|
||||||
set_icon_name: relm4_icons::icon_names::COPY,
|
set_icon_name: "copy-symbolic",
|
||||||
set_label: "Copy",
|
set_label: &i18n("Copy"),
|
||||||
},
|
},
|
||||||
|
|
||||||
connect_clicked => AccountInput::Copy(CopyTargets::PostNumber),
|
connect_clicked => AccountInput::Copy(CopyTargets::PostNumber),
|
||||||
|
@ -211,10 +211,11 @@ impl Component for AccountView {
|
||||||
for service in services {
|
for service in services {
|
||||||
match service {
|
match service {
|
||||||
libpaket::stammdaten::CustomerDataService::Packstation => {
|
libpaket::stammdaten::CustomerDataService::Packstation => {
|
||||||
sender.output(AccountOutput::HaveService(
|
sender
|
||||||
AccountServices::PackstationAvailable,
|
.output(AccountOutput::HaveService(
|
||||||
))
|
AccountServices::PackstationAvailable,
|
||||||
.unwrap();
|
))
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
};
|
};
|
|
@ -39,6 +39,15 @@ impl FactoryComponent for AdviceCard {
|
||||||
type CommandOutput = AdviceCardCmds;
|
type CommandOutput = AdviceCardCmds;
|
||||||
type ParentWidget = gtk::FlowBox;
|
type ParentWidget = gtk::FlowBox;
|
||||||
|
|
||||||
|
/* TODO:
|
||||||
|
* Action "View in Image viewer":
|
||||||
|
* Triggers:
|
||||||
|
* - One click on "enlarge" button
|
||||||
|
* - Double click on "texture" itself
|
||||||
|
* (Is there a better way than this?)
|
||||||
|
* Create temporary file, let application open this file...
|
||||||
|
*/
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
#[root]
|
#[root]
|
||||||
gtk::FlowBoxChild {
|
gtk::FlowBoxChild {
|
||||||
|
@ -46,21 +55,36 @@ impl FactoryComponent for AdviceCard {
|
||||||
set_valign: gtk::Align::Start,
|
set_valign: gtk::Align::Start,
|
||||||
|
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_child = >k::Overlay {
|
set_child = &adw::Clamp {
|
||||||
set_margin_all: 8,
|
|
||||||
|
|
||||||
add_overlay = >k::Spinner {
|
|
||||||
start: (),
|
|
||||||
set_align: gtk::Align::Center,
|
|
||||||
|
|
||||||
#[track(self.changed_texture())]
|
|
||||||
set_visible: self.texture.is_none(),
|
|
||||||
},
|
|
||||||
|
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_child = >k::Picture {
|
set_child = >k::Overlay {
|
||||||
#[track(self.changed_texture())]
|
set_margin_all: 8,
|
||||||
set_paintable: self.texture.as_ref()
|
|
||||||
|
add_overlay = >k::Spinner {
|
||||||
|
start: (),
|
||||||
|
set_align: gtk::Align::Center,
|
||||||
|
|
||||||
|
#[track(self.changed_texture())]
|
||||||
|
set_visible: self.texture.is_none(),
|
||||||
|
},
|
||||||
|
|
||||||
|
add_overlay = >k::Button {
|
||||||
|
set_valign: gtk::Align::End,
|
||||||
|
set_halign: gtk::Align::End,
|
||||||
|
|
||||||
|
add_css_class: relm4::css::OSD,
|
||||||
|
add_css_class: relm4::css::CIRCULAR,
|
||||||
|
|
||||||
|
set_icon_name: "four-arrows-pointing-outward-symbolic",
|
||||||
|
|
||||||
|
set_margin_all: 8,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = >k::Picture {
|
||||||
|
#[track(self.changed_texture())]
|
||||||
|
set_paintable: self.texture.as_ref()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,19 +1,16 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use futures::lock::Mutex;
|
use futures::lock::Mutex;
|
||||||
use libpaket::{AdviceClient as LibraryAdviceClient, LibraryResult};
|
use libpaket::{AdviceClient as LibraryAdviceClient, LibraryResult};
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use gio::prelude::*;
|
|
||||||
use glib::prelude::*;
|
|
||||||
use gtk::{gio, glib};
|
|
||||||
use relm4::prelude::*;
|
|
||||||
use relm4::prelude::*;
|
use relm4::prelude::*;
|
||||||
|
|
||||||
use relm4::factory::FactoryVecDeque;
|
use relm4::factory::FactoryVecDeque;
|
||||||
|
|
||||||
use crate::advice::{Advice, AdviceCard, AdviceProd};
|
use crate::advice::{Advice, AdviceCard, AdviceProd};
|
||||||
|
use crate::i18n::i18n;
|
||||||
|
use crate::utils;
|
||||||
|
|
||||||
struct AdviceClientImpl {
|
struct AdviceClientImpl {
|
||||||
uat_token: libpaket::advices::UatToken,
|
uat_token: libpaket::advices::UatToken,
|
||||||
|
@ -72,9 +69,7 @@ impl FactoryComponent for AdvicesDayView {
|
||||||
set_label: self.date.as_str(),
|
set_label: self.date.as_str(),
|
||||||
},
|
},
|
||||||
|
|
||||||
self.factory.widget() -> >k::FlowBox {
|
self.factory.widget() -> >k::FlowBox {},
|
||||||
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,44 +128,36 @@ impl AsyncComponent for AdvicesView {
|
||||||
type CommandOutput = AdvicesViewCommands;
|
type CommandOutput = AdvicesViewCommands;
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
gtk::ScrolledWindow {
|
adw::ViewStack {
|
||||||
adw::Clamp {
|
#[name = "advices_page_loading"]
|
||||||
|
add = &utils::status_page(&i18n("Loading mail notifications..."), None, None) -> adw::StatusPage {},
|
||||||
|
|
||||||
|
#[name = "advices_page_no_available"]
|
||||||
|
add = &utils::status_page(&i18n("No mail notifications available."), None, None) -> adw::StatusPage {},
|
||||||
|
|
||||||
|
#[name = "advices_page_have_some"]
|
||||||
|
add = >k::ScrolledWindow {
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_child = &adw::ViewStack {
|
set_child = >k::Viewport {
|
||||||
#[name = "advices_page_loading"]
|
set_vscroll_policy: gtk::ScrollablePolicy::Natural,
|
||||||
add = &adw::StatusPage {
|
|
||||||
set_title: "Loading mail notifications...",
|
|
||||||
},
|
|
||||||
|
|
||||||
#[name = "advices_page_no_available"]
|
#[wrap(Some)]
|
||||||
add = &adw::StatusPage {
|
set_child = model.factory.widget() -> >k::Box {
|
||||||
set_title: "No mail notifications available."
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
},
|
|
||||||
|
|
||||||
#[name = "advices_page_have_some"]
|
|
||||||
add = &adw::Clamp {
|
|
||||||
#[wrap(Some)]
|
|
||||||
set_child = >k::ScrolledWindow {
|
|
||||||
#[wrap(Some)]
|
|
||||||
set_child = model.factory.widget() -> >k::Box {
|
|
||||||
set_orientation: gtk::Orientation::Vertical,
|
|
||||||
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
#[track(model.changed_state())]
|
|
||||||
set_visible_child: {
|
|
||||||
let page: >k::Widget = match model.state {
|
|
||||||
AdvicesViewState::Loading => advices_page_loading.upcast_ref::<gtk::Widget>(),
|
|
||||||
AdvicesViewState::HaveNone => advices_page_no_available.upcast_ref::<gtk::Widget>(),
|
|
||||||
AdvicesViewState::HaveSome => advices_page_have_some.upcast_ref::<gtk::Widget>(),
|
|
||||||
};
|
|
||||||
page
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
|
||||||
|
#[track(model.changed_state())]
|
||||||
|
set_visible_child: {
|
||||||
|
let page: >k::Widget = match model.state {
|
||||||
|
AdvicesViewState::Loading => advices_page_loading.upcast_ref::<gtk::Widget>(),
|
||||||
|
AdvicesViewState::HaveNone => advices_page_no_available.upcast_ref::<gtk::Widget>(),
|
||||||
|
AdvicesViewState::HaveSome => advices_page_have_some.upcast_ref::<gtk::Widget>(),
|
||||||
|
};
|
||||||
|
page
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn init(
|
async fn init(
|
||||||
|
@ -204,7 +191,7 @@ impl AsyncComponent for AdvicesView {
|
||||||
AdvicesViewInput::Reset => {
|
AdvicesViewInput::Reset => {
|
||||||
self.set_state(AdvicesViewState::Loading);
|
self.set_state(AdvicesViewState::Loading);
|
||||||
self.factory.guard().clear();
|
self.factory.guard().clear();
|
||||||
},
|
}
|
||||||
AdvicesViewInput::Fetch => {
|
AdvicesViewInput::Fetch => {
|
||||||
self.set_state(AdvicesViewState::Loading);
|
self.set_state(AdvicesViewState::Loading);
|
||||||
|
|
||||||
|
@ -232,9 +219,9 @@ impl AsyncComponent for AdvicesView {
|
||||||
|
|
||||||
let client = AdviceClient::new(uat_token);
|
let client = AdviceClient::new(uat_token);
|
||||||
|
|
||||||
if let Some(current) = advices.get_current_advice() {
|
for new_n in advices.get_current_advices() {
|
||||||
let mut advices_arr = Vec::new();
|
let mut advices_arr = Vec::new();
|
||||||
for item in ¤t.list {
|
for item in &new_n.list {
|
||||||
advices_arr.push(Advice::Prod(AdviceProd {
|
advices_arr.push(Advice::Prod(AdviceProd {
|
||||||
client: client.clone(),
|
client: client.clone(),
|
||||||
model: item.clone(),
|
model: item.clone(),
|
||||||
|
@ -242,7 +229,7 @@ impl AsyncComponent for AdvicesView {
|
||||||
}
|
}
|
||||||
|
|
||||||
arr.push(AdvicesForDay {
|
arr.push(AdvicesForDay {
|
||||||
date: current.date.clone(),
|
date: new_n.date.clone(),
|
||||||
advices: advices_arr,
|
advices: advices_arr,
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
use adw::{self, glib, prelude::*};
|
use crate::i18n::i18n;
|
||||||
use gtk;
|
use crate::login::{new_login_shared_state, Login, LoginOutput};
|
||||||
use paket::login::{new_login_shared_state, Login, LoginOutput};
|
use crate::ready::{Ready, ReadyOutput};
|
||||||
use paket::ready::{Ready, ReadyOutput};
|
use adw::{self, prelude::*};
|
||||||
use relm4::{main_adw_application, prelude::*, AsyncComponentSender, RELM_THREADS};
|
use relm4::{main_adw_application, prelude::*, AsyncComponentSender};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
enum AppState {
|
enum AppState {
|
||||||
|
@ -13,13 +13,13 @@ enum AppState {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct AppError {
|
pub struct AppError {
|
||||||
short: String,
|
short: String,
|
||||||
long: String,
|
long: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum AppInput {
|
pub enum AppInput {
|
||||||
AddBreakpoint(adw::Breakpoint),
|
AddBreakpoint(adw::Breakpoint),
|
||||||
SwitchToLogin,
|
SwitchToLogin,
|
||||||
SwitchToLoading,
|
SwitchToLoading,
|
||||||
|
@ -28,7 +28,7 @@ enum AppInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracker::track]
|
#[tracker::track]
|
||||||
struct App {
|
pub struct App {
|
||||||
state: AppState,
|
state: AppState,
|
||||||
_network_fail: bool,
|
_network_fail: bool,
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ struct App {
|
||||||
ready: Controller<Ready>,
|
ready: Controller<Ready>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[relm4::component(async)]
|
#[relm4::component(async, pub)]
|
||||||
impl AsyncComponent for App {
|
impl AsyncComponent for App {
|
||||||
type Input = AppInput;
|
type Input = AppInput;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
@ -111,7 +111,7 @@ impl AsyncComponent for App {
|
||||||
.forward(sender.input_sender(), convert_ready_response);
|
.forward(sender.input_sender(), convert_ready_response);
|
||||||
|
|
||||||
let login = Login::builder()
|
let login = Login::builder()
|
||||||
.launch_with_broker(login_shared_state.clone(), &paket::LOGIN_BROKER)
|
.launch_with_broker(login_shared_state.clone(), &crate::LOGIN_BROKER)
|
||||||
.forward(sender.input_sender(), convert_login_response);
|
.forward(sender.input_sender(), convert_login_response);
|
||||||
|
|
||||||
let model = App {
|
let model = App {
|
||||||
|
@ -166,11 +166,11 @@ fn convert_login_response(response: LoginOutput) -> AppInput {
|
||||||
LoginOutput::RequiresLogin => AppInput::SwitchToLogin,
|
LoginOutput::RequiresLogin => AppInput::SwitchToLogin,
|
||||||
LoginOutput::RequiresLoading => AppInput::SwitchToLoading,
|
LoginOutput::RequiresLoading => AppInput::SwitchToLoading,
|
||||||
LoginOutput::Error(library_error) => AppInput::FatalErr(AppError {
|
LoginOutput::Error(library_error) => AppInput::FatalErr(AppError {
|
||||||
short: "Unhandled API error".to_string(),
|
short: i18n("Unhandled API error"),
|
||||||
long: library_error.to_string(),
|
long: library_error.to_string(),
|
||||||
}),
|
}),
|
||||||
LoginOutput::KeyringError(error) => AppInput::FatalErr(AppError {
|
LoginOutput::KeyringError(error) => AppInput::FatalErr(AppError {
|
||||||
short: "Keyring usage failed".to_string(),
|
short: i18n("Keyring usage failed"),
|
||||||
long: error.to_string(),
|
long: error.to_string(),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
@ -182,17 +182,3 @@ fn convert_ready_response(response: ReadyOutput) -> AppInput {
|
||||||
ReadyOutput::AddBreakpoint(breakpoint) => AppInput::AddBreakpoint(breakpoint),
|
ReadyOutput::AddBreakpoint(breakpoint) => AppInput::AddBreakpoint(breakpoint),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
|
||||||
RELM_THREADS.set(4).unwrap();
|
|
||||||
aperture::init(paket::constants::APP_ID);
|
|
||||||
let display = gtk::gdk::Display::default().unwrap();
|
|
||||||
let theme = gtk::IconTheme::for_display(&display);
|
|
||||||
theme.add_resource_path("/de/j4ne/Paket/icons/");
|
|
||||||
theme.add_resource_path("/de/j4ne/Paket/scalable/actions/");
|
|
||||||
relm4_icons::initialize_icons();
|
|
||||||
|
|
||||||
let app = RelmApp::new(paket::constants::APP_ID);
|
|
||||||
|
|
||||||
app.run_async::<App>(());
|
|
||||||
}
|
|
81
src/bin/locker_scanner.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
/*use relm4::{gtk, prelude::*, RelmApp};
|
||||||
|
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use libpaket::locker::LockerServiceUUID;
|
||||||
|
use paket::lockers::{Locker, LockerManager, LockerScannerInput};
|
||||||
|
|
||||||
|
struct App;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum AppInput {
|
||||||
|
LockerFound(LockerServiceUUID),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relm4::component(async)]
|
||||||
|
impl AsyncComponent for App {
|
||||||
|
type Init = ();
|
||||||
|
type Input = AppInput;
|
||||||
|
type Output = ();
|
||||||
|
type CommandOutput = ();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
#[root]
|
||||||
|
main_window = gtk::ApplicationWindow::new(&relm4::main_application()) {
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = list_box = >k::ListBox {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init(
|
||||||
|
init: Self::Init,
|
||||||
|
root: Self::Root,
|
||||||
|
sender: AsyncComponentSender<Self>,
|
||||||
|
) -> AsyncComponentParts<Self> {
|
||||||
|
let model = App {};
|
||||||
|
|
||||||
|
let locker = LockerManager::builder()
|
||||||
|
.launch(())
|
||||||
|
.forward(sender.input_sender(), transform_scanner_to_app);
|
||||||
|
|
||||||
|
let widgets = view_output!();
|
||||||
|
|
||||||
|
AsyncComponentParts { model, widgets }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_with_view(
|
||||||
|
&mut self,
|
||||||
|
widgets: &mut Self::Widgets,
|
||||||
|
message: Self::Input,
|
||||||
|
sender: AsyncComponentSender<Self>,
|
||||||
|
root: &Self::Root,
|
||||||
|
) -> Self::Output {
|
||||||
|
match message {
|
||||||
|
AppInput::LockerFound(locker) => {
|
||||||
|
let label = gtk::Label::builder()
|
||||||
|
.label(Into::<uuid::Uuid>::into(locker).to_string())
|
||||||
|
.build();
|
||||||
|
widgets.list_box.append(&label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update_view(widgets, sender);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform_scanner_to_app(input: Locker) -> AppInput {
|
||||||
|
AppInput::LockerFound(input.get_service_uuid().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
paket::init();
|
||||||
|
relm4::RELM_THREADS.set(10).unwrap();
|
||||||
|
relm4::RELM_BLOCKING_THREADS.set(10).unwrap();
|
||||||
|
|
||||||
|
let app = RelmApp::new(format!("{}.TestBle", paket::config::APPLICATION_ID).as_str());
|
||||||
|
app.run_async::<App>(());
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
fn main() {}
|
9
src/bin/paket.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
use relm4::RelmApp;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
paket::init();
|
||||||
|
|
||||||
|
let app = RelmApp::new(paket::config::APPLICATION_ID);
|
||||||
|
|
||||||
|
app.run_async::<paket::app::App>(());
|
||||||
|
}
|
83
src/bin/qr_test.rs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
use gtk;
|
||||||
|
use relm4::{RelmApp, RELM_THREADS};
|
||||||
|
|
||||||
|
use adw::{self, prelude::*};
|
||||||
|
use relm4::{main_adw_application, prelude::*, AsyncComponentSender};
|
||||||
|
|
||||||
|
use paket::scanner::*;
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
scanner: AsyncController<Scanner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[relm4::component(async, pub)]
|
||||||
|
impl AsyncComponent for App {
|
||||||
|
type Input = ();
|
||||||
|
type Output = ();
|
||||||
|
type Init = ();
|
||||||
|
type CommandOutput = ();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
#[root]
|
||||||
|
main_window = adw::ApplicationWindow::new(&main_adw_application()) {
|
||||||
|
set_default_height: 600,
|
||||||
|
set_default_width: 800,
|
||||||
|
set_width_request: 300,
|
||||||
|
set_height_request: 300,
|
||||||
|
|
||||||
|
#[wrap(Some)]
|
||||||
|
#[name = "navigation"]
|
||||||
|
set_content = &adw::NavigationView {
|
||||||
|
push = &adw::NavigationPage {
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_child = &adw::ToolbarView {
|
||||||
|
add_top_bar = &adw::HeaderBar {},
|
||||||
|
|
||||||
|
#[wrap(Some)]
|
||||||
|
set_content = >k::Label {
|
||||||
|
set_label: "First page"
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
push: model.scanner.widget(),
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init(
|
||||||
|
_: Self::Init,
|
||||||
|
root: Self::Root,
|
||||||
|
sender: AsyncComponentSender<Self>,
|
||||||
|
) -> AsyncComponentParts<Self> {
|
||||||
|
let scanner = Scanner::builder().launch(()).detach();
|
||||||
|
|
||||||
|
let model = App { scanner };
|
||||||
|
|
||||||
|
let widgets = view_output!();
|
||||||
|
|
||||||
|
model.scanner.emit(ScannerInput::Activate);
|
||||||
|
|
||||||
|
AsyncComponentParts { model, widgets }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_with_view(
|
||||||
|
&mut self,
|
||||||
|
widgets: &mut Self::Widgets,
|
||||||
|
message: Self::Input,
|
||||||
|
sender: AsyncComponentSender<Self>,
|
||||||
|
root: &Self::Root,
|
||||||
|
) -> Self::Output {
|
||||||
|
self.update_view(widgets, sender);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
paket::init();
|
||||||
|
|
||||||
|
let app = RelmApp::new(paket::config::APPLICATION_ID);
|
||||||
|
|
||||||
|
app.run_async::<App>(());
|
||||||
|
}
|
215
src/i18n.rs
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
// i18n.rs
|
||||||
|
//
|
||||||
|
// Copyright 2020 Christopher Davis <christopherdavis@gnome.org>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
use gettext::{gettext, ngettext, npgettext, pgettext};
|
||||||
|
use regex::{Captures, Regex};
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn freplace(input: String, args: &[&str]) -> String {
|
||||||
|
let mut parts = input.split("{}");
|
||||||
|
let mut output = parts.next().unwrap_or_default().to_string();
|
||||||
|
for (p, a) in parts.zip(args.iter()) {
|
||||||
|
output += &(a.to_string() + p);
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn kreplace(input: String, kwargs: &[(&str, &str)]) -> String {
|
||||||
|
let mut s = input;
|
||||||
|
for (k, v) in kwargs {
|
||||||
|
if let Ok(re) = Regex::new(&format!("\\{{{}\\}}", k)) {
|
||||||
|
s = re
|
||||||
|
.replace_all(&s, |_: &Captures<'_>| v.to_string())
|
||||||
|
.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple translations functions
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn i18n(format: &str) -> String {
|
||||||
|
gettext(format)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn i18n_f(format: &str, args: &[&str]) -> String {
|
||||||
|
let s = gettext(format);
|
||||||
|
freplace(s, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn i18n_k(format: &str, kwargs: &[(&str, &str)]) -> String {
|
||||||
|
let s = gettext(format);
|
||||||
|
kreplace(s, kwargs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singular and plural translations functions
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn ni18n(single: &str, multiple: &str, number: u32) -> String {
|
||||||
|
ngettext(single, multiple, number)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn ni18n_f(single: &str, multiple: &str, number: u32, args: &[&str]) -> String {
|
||||||
|
let s = ngettext(single, multiple, number);
|
||||||
|
freplace(s, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn ni18n_k(single: &str, multiple: &str, number: u32, kwargs: &[(&str, &str)]) -> String {
|
||||||
|
let s = ngettext(single, multiple, number);
|
||||||
|
kreplace(s, kwargs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translations with context functions
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn pi18n(ctx: &str, format: &str) -> String {
|
||||||
|
pgettext(ctx, format)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn pi18n_f(ctx: &str, format: &str, args: &[&str]) -> String {
|
||||||
|
let s = pgettext(ctx, format);
|
||||||
|
freplace(s, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn pi18n_k(ctx: &str, format: &str, kwargs: &[(&str, &str)]) -> String {
|
||||||
|
let s = pgettext(ctx, format);
|
||||||
|
kreplace(s, kwargs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singular and plural with context
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn pni18n(ctx: &str, single: &str, multiple: &str, number: u32) -> String {
|
||||||
|
npgettext(ctx, single, multiple, number)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn pni18n_f(ctx: &str, single: &str, multiple: &str, number: u32, args: &[&str]) -> String {
|
||||||
|
let s = npgettext(ctx, single, multiple, number);
|
||||||
|
freplace(s, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn pni18n_k(
|
||||||
|
ctx: &str,
|
||||||
|
single: &str,
|
||||||
|
multiple: &str,
|
||||||
|
number: u32,
|
||||||
|
kwargs: &[(&str, &str)],
|
||||||
|
) -> String {
|
||||||
|
let s = npgettext(ctx, single, multiple, number);
|
||||||
|
kreplace(s, kwargs)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn test_i18n() {
|
||||||
|
let out = i18n("translate1");
|
||||||
|
assert_eq!(out, "translate1");
|
||||||
|
|
||||||
|
let out = ni18n("translate1", "translate multi", 1);
|
||||||
|
assert_eq!(out, "translate1");
|
||||||
|
let out = ni18n("translate1", "translate multi", 2);
|
||||||
|
assert_eq!(out, "translate multi");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_i18n_f() {
|
||||||
|
let out = i18n_f("{} param", &["one"]);
|
||||||
|
assert_eq!(out, "one param");
|
||||||
|
|
||||||
|
let out = i18n_f("middle {} param", &["one"]);
|
||||||
|
assert_eq!(out, "middle one param");
|
||||||
|
|
||||||
|
let out = i18n_f("end {}", &["one"]);
|
||||||
|
assert_eq!(out, "end one");
|
||||||
|
|
||||||
|
let out = i18n_f("multiple {} and {}", &["one", "two"]);
|
||||||
|
assert_eq!(out, "multiple one and two");
|
||||||
|
|
||||||
|
let out = ni18n_f("singular {} and {}", "plural {} and {}", 2, &["one", "two"]);
|
||||||
|
assert_eq!(out, "plural one and two");
|
||||||
|
let out = ni18n_f("singular {} and {}", "plural {} and {}", 1, &["one", "two"]);
|
||||||
|
assert_eq!(out, "singular one and two");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_i18n_k() {
|
||||||
|
let out = i18n_k("{one} param", &[("one", "one")]);
|
||||||
|
assert_eq!(out, "one param");
|
||||||
|
|
||||||
|
let out = i18n_k("middle {one} param", &[("one", "one")]);
|
||||||
|
assert_eq!(out, "middle one param");
|
||||||
|
|
||||||
|
let out = i18n_k("end {one}", &[("one", "one")]);
|
||||||
|
assert_eq!(out, "end one");
|
||||||
|
|
||||||
|
let out = i18n_k("multiple {one} and {two}", &[("one", "1"), ("two", "two")]);
|
||||||
|
assert_eq!(out, "multiple 1 and two");
|
||||||
|
|
||||||
|
let out = i18n_k("multiple {two} and {one}", &[("one", "1"), ("two", "two")]);
|
||||||
|
assert_eq!(out, "multiple two and 1");
|
||||||
|
|
||||||
|
let out = i18n_k("multiple {one} and {one}", &[("one", "1"), ("two", "two")]);
|
||||||
|
assert_eq!(out, "multiple 1 and 1");
|
||||||
|
|
||||||
|
let out = ni18n_k(
|
||||||
|
"singular {one} and {two}",
|
||||||
|
"plural {one} and {two}",
|
||||||
|
1,
|
||||||
|
&[("one", "1"), ("two", "two")],
|
||||||
|
);
|
||||||
|
assert_eq!(out, "singular 1 and two");
|
||||||
|
let out = ni18n_k(
|
||||||
|
"singular {one} and {two}",
|
||||||
|
"plural {one} and {two}",
|
||||||
|
2,
|
||||||
|
&[("one", "1"), ("two", "two")],
|
||||||
|
);
|
||||||
|
assert_eq!(out, "plural 1 and two");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pi18n() {
|
||||||
|
let out = pi18n("This is the context", "translate1");
|
||||||
|
assert_eq!(out, "translate1");
|
||||||
|
|
||||||
|
let out = pni18n("context", "translate1", "translate multi", 1);
|
||||||
|
assert_eq!(out, "translate1");
|
||||||
|
let out = pni18n("The context string", "translate1", "translate multi", 2);
|
||||||
|
assert_eq!(out, "translate multi");
|
||||||
|
|
||||||
|
let out = pi18n_f("Context for translation", "{} param", &["one"]);
|
||||||
|
assert_eq!(out, "one param");
|
||||||
|
|
||||||
|
let out = pi18n_k("context", "{one} param", &[("one", "one")]);
|
||||||
|
assert_eq!(out, "one param");
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,10 +4,12 @@ use gtk::glib;
|
||||||
use libpaket::{locker::crypto::CustomerKeySeed, login::RefreshToken, LibraryError};
|
use libpaket::{locker::crypto::CustomerKeySeed, login::RefreshToken, LibraryError};
|
||||||
use secrecy::{zeroize::Zeroize, ExposeSecret, SecretBox};
|
use secrecy::{zeroize::Zeroize, ExposeSecret, SecretBox};
|
||||||
|
|
||||||
|
use crate::i18n::i18n;
|
||||||
|
|
||||||
pub static KEYRING: OnceLock<oo7::Keyring> = OnceLock::new();
|
pub static KEYRING: OnceLock<oo7::Keyring> = OnceLock::new();
|
||||||
|
|
||||||
fn get_keyring_base_attribute() -> (&'static str, &'static str) {
|
fn get_keyring_base_attribute() -> (&'static str, &'static str) {
|
||||||
("app", crate::constants::APP_ID)
|
("app", crate::config::APPLICATION_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_keyring_attributes_refresh_token() -> Vec<(&'static str, &'static str)> {
|
fn get_keyring_attributes_refresh_token() -> Vec<(&'static str, &'static str)> {
|
||||||
|
@ -60,7 +62,7 @@ pub async fn keyring_get_refresh_token() -> oo7::Result<Option<RefreshToken>> {
|
||||||
pub async fn keyring_set_refresh_token(value: String) -> oo7::Result<()> {
|
pub async fn keyring_set_refresh_token(value: String) -> oo7::Result<()> {
|
||||||
get_keyring()
|
get_keyring()
|
||||||
.create_item(
|
.create_item(
|
||||||
"Paket: Login credentials",
|
&i18n("Paket: Login credentials"),
|
||||||
&get_keyring_attributes_refresh_token(),
|
&get_keyring_attributes_refresh_token(),
|
||||||
value,
|
value,
|
||||||
true,
|
true,
|
||||||
|
@ -168,7 +170,7 @@ pub async fn keyring_set_packstation(data: &CustomerKeySeed) -> KeyringResult<()
|
||||||
|
|
||||||
Ok(get_keyring()
|
Ok(get_keyring()
|
||||||
.create_item(
|
.create_item(
|
||||||
"Paket: Device keys",
|
&i18n("Paket: Device keys for parcel locker service"),
|
||||||
&get_keyring_attributes_packstation(),
|
&get_keyring_attributes_packstation(),
|
||||||
string.expose_secret(),
|
string.expose_secret(),
|
||||||
true,
|
true,
|
47
src/lib.rs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
pub mod account;
|
||||||
|
pub mod advice;
|
||||||
|
pub mod advices;
|
||||||
|
pub mod app;
|
||||||
|
pub mod i18n;
|
||||||
|
pub mod keyring;
|
||||||
|
//pub mod lockers;
|
||||||
|
pub mod login;
|
||||||
|
pub mod packstation;
|
||||||
|
pub mod ready;
|
||||||
|
pub mod scanner;
|
||||||
|
pub mod tracking;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
pub mod config {
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/config.rs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init() {
|
||||||
|
config::init();
|
||||||
|
|
||||||
|
use config::{APPLICATION_ID, GETTEXT_PACKAGE, LOCALEDIR};
|
||||||
|
use gettext::{bind_textdomain_codeset, bindtextdomain, setlocale, textdomain, LocaleCategory};
|
||||||
|
|
||||||
|
aperture::init(APPLICATION_ID);
|
||||||
|
|
||||||
|
relm4::RELM_THREADS.set(4).unwrap();
|
||||||
|
|
||||||
|
//debug!("Setting up locale data");
|
||||||
|
setlocale(LocaleCategory::LcAll, "");
|
||||||
|
|
||||||
|
bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR).expect("Unable to bind the text domain");
|
||||||
|
bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8")
|
||||||
|
.expect("Unable to set the text domain encoding");
|
||||||
|
textdomain(GETTEXT_PACKAGE).expect("Unable to switch to the text domain");
|
||||||
|
|
||||||
|
let display = gtk::gdk::Display::default().unwrap();
|
||||||
|
let theme: gtk::IconTheme = gtk::IconTheme::for_display(&display);
|
||||||
|
theme.add_resource_path("/de/j4ne/Paket/icons");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use login::LoginSharedState;
|
||||||
|
|
||||||
|
pub static LOGIN_BROKER: relm4::MessageBroker<login::LoginInput> = relm4::MessageBroker::new();
|
||||||
|
pub fn send_log_out() {
|
||||||
|
LOGIN_BROKER.send(login::LoginInput::LogOut);
|
||||||
|
}
|
273
src/lockers.rs
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
use std::borrow::Borrow;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
|
use bluer::monitor::Monitor;
|
||||||
|
use bluer::DiscoveryFilter;
|
||||||
|
use channel::mpsc::UnboundedReceiver;
|
||||||
|
use channel::mpsc::UnboundedSender;
|
||||||
|
use futures::*;
|
||||||
|
use libpaket::locker::LockerDevice as LockerDeviceMetadata;
|
||||||
|
use libpaket::locker::LockerServiceUUID;
|
||||||
|
use relm4::prelude::*;
|
||||||
|
use relm4::{Component, ComponentSender};
|
||||||
|
|
||||||
|
pub enum LockerDeviceOutput {
|
||||||
|
Lost(LockerServiceUUID),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum LockerDeviceCommand {}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LockerDevice {
|
||||||
|
device: bluer::Device,
|
||||||
|
metadata: LockerDeviceMetadata,
|
||||||
|
rx: Arc<UnboundedReceiver<LockerDeviceOutput>>,
|
||||||
|
tx: Arc<RwLock<UnboundedSender<LockerDeviceOutput>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Locker {
|
||||||
|
arc: Arc<Mutex<Option<LockerDevice>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Locker {
|
||||||
|
fn new(device: LockerDevice) -> Self {
|
||||||
|
Self {
|
||||||
|
arc: Arc::new(Mutex::new(Some(device))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_available(&self) -> bool {
|
||||||
|
let lock = self.arc.lock().unwrap();
|
||||||
|
lock.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_device(&self) -> Option<bluer::Device> {
|
||||||
|
let lock = self.arc.lock().unwrap();
|
||||||
|
let locker = lock.as_ref()?;
|
||||||
|
Some(locker.device.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_receiver(&self) -> Option<Arc<UnboundedReceiver<LockerDeviceOutput>>> {
|
||||||
|
let lock = self.arc.lock().unwrap();
|
||||||
|
let locker = lock.as_ref()?;
|
||||||
|
Some(locker.rx.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_service_uuid(&self) -> Option<LockerServiceUUID> {
|
||||||
|
let lock = self.arc.lock().unwrap();
|
||||||
|
let locker = lock.as_ref()?;
|
||||||
|
Some(locker.metadata.service_uuid.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn emit_lost(&self) {
|
||||||
|
let locker = self.arc.lock().unwrap().take().unwrap();
|
||||||
|
|
||||||
|
let tx = locker.tx.clone();
|
||||||
|
let mut tx = tx.write().unwrap();
|
||||||
|
tx.send(LockerDeviceOutput::Lost(locker.metadata.service_uuid))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsafe impl Send for Locker {}
|
||||||
|
// unsafe impl Sync for Locker {}
|
||||||
|
|
||||||
|
impl LockerDevice {
|
||||||
|
fn new(
|
||||||
|
adapter: &bluer::Adapter,
|
||||||
|
address: &bluer::Address,
|
||||||
|
metadata: LockerDeviceMetadata,
|
||||||
|
) -> Self {
|
||||||
|
let device = adapter.device(address.clone()).unwrap();
|
||||||
|
|
||||||
|
let (tx, rx) = futures::channel::mpsc::unbounded::<LockerDeviceOutput>();
|
||||||
|
|
||||||
|
LockerDevice {
|
||||||
|
device,
|
||||||
|
metadata,
|
||||||
|
rx: Arc::new(rx),
|
||||||
|
tx: Arc::new(RwLock::new(tx)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LockerManager {
|
||||||
|
session: bluer::Session,
|
||||||
|
|
||||||
|
adapter: Option<bluer::Adapter>,
|
||||||
|
lockers: HashMap<bluer::Address, Locker>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum LockerScannerInput {
|
||||||
|
SetAdapter(bluer::Adapter),
|
||||||
|
Scan,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum LockerManagerCommands {
|
||||||
|
LockerFound((bluer::Address, LockerDeviceMetadata)),
|
||||||
|
LockerLost(bluer::Address),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncComponent for LockerManager {
|
||||||
|
type Init = ();
|
||||||
|
type Input = LockerScannerInput;
|
||||||
|
type Output = Locker;
|
||||||
|
type Root = ();
|
||||||
|
type CommandOutput = LockerManagerCommands;
|
||||||
|
type Widgets = ();
|
||||||
|
|
||||||
|
fn init_root() -> Self::Root {
|
||||||
|
()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init(
|
||||||
|
_: Self::Init,
|
||||||
|
_: Self::Root,
|
||||||
|
sender: AsyncComponentSender<Self>,
|
||||||
|
) -> AsyncComponentParts<Self> {
|
||||||
|
let session = bluer::Session::new().await.unwrap();
|
||||||
|
|
||||||
|
let model = Self {
|
||||||
|
session,
|
||||||
|
adapter: None,
|
||||||
|
lockers: HashMap::with_capacity(1),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(res) = model.session.default_adapter().await {
|
||||||
|
sender.input(LockerScannerInput::SetAdapter(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncComponentParts { model, widgets: () }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
&mut self,
|
||||||
|
msg: Self::Input,
|
||||||
|
sender: AsyncComponentSender<Self>,
|
||||||
|
_: &Self::Root,
|
||||||
|
) {
|
||||||
|
match msg {
|
||||||
|
LockerScannerInput::Scan => {
|
||||||
|
// TODO: move that into sender.command
|
||||||
|
// for some reason that didn't work
|
||||||
|
let adapter = self.adapter.as_ref().unwrap().clone();
|
||||||
|
let uuids = HashSet::new();
|
||||||
|
println!("A");
|
||||||
|
if let Err(_) = adapter
|
||||||
|
.set_discovery_filter(DiscoveryFilter {
|
||||||
|
uuids,
|
||||||
|
rssi: None,
|
||||||
|
pathloss: None,
|
||||||
|
transport: bluer::DiscoveryTransport::Le,
|
||||||
|
duplicate_data: false,
|
||||||
|
discoverable: false,
|
||||||
|
pattern: None,
|
||||||
|
_non_exhaustive: (),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
println!("B");
|
||||||
|
let Ok(mut discover) = adapter.discover_devices().await else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
while let Some(evt) = discover.next().await {
|
||||||
|
println!("E: {:?}", evt);
|
||||||
|
match evt {
|
||||||
|
bluer::AdapterEvent::DeviceAdded(addr) => {
|
||||||
|
let device = adapter.device(addr).unwrap();
|
||||||
|
println!("device: {:?}", device);
|
||||||
|
let Some(service_data) = device.service_data().await.unwrap() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for (uuid, data) in service_data.iter() {
|
||||||
|
let Ok(locker_service_uuid) =
|
||||||
|
LockerServiceUUID::try_from(uuid.clone())
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let locker =
|
||||||
|
LockerDeviceMetadata::new(locker_service_uuid, data).unwrap();
|
||||||
|
sender
|
||||||
|
.command_sender()
|
||||||
|
.emit(LockerManagerCommands::LockerFound((addr, locker)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bluer::AdapterEvent::DeviceRemoved(_) => (),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LockerScannerInput::SetAdapter(adapter) => match adapter.is_powered().await {
|
||||||
|
Ok(powered) => {
|
||||||
|
if powered {
|
||||||
|
println!("Adapter: already turned on.");
|
||||||
|
self.adapter = Some(adapter);
|
||||||
|
sender.input(LockerScannerInput::Scan);
|
||||||
|
} else {
|
||||||
|
println!("Adapter: trying to turn on.");
|
||||||
|
match adapter.set_powered(true).await {
|
||||||
|
Ok(_) => {
|
||||||
|
println!("Adapter: turned on");
|
||||||
|
self.adapter = Some(adapter);
|
||||||
|
sender.input(LockerScannerInput::Scan);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("Adapter: not turned on, error: {:?}", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_cmd(
|
||||||
|
&mut self,
|
||||||
|
message: Self::CommandOutput,
|
||||||
|
sender: AsyncComponentSender<Self>,
|
||||||
|
_: &Self::Root,
|
||||||
|
) {
|
||||||
|
match message {
|
||||||
|
LockerManagerCommands::LockerFound((addr, metadata)) => {
|
||||||
|
for (iter_addr, _) in &self.lockers {
|
||||||
|
if *iter_addr == addr {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let locker = LockerDevice::new(self.adapter.as_ref().unwrap(), &addr, metadata);
|
||||||
|
let locker = Locker::new(locker);
|
||||||
|
self.lockers.insert(addr.clone(), locker.clone());
|
||||||
|
|
||||||
|
let adapter = self.adapter.as_ref().unwrap().clone();
|
||||||
|
|
||||||
|
sender.command(move |sender, shutdown| {
|
||||||
|
shutdown
|
||||||
|
.register(async move {
|
||||||
|
let device = adapter.device(addr).unwrap();
|
||||||
|
let mut events = device.events().await.unwrap();
|
||||||
|
while let Some(_) = events.next().await {}
|
||||||
|
sender.emit(LockerManagerCommands::LockerLost(addr));
|
||||||
|
})
|
||||||
|
.drop_on_shutdown()
|
||||||
|
});
|
||||||
|
|
||||||
|
sender.output(locker).unwrap();
|
||||||
|
}
|
||||||
|
LockerManagerCommands::LockerLost(addr) => {
|
||||||
|
let locker = self.lockers.remove(&addr).unwrap();
|
||||||
|
locker.emit_lost().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,11 @@ use relm4::{
|
||||||
};
|
};
|
||||||
use webkit::{prelude::WebViewExt, URIRequest, WebContext, WebView};
|
use webkit::{prelude::WebViewExt, URIRequest, WebContext, WebView};
|
||||||
|
|
||||||
use crate::keyring::{keyring_get_refresh_token, keyring_is_available, keyring_set_refresh_token};
|
use crate::{
|
||||||
|
i18n::i18n,
|
||||||
|
keyring::{keyring_get_refresh_token, keyring_is_available, keyring_set_refresh_token},
|
||||||
|
utils::status_page,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum LoginInput {
|
pub enum LoginInput {
|
||||||
|
@ -109,7 +113,7 @@ impl AsyncComponent for Login {
|
||||||
add = &adw::Bin {
|
add = &adw::Bin {
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_child = &adw::StatusPage {
|
set_child = &adw::StatusPage {
|
||||||
set_title: "Welcome to Paket!",
|
set_title: &i18n("Welcome to Paket!"),
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_child = &adw::Clamp {
|
set_child = &adw::Clamp {
|
||||||
set_maximum_size: 260,
|
set_maximum_size: 260,
|
||||||
|
@ -119,7 +123,7 @@ impl AsyncComponent for Login {
|
||||||
add_css_class: relm4::css::SUGGESTED_ACTION,
|
add_css_class: relm4::css::SUGGESTED_ACTION,
|
||||||
add_css_class: relm4::css::PILL,
|
add_css_class: relm4::css::PILL,
|
||||||
|
|
||||||
set_label: "Log In",
|
set_label: &i18n("Log In"),
|
||||||
|
|
||||||
connect_clicked => LoginInput::ConsentToLogin,
|
connect_clicked => LoginInput::ConsentToLogin,
|
||||||
},
|
},
|
||||||
|
@ -150,9 +154,7 @@ impl AsyncComponent for Login {
|
||||||
},
|
},
|
||||||
|
|
||||||
#[name = "page_offline"]
|
#[name = "page_offline"]
|
||||||
add = &adw::Bin {
|
add = &status_page(&i18n("Internet unavailable"), None, None) -> adw::StatusPage {},
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
#[track(model.changed_state())]
|
#[track(model.changed_state())]
|
||||||
set_visible_child: {
|
set_visible_child: {
|
49
src/meson.build
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
# SPDX-FileCopyrightText: 2022 Emmanuele Bassi
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name()
|
||||||
|
|
||||||
|
cargo_env = {
|
||||||
|
'PAKET_VERSION': '@0@@1@'.format(meson.project_version(), version_suffix),
|
||||||
|
'PAKET_GETTEXT_PACKAGE': 'paket',
|
||||||
|
'PAKET_LOCALEDIR': get_option('prefix') / get_option('localedir'),
|
||||||
|
'PAKET_PKGDATADIR': pkgdatadir,
|
||||||
|
'PAKET_APPLICATION_ID': application_id,
|
||||||
|
'PAKET_PROFILE': get_option('profile'),
|
||||||
|
}
|
||||||
|
|
||||||
|
cargo_options = [ '--manifest-path', meson.project_source_root() / 'Cargo.toml' ]
|
||||||
|
cargo_options += [ '--target-dir', meson.project_build_root() / 'src' ]
|
||||||
|
cargo_options += [ '--bin', meson.project_name() ]
|
||||||
|
|
||||||
|
cargo_cmd = [ cargo ]
|
||||||
|
if get_option('profile') == 'default'
|
||||||
|
cargo_options += [ '--release' ]
|
||||||
|
cargo_cmd += [ 'auditable' ]
|
||||||
|
rust_target = 'release'
|
||||||
|
message('Building in release mode')
|
||||||
|
else
|
||||||
|
rust_target = 'debug'
|
||||||
|
message('Building in debug mode')
|
||||||
|
endif
|
||||||
|
|
||||||
|
cargo_env = {
|
||||||
|
'CARGO_HOME': meson.project_build_root() / 'cargo-home',
|
||||||
|
'PAKET_BUILT_BY': 'meson',
|
||||||
|
}
|
||||||
|
|
||||||
|
cargo_release = custom_target(
|
||||||
|
'cargo-build',
|
||||||
|
build_by_default: true,
|
||||||
|
output: meson.project_name(),
|
||||||
|
console: true,
|
||||||
|
install: true,
|
||||||
|
install_dir: get_option('bindir'),
|
||||||
|
env: cargo_env,
|
||||||
|
command: [
|
||||||
|
cargo_cmd, 'build', '--locked',
|
||||||
|
cargo_options,
|
||||||
|
'&&',
|
||||||
|
'cp', 'src' / rust_target / meson.project_name(), '@OUTPUT@',
|
||||||
|
],
|
||||||
|
)
|
|
@ -4,6 +4,7 @@ use relm4::prelude::*;
|
||||||
use relm4::{Component, ComponentParts, WidgetRef};
|
use relm4::{Component, ComponentParts, WidgetRef};
|
||||||
use secrecy::{ExposeSecret, SecretBox};
|
use secrecy::{ExposeSecret, SecretBox};
|
||||||
|
|
||||||
|
use crate::i18n::i18n;
|
||||||
use crate::keyring::{keyring_get_packstation, keyring_set_packstation};
|
use crate::keyring::{keyring_get_packstation, keyring_set_packstation};
|
||||||
use crate::login::get_id_token;
|
use crate::login::get_id_token;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -78,8 +79,8 @@ impl Component for PackstationView {
|
||||||
add = page_registration = &adw::ViewStack {
|
add = page_registration = &adw::ViewStack {
|
||||||
#[name = "registration_page_beginning"]
|
#[name = "registration_page_beginning"]
|
||||||
add = &adw::StatusPage {
|
add = &adw::StatusPage {
|
||||||
set_title: "Register your device",
|
set_title: &i18n("Register your device"),
|
||||||
set_description: Some("Registration is required to interact with parcel lockers\nYou'll need camera access and the letter with the registration qr-code you received. If you don't have one, request the <a href=\"https://www.dhl.de/de/privatkunden/pakete-empfangen/an-einem-abholort-empfangen/packstation/packstation-registrierung.html\">Packstation service</a> and come back later."),
|
set_description: Some(&i18n("Registration is required to interact with parcel lockers\nYou'll need camera access and the letter with the registration qr-code you received. If you don't have one, request the <a href=\"https://www.dhl.de/de/privatkunden/pakete-empfangen/an-einem-abholort-empfangen/packstation/packstation-registrierung.html\">Packstation service</a> and come back later.")),
|
||||||
|
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_child = &adw::Clamp {
|
set_child = &adw::Clamp {
|
||||||
|
@ -92,8 +93,8 @@ impl Component for PackstationView {
|
||||||
|
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_child = &adw::ButtonContent {
|
set_child = &adw::ButtonContent {
|
||||||
set_label: "Register",
|
set_label: &i18n("Register"),
|
||||||
set_icon_name: relm4_icons::icon_names::QR_CODE_SCANNER,
|
set_icon_name: "qr-code-scanner-symbolic",
|
||||||
},
|
},
|
||||||
|
|
||||||
connect_clicked[sender = sender.clone()] => move |_| {
|
connect_clicked[sender = sender.clone()] => move |_| {
|
||||||
|
@ -108,7 +109,7 @@ impl Component for PackstationView {
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_paintable = &adw::SpinnerPaintable::new(Some(registration_page_loading.widget_ref())) {},
|
set_paintable = &adw::SpinnerPaintable::new(Some(registration_page_loading.widget_ref())) {},
|
||||||
|
|
||||||
set_title: "Registering your device"
|
set_title: &i18n("Registering your device"),
|
||||||
},
|
},
|
||||||
|
|
||||||
#[track(model.changed_state())]
|
#[track(model.changed_state())]
|
||||||
|
@ -197,7 +198,7 @@ impl Component for PackstationView {
|
||||||
Ok(value) => {
|
Ok(value) => {
|
||||||
keyring_set_packstation(&value).await.unwrap();
|
keyring_set_packstation(&value).await.unwrap();
|
||||||
PackstationViewCommand::GotDeviceCredentials(value)
|
PackstationViewCommand::GotDeviceCredentials(value)
|
||||||
},
|
}
|
||||||
Err(err) => todo!(),
|
Err(err) => todo!(),
|
||||||
},
|
},
|
||||||
Err(err) => todo!(),
|
Err(err) => todo!(),
|
||||||
|
@ -223,20 +224,18 @@ impl Component for PackstationView {
|
||||||
}
|
}
|
||||||
PackstationViewCommand::GotNothing => {
|
PackstationViewCommand::GotNothing => {
|
||||||
self.set_state(State::RegisterWizard(RegisterState::Beginning));
|
self.set_state(State::RegisterWizard(RegisterState::Beginning));
|
||||||
},
|
}
|
||||||
PackstationViewCommand::GotAPIRegisterError(error) => {
|
PackstationViewCommand::GotAPIRegisterError(error) => {
|
||||||
todo!()
|
todo!()
|
||||||
},
|
}
|
||||||
PackstationViewCommand::GotLibraryError(error) => {
|
PackstationViewCommand::GotLibraryError(error) => match error {
|
||||||
match error {
|
libpaket::LibraryError::Unauthorized => todo!(),
|
||||||
libpaket::LibraryError::Unauthorized => todo!(),
|
libpaket::LibraryError::DecodeError(_) => todo!(),
|
||||||
libpaket::LibraryError::DecodeError(_) => todo!(),
|
libpaket::LibraryError::APIChange => todo!(),
|
||||||
libpaket::LibraryError::APIChange => todo!(),
|
libpaket::LibraryError::Deprecated => todo!(),
|
||||||
libpaket::LibraryError::Deprecated => todo!(),
|
|
||||||
|
|
||||||
libpaket::LibraryError::InvalidArgument(error) => panic!("{}", error),
|
libpaket::LibraryError::InvalidArgument(error) => panic!("{}", error),
|
||||||
libpaket::LibraryError::NetworkFetch => panic!(),
|
libpaket::LibraryError::NetworkFetch => panic!(),
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,12 @@
|
||||||
|
use crate::i18n::i18n;
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use relm4::prelude::*;
|
use relm4::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
account::{AccountOutput, AccountServices, AccountView}, advices::{AdvicesView, AdvicesViewInput}, packstation::{PackstationView, PackstationViewInput, PackstationViewOutput}, tracking::{TrackingInput, TrackingOutput, TrackingView}
|
account::{AccountOutput, AccountServices, AccountView},
|
||||||
|
advices::{AdvicesView, AdvicesViewInput},
|
||||||
|
packstation::{PackstationView, PackstationViewInput, PackstationViewOutput},
|
||||||
|
tracking::{TrackingInput, TrackingOutput, TrackingView},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tracker::track]
|
#[tracker::track]
|
||||||
|
@ -73,14 +77,14 @@ impl Component for Ready {
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
#[name = "ready_view_stack"]
|
#[name = "ready_view_stack"]
|
||||||
set_child = &adw::ViewStack {
|
set_child = &adw::ViewStack {
|
||||||
add = &model.advices_component.widget().clone() -> gtk::ScrolledWindow {
|
add = &model.advices_component.widget().clone() -> adw::ViewStack {
|
||||||
#[track(model.changed_have_service_advices())]
|
#[track(model.changed_have_service_advices())]
|
||||||
set_visible: model.have_service_advices,
|
set_visible: model.have_service_advices,
|
||||||
|
|
||||||
} -> page_advices: adw::ViewStackPage {
|
} -> page_advices: adw::ViewStackPage {
|
||||||
set_title: Some("Mail notification"),
|
set_title: Some(&i18n("Mail")),
|
||||||
set_name: Some("page_advices"),
|
set_name: Some(&i18n("page_advices")),
|
||||||
set_icon_name: Some(relm4_icons::icon_names::MAIL),
|
set_icon_name: Some(&i18n("mail-symbolic")),
|
||||||
|
|
||||||
#[track(model.changed_have_service_advices())]
|
#[track(model.changed_have_service_advices())]
|
||||||
set_visible: model.have_service_advices,
|
set_visible: model.have_service_advices,
|
||||||
|
@ -90,9 +94,9 @@ impl Component for Ready {
|
||||||
#[track(model.changed_have_service_tracking())]
|
#[track(model.changed_have_service_tracking())]
|
||||||
set_visible: model.have_service_tracking,
|
set_visible: model.have_service_tracking,
|
||||||
} -> page_tracking: adw::ViewStackPage {
|
} -> page_tracking: adw::ViewStackPage {
|
||||||
set_title: Some("Shipment tracking"),
|
set_title: Some(&i18n("Tracking")),
|
||||||
set_name: Some("page_tracking"),
|
set_name: Some("page_tracking"),
|
||||||
set_icon_name: Some(relm4_icons::icon_names::PACKAGE_X_GENERIC),
|
set_icon_name: Some("package-x-generic-symbolic"),
|
||||||
|
|
||||||
#[track(model.changed_have_service_tracking())]
|
#[track(model.changed_have_service_tracking())]
|
||||||
set_visible: model.have_service_tracking,
|
set_visible: model.have_service_tracking,
|
||||||
|
@ -102,8 +106,9 @@ impl Component for Ready {
|
||||||
#[track(model.changed_have_service_packstation())]
|
#[track(model.changed_have_service_packstation())]
|
||||||
set_visible: model.have_service_packstation,
|
set_visible: model.have_service_packstation,
|
||||||
} -> page_packstation: adw::ViewStackPage {
|
} -> page_packstation: adw::ViewStackPage {
|
||||||
set_title: Some("Packstation"),
|
set_title: Some(&i18n("Locker")),
|
||||||
set_name: Some("page_packstation"),
|
set_name: Some("page_packstation"),
|
||||||
|
set_icon_name: Some("parcel-locker-symbolic"),
|
||||||
|
|
||||||
#[track(model.changed_have_service_packstation())]
|
#[track(model.changed_have_service_packstation())]
|
||||||
set_visible: model.have_service_packstation,
|
set_visible: model.have_service_packstation,
|
||||||
|
@ -111,9 +116,9 @@ impl Component for Ready {
|
||||||
|
|
||||||
add = &model.account_component.widget().clone() -> adw::Bin {
|
add = &model.account_component.widget().clone() -> adw::Bin {
|
||||||
} -> page_account: adw::ViewStackPage {
|
} -> page_account: adw::ViewStackPage {
|
||||||
set_title: Some("Account"),
|
set_title: Some(&i18n("Account")),
|
||||||
set_name: Some("page_account"),
|
set_name: Some("page_account"),
|
||||||
set_icon_name: Some(relm4_icons::icon_names::PERSON)
|
set_icon_name: Some("person-symbolic")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -205,7 +210,7 @@ impl Component for Ready {
|
||||||
}
|
}
|
||||||
AccountServices::SendungVerfolgung => {
|
AccountServices::SendungVerfolgung => {
|
||||||
self.set_have_service_tracking(true);
|
self.set_have_service_tracking(true);
|
||||||
self.tracking_component.emit(TrackingInput::Search(None))
|
self.tracking_component.emit(TrackingInput::Init);
|
||||||
}
|
}
|
||||||
AccountServices::PackstationAvailable => {
|
AccountServices::PackstationAvailable => {
|
||||||
self.set_have_service_packstation(true);
|
self.set_have_service_packstation(true);
|
||||||
|
@ -213,6 +218,7 @@ impl Component for Ready {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ReadyInput::ServiceBorked(service) => match service {
|
ReadyInput::ServiceBorked(service) => match service {
|
||||||
|
// TODO: localization
|
||||||
AccountServices::Advices => {
|
AccountServices::Advices => {
|
||||||
self.toast_overlay.add_toast(
|
self.toast_overlay.add_toast(
|
||||||
adw::Toast::builder()
|
adw::Toast::builder()
|
||||||
|
@ -221,7 +227,7 @@ impl Component for Ready {
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
self.set_have_service_advices(false);
|
self.set_have_service_advices(false);
|
||||||
},
|
}
|
||||||
AccountServices::SendungVerfolgung => {
|
AccountServices::SendungVerfolgung => {
|
||||||
self.toast_overlay.add_toast(
|
self.toast_overlay.add_toast(
|
||||||
adw::Toast::builder()
|
adw::Toast::builder()
|
||||||
|
@ -230,7 +236,7 @@ impl Component for Ready {
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
self.set_have_service_tracking(false);
|
self.set_have_service_tracking(false);
|
||||||
},
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
},
|
},
|
||||||
ReadyInput::NavigationPageTemp(page) => {
|
ReadyInput::NavigationPageTemp(page) => {
|
||||||
|
@ -256,6 +262,7 @@ impl Component for Ready {
|
||||||
fn convert_tracking_output(value: TrackingOutput) -> ReadyInput {
|
fn convert_tracking_output(value: TrackingOutput) -> ReadyInput {
|
||||||
match value {
|
match value {
|
||||||
TrackingOutput::Borked => ReadyInput::ServiceBorked(AccountServices::SendungVerfolgung),
|
TrackingOutput::Borked => ReadyInput::ServiceBorked(AccountServices::SendungVerfolgung),
|
||||||
|
_ => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,6 +276,8 @@ fn convert_account_output(value: AccountOutput) -> ReadyInput {
|
||||||
|
|
||||||
fn convert_packstation_output(value: PackstationViewOutput) -> ReadyInput {
|
fn convert_packstation_output(value: PackstationViewOutput) -> ReadyInput {
|
||||||
match value {
|
match value {
|
||||||
PackstationViewOutput::NavigationPage(navigation_page) => ReadyInput::NavigationPageTemp(navigation_page),
|
PackstationViewOutput::NavigationPage(navigation_page) => {
|
||||||
|
ReadyInput::NavigationPageTemp(navigation_page)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
pub use r#impl::{Scanner, ScannerOutput};
|
pub use r#impl::{Scanner, ScannerInput, ScannerOutput};
|
||||||
|
|
||||||
mod r#impl {
|
mod r#impl {
|
||||||
|
use crate::i18n::{i18n, i18n_f};
|
||||||
|
use crate::utils;
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use relm4::prelude::*;
|
use relm4::prelude::*;
|
||||||
use relm4::WidgetRef;
|
use relm4::WidgetRef;
|
||||||
|
@ -39,6 +41,8 @@ mod r#impl {
|
||||||
pub struct Scanner {
|
pub struct Scanner {
|
||||||
state: State,
|
state: State,
|
||||||
in_activation: bool,
|
in_activation: bool,
|
||||||
|
#[no_eq]
|
||||||
|
error: Option<ErrorInternal>,
|
||||||
|
|
||||||
#[do_not_track]
|
#[do_not_track]
|
||||||
camera: Option<aperture::Camera>,
|
camera: Option<aperture::Camera>,
|
||||||
|
@ -95,13 +99,8 @@ mod r#impl {
|
||||||
add = &page_view_finder -> aperture::Viewfinder {
|
add = &page_view_finder -> aperture::Viewfinder {
|
||||||
set_detect_codes: true,
|
set_detect_codes: true,
|
||||||
|
|
||||||
connect_code_detected[sender = sender.clone()] => move |_, code_type, value| {
|
connect_code_detected[sender = sender.clone()] => move |_, bytes| {
|
||||||
match code_type {
|
println!("Got connect_code_detected");
|
||||||
aperture::CodeType::Qr => {
|
|
||||||
let _ = sender.output(ScannerOutput::CodeDetected(value.to_string()));
|
|
||||||
},
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
connect_state_notify[sender = sender.clone()] => move |view_finder|{
|
connect_state_notify[sender = sender.clone()] => move |view_finder|{
|
||||||
|
@ -119,17 +118,19 @@ mod r#impl {
|
||||||
#[name = "page_error"]
|
#[name = "page_error"]
|
||||||
add = &adw::Bin {
|
add = &adw::Bin {
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_child = &adw::StatusPage {
|
set_child = &utils::status_page_with_back_button("An error occured", None) -> adw::StatusPage {
|
||||||
set_title: "Error occured",
|
#[track = "model.changed_error() && model.get_error().is_some()"]
|
||||||
|
set_title?: &model.error.as_ref().map(|error| error.title()),
|
||||||
|
#[track = "model.changed_error() && model.get_error().is_some()"]
|
||||||
|
set_description: model.error.as_ref().map(|error| error.description()).as_deref(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
#[name = "page_no_cameras"]
|
#[name = "page_no_cameras"]
|
||||||
add = &adw::Bin {
|
add = &adw::Bin {
|
||||||
#[wrap(Some)]
|
set_child: Some(
|
||||||
set_child = &adw::StatusPage {
|
&utils::status_page_with_back_button("No cameras found", None)
|
||||||
set_title: "No cameras detected",
|
),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
#[track(model.changed_state())]
|
#[track(model.changed_state())]
|
||||||
|
@ -169,6 +170,7 @@ mod r#impl {
|
||||||
let model = Scanner {
|
let model = Scanner {
|
||||||
state: State::Nothing,
|
state: State::Nothing,
|
||||||
view_finder,
|
view_finder,
|
||||||
|
error: None,
|
||||||
camera: None,
|
camera: None,
|
||||||
in_activation: false,
|
in_activation: false,
|
||||||
tracker: 0,
|
tracker: 0,
|
||||||
|
@ -301,4 +303,32 @@ mod r#impl {
|
||||||
ErrorInternal::Provider(value)
|
ErrorInternal::Provider(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ErrorInternal {
|
||||||
|
fn title(&self) -> String {
|
||||||
|
match self {
|
||||||
|
ErrorInternal::Pipewire(error) => match error {
|
||||||
|
aperture::PipewireError::OldVersion => i18n("Pipewire needs upgrading"),
|
||||||
|
aperture::PipewireError::ProvidedStarted => i18n("Internal error"),
|
||||||
|
},
|
||||||
|
ErrorInternal::Provider(error) => match error {
|
||||||
|
aperture::ProviderError::MissingPlugin(_) => i18n("A plugin is missing"),
|
||||||
|
aperture::ProviderError::BoolError(_) => i18n("External error"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self) -> String {
|
||||||
|
match self {
|
||||||
|
ErrorInternal::Pipewire(error) => match error {
|
||||||
|
aperture::PipewireError::OldVersion => i18n("The current Pipewire version is too old to use and must be upgraded"),
|
||||||
|
aperture::PipewireError::ProvidedStarted => i18n("The aperture::DeviceProvider was already started. This is a logic bug, please report this issue."),
|
||||||
|
},
|
||||||
|
ErrorInternal::Provider(error) => match error {
|
||||||
|
aperture::ProviderError::MissingPlugin(plugin) => i18n_f("Please install `{}` to use the Scanner", &[&plugin]),
|
||||||
|
aperture::ProviderError::BoolError(bool_error) => i18n_f("Programming error: {}", &[&bool_error.message]),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -4,16 +4,23 @@ use libpaket::{LibraryError, LibraryResult};
|
||||||
use relm4::factory::{FactoryComponent, FactoryHashMap};
|
use relm4::factory::{FactoryComponent, FactoryHashMap};
|
||||||
use relm4::prelude::*;
|
use relm4::prelude::*;
|
||||||
|
|
||||||
|
use crate::i18n::{i18n, i18n_f};
|
||||||
use crate::login::get_id_token;
|
use crate::login::get_id_token;
|
||||||
use crate::LoginSharedState;
|
use crate::LoginSharedState;
|
||||||
|
|
||||||
|
#[tracker::track]
|
||||||
pub struct TrackingView {
|
pub struct TrackingView {
|
||||||
|
#[do_not_track]
|
||||||
factory: FactoryHashMap<String, crate::tracking::ShipmentView>,
|
factory: FactoryHashMap<String, crate::tracking::ShipmentView>,
|
||||||
|
#[do_not_track]
|
||||||
login: LoginSharedState,
|
login: LoginSharedState,
|
||||||
|
|
||||||
|
is_init: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum TrackingInput {
|
pub enum TrackingInput {
|
||||||
|
Init,
|
||||||
Search(Option<String>),
|
Search(Option<String>),
|
||||||
Notification(String),
|
Notification(String),
|
||||||
Reset,
|
Reset,
|
||||||
|
@ -27,6 +34,7 @@ pub enum TrackingCmds {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum TrackingOutput {
|
pub enum TrackingOutput {
|
||||||
Borked,
|
Borked,
|
||||||
|
NavigationPageTemp(adw::NavigationPage),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[relm4::component(pub)]
|
#[relm4::component(pub)]
|
||||||
|
@ -40,27 +48,24 @@ impl Component for TrackingView {
|
||||||
#[root]
|
#[root]
|
||||||
adw::ToastOverlay {
|
adw::ToastOverlay {
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_child = >k::ScrolledWindow {
|
set_child = >k::Box {
|
||||||
set_propagate_natural_width: true,
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
#[wrap(Some)]
|
|
||||||
set_child = &adw::Clamp {
|
gtk::ScrolledWindow {
|
||||||
set_maximum_size: 1800,
|
set_propagate_natural_width: true,
|
||||||
|
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_child = >k::Box {
|
set_child = &adw::Clamp {
|
||||||
set_orientation: gtk::Orientation::Vertical,
|
#[wrap(Some)]
|
||||||
set_margin_start: 6,
|
set_child = >k::Box {
|
||||||
set_margin_end: 6,
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
|
set_margin_start: 6,
|
||||||
|
set_margin_end: 6,
|
||||||
|
|
||||||
set_margin_bottom: 12,
|
gtk::Box {
|
||||||
|
|
||||||
adw::Clamp {
|
|
||||||
#[wrap(Some)]
|
|
||||||
set_child = >k::Box {
|
|
||||||
set_orientation: gtk::Orientation::Horizontal,
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
set_margin_all: 2,
|
add_css_class: relm4::css::LINKED,
|
||||||
|
set_margin_bottom: 12,
|
||||||
add_css_class: relm4::css::TOOLBAR,
|
|
||||||
|
|
||||||
#[name = "tracking_entry"]
|
#[name = "tracking_entry"]
|
||||||
gtk::Entry {
|
gtk::Entry {
|
||||||
|
@ -70,21 +75,22 @@ impl Component for TrackingView {
|
||||||
|
|
||||||
#[name = "tracking_entry_button"]
|
#[name = "tracking_entry_button"]
|
||||||
gtk::Button {
|
gtk::Button {
|
||||||
set_icon_name: relm4_icons::icon_names::LOUPE_LARGE
|
set_icon_name: "loupe-large-symbolic",
|
||||||
|
add_css_class: relm4::css::RAISED,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
#[local_ref]
|
#[local_ref]
|
||||||
tracking_box -> gtk::Box {
|
tracking_box -> gtk::ListBox {
|
||||||
set_spacing: 8,
|
add_css_class: relm4::css::BOXED_LIST,
|
||||||
set_orientation: gtk::Orientation::Vertical,
|
set_selection_mode: gtk::SelectionMode::None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,6 +104,8 @@ impl Component for TrackingView {
|
||||||
let model = TrackingView {
|
let model = TrackingView {
|
||||||
factory,
|
factory,
|
||||||
login: init,
|
login: init,
|
||||||
|
tracker: 0,
|
||||||
|
is_init: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let tracking_box = model.factory.widget();
|
let tracking_box = model.factory.widget();
|
||||||
|
@ -119,12 +127,20 @@ impl Component for TrackingView {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sender.input(TrackingInput::Reset);
|
||||||
|
|
||||||
ComponentParts { model, widgets }
|
ComponentParts { model, widgets }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>, root: &Self::Root) {
|
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>, root: &Self::Root) {
|
||||||
|
self.reset();
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
|
TrackingInput::Init => {
|
||||||
|
self.set_is_init(true);
|
||||||
|
}
|
||||||
TrackingInput::Reset => {
|
TrackingInput::Reset => {
|
||||||
|
self.set_is_init(false);
|
||||||
self.factory.clear();
|
self.factory.clear();
|
||||||
}
|
}
|
||||||
TrackingInput::Search(value) => {
|
TrackingInput::Search(value) => {
|
||||||
|
@ -132,9 +148,9 @@ impl Component for TrackingView {
|
||||||
if let Some(value) = &value {
|
if let Some(value) = &value {
|
||||||
// https://www.pakete-verfolgen.de/dhl-sendungsnummer/
|
// https://www.pakete-verfolgen.de/dhl-sendungsnummer/
|
||||||
if value.len() < 8 {
|
if value.len() < 8 {
|
||||||
sender.input(TrackingInput::Notification(
|
sender.input(TrackingInput::Notification(i18n(
|
||||||
"The id is too short to be valid.".to_string(),
|
"The id is too short to be valid.",
|
||||||
));
|
)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,41 +188,46 @@ impl Component for TrackingView {
|
||||||
sender: ComponentSender<Self>,
|
sender: ComponentSender<Self>,
|
||||||
root: &Self::Root,
|
root: &Self::Root,
|
||||||
) {
|
) {
|
||||||
|
self.reset();
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
TrackingCmds::GotTracking(res) => match res {
|
TrackingCmds::GotTracking(res) => match res {
|
||||||
Ok(shipment_vec) => {
|
Ok(shipment_vec) => {
|
||||||
|
if self.is_init {
|
||||||
|
self.set_is_init(false);
|
||||||
|
}
|
||||||
for item in shipment_vec {
|
for item in shipment_vec {
|
||||||
if let Some(err) = item.error {
|
if let Some(err) = item.error {
|
||||||
// TODO: gettext
|
// TODO: gettext
|
||||||
if err.id_invalid {
|
if err.id_invalid {
|
||||||
sender.input(TrackingInput::Notification(format!(
|
sender.input(TrackingInput::Notification(i18n_f(
|
||||||
"The id is invalid ({})",
|
"The id is invalid ({})",
|
||||||
item.id
|
&[&item.id],
|
||||||
)));
|
)));
|
||||||
} else if err.letter_not_found {
|
} else if err.letter_not_found {
|
||||||
sender.input(TrackingInput::Notification(format!(
|
sender.input(TrackingInput::Notification(i18n_f(
|
||||||
"The letter wasn't found ({})",
|
"The letter wasn't found ({})",
|
||||||
item.id
|
&[&item.id],
|
||||||
)));
|
)));
|
||||||
} else if err.id_not_searchable {
|
} else if err.id_not_searchable {
|
||||||
sender.input(TrackingInput::Notification(format!(
|
sender.input(TrackingInput::Notification(i18n_f(
|
||||||
"The id is not searchable ({})",
|
"The id is not searchable ({})",
|
||||||
item.id
|
&[&item.id],
|
||||||
)));
|
)));
|
||||||
} else if err.data_to_old {
|
} else if err.data_to_old {
|
||||||
sender.input(TrackingInput::Notification(format!(
|
sender.input(TrackingInput::Notification(i18n_f(
|
||||||
"No data available with id ({}) (data expired)",
|
"Data was removed with the id ({})",
|
||||||
item.id
|
&[&item.id],
|
||||||
)));
|
)));
|
||||||
} else if err.not_from_dhl {
|
} else if err.not_from_dhl {
|
||||||
sender.input(TrackingInput::Notification(format!(
|
sender.input(TrackingInput::Notification(i18n_f(
|
||||||
"The id is not from DHL ({})",
|
"The id is not from DHL ({})",
|
||||||
item.id
|
&[&item.id],
|
||||||
)));
|
)));
|
||||||
} else if err.no_data_available {
|
} else if err.no_data_available {
|
||||||
sender.input(TrackingInput::Notification(format!(
|
sender.input(TrackingInput::Notification(i18n_f(
|
||||||
"No data available with id ({})",
|
"No data available with id ({})",
|
||||||
item.id
|
&[&item.id],
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -216,9 +237,11 @@ impl Component for TrackingView {
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if err == LibraryError::APIChange {
|
if err == LibraryError::APIChange {
|
||||||
println!("Upstream API for parcel tracking broke");
|
println!(
|
||||||
sender.output(TrackingOutput::Borked).unwrap();
|
"Upstream API for parcel tracking broke, assuming Captcha failure"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
|
// TODO: Localize strings
|
||||||
sender.input(TrackingInput::Notification(format!(
|
sender.input(TrackingInput::Notification(format!(
|
||||||
"Unknown Error: {}",
|
"Unknown Error: {}",
|
||||||
err.to_string()
|
err.to_string()
|
||||||
|
@ -232,21 +255,10 @@ impl Component for TrackingView {
|
||||||
|
|
||||||
struct ShipmentView {
|
struct ShipmentView {
|
||||||
model: Shipment,
|
model: Shipment,
|
||||||
|
|
||||||
// model abstraction
|
|
||||||
have_events: bool,
|
|
||||||
|
|
||||||
// state
|
|
||||||
expanded: bool,
|
|
||||||
|
|
||||||
// workarounds
|
|
||||||
list_box_history: gtk::Box,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ViewInput {
|
pub enum ViewInput {}
|
||||||
ToggleExpand,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[relm4::factory]
|
#[relm4::factory]
|
||||||
impl FactoryComponent for ShipmentView {
|
impl FactoryComponent for ShipmentView {
|
||||||
|
@ -254,19 +266,15 @@ impl FactoryComponent for ShipmentView {
|
||||||
type Init = Shipment;
|
type Init = Shipment;
|
||||||
type Output = ();
|
type Output = ();
|
||||||
type Input = ViewInput;
|
type Input = ViewInput;
|
||||||
type ParentWidget = gtk::Box;
|
type ParentWidget = gtk::ListBox;
|
||||||
type Index = String;
|
type Index = String;
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
#[root]
|
#[root]
|
||||||
gtk::Box {
|
gtk::Box {
|
||||||
add_css_class: relm4::css::CARD,
|
|
||||||
set_hexpand: true,
|
set_hexpand: true,
|
||||||
set_margin_all: 8,
|
|
||||||
set_orientation: gtk::Orientation::Vertical,
|
set_orientation: gtk::Orientation::Vertical,
|
||||||
inline_css: "border-radius: 12px 12px 0px 0px",
|
|
||||||
|
|
||||||
// title box
|
|
||||||
gtk::Box {
|
gtk::Box {
|
||||||
set_orientation: gtk::Orientation::Horizontal,
|
set_orientation: gtk::Orientation::Horizontal,
|
||||||
set_hexpand: true,
|
set_hexpand: true,
|
||||||
|
@ -282,7 +290,8 @@ impl FactoryComponent for ShipmentView {
|
||||||
add_css_class: relm4::css::CAPTION_HEADING,
|
add_css_class: relm4::css::CAPTION_HEADING,
|
||||||
set_halign: gtk::Align::Start,
|
set_halign: gtk::Align::Start,
|
||||||
|
|
||||||
set_label: &self.model.id,
|
set_label?: self.subtitle(),
|
||||||
|
set_visible: self.has_subtitle(),
|
||||||
},
|
},
|
||||||
|
|
||||||
gtk::Label {
|
gtk::Label {
|
||||||
|
@ -290,126 +299,53 @@ impl FactoryComponent for ShipmentView {
|
||||||
set_halign: gtk::Align::Start,
|
set_halign: gtk::Align::Start,
|
||||||
set_wrap: true,
|
set_wrap: true,
|
||||||
|
|
||||||
set_label: {
|
set_label: &self.maintitle(),
|
||||||
// TODO: gettext
|
|
||||||
if let Some(shipment_name) = &self.model.special.shipment_name {
|
|
||||||
shipment_name.as_str()
|
|
||||||
} else if let Some(product_name) = &self.model.special.product_name {
|
|
||||||
product_name.as_str()
|
|
||||||
} else {
|
|
||||||
match self.model.quelle {
|
|
||||||
libpaket::tracking::SendungsQuelle::TTBRIEF => "Letter",
|
|
||||||
libpaket::tracking::SendungsQuelle::PAKET => "Parcel",
|
|
||||||
libpaket::tracking::SendungsQuelle::SVB => "quelle: SVB",
|
|
||||||
libpaket::tracking::SendungsQuelle::OPTIMA => "quelle: OPTIMA",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
gtk::Button {
|
|
||||||
set_halign: gtk::Align::End,
|
|
||||||
add_css_class: relm4::css::FLAT,
|
|
||||||
|
|
||||||
#[watch]
|
|
||||||
set_icon_name: {
|
|
||||||
if self.expanded {
|
|
||||||
relm4_icons::icon_names::MINUS
|
|
||||||
} else {
|
|
||||||
relm4_icons::icon_names::PLUS
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
connect_clicked => ViewInput::ToggleExpand,
|
|
||||||
}
|
|
||||||
}, // title box end
|
|
||||||
|
|
||||||
gtk::ProgressBar {
|
|
||||||
add_css_class: relm4::css::OSD,
|
|
||||||
|
|
||||||
set_fraction: f64::from(self.model.history.maximal_fortschritt) / f64::from(self.model.history.fortschritt),
|
|
||||||
},
|
|
||||||
|
|
||||||
gtk::Revealer {
|
|
||||||
#[watch]
|
|
||||||
set_reveal_child: self.expanded,
|
|
||||||
|
|
||||||
#[wrap(Some)]
|
|
||||||
set_child = >k::Box {
|
|
||||||
set_margin_all: 8,
|
|
||||||
|
|
||||||
// history viewstack
|
|
||||||
gtk::Label {
|
|
||||||
set_visible: !self.have_events,
|
|
||||||
|
|
||||||
set_label: "No events",
|
|
||||||
},
|
|
||||||
|
|
||||||
append = &self.list_box_history.clone() {
|
|
||||||
set_visible: self.have_events,
|
|
||||||
},
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_model(
|
fn init_model(init: Self::Init, _: &Self::Index, _: relm4::FactorySender<Self>) -> Self {
|
||||||
init: Self::Init,
|
let _self = ShipmentView { model: init };
|
||||||
index: &Self::Index,
|
|
||||||
sender: relm4::FactorySender<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let have_events = init.history.events.as_ref().is_some_and(|a| a.len() > 0);
|
|
||||||
|
|
||||||
let list_box_history = gtk::Box::builder()
|
|
||||||
.orientation(gtk::Orientation::Vertical)
|
|
||||||
.spacing(8)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let _self = ShipmentView {
|
|
||||||
have_events,
|
|
||||||
model: init,
|
|
||||||
list_box_history,
|
|
||||||
expanded: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
for elem in _self.model.history.events.as_ref().unwrap() {
|
|
||||||
let label_datum = gtk::Label::builder()
|
|
||||||
.css_classes([relm4::css::NUMERIC])
|
|
||||||
.label(&elem.datum)
|
|
||||||
.halign(gtk::Align::Start)
|
|
||||||
.valign(gtk::Align::Start)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// TODO: is html, parse it
|
|
||||||
let label_status = gtk::Label::builder()
|
|
||||||
.wrap(true)
|
|
||||||
.halign(gtk::Align::Start)
|
|
||||||
.valign(gtk::Align::Start)
|
|
||||||
.build();
|
|
||||||
label_status.set_markup(&elem.status);
|
|
||||||
|
|
||||||
let boxie = gtk::Box::builder()
|
|
||||||
.orientation(gtk::Orientation::Horizontal)
|
|
||||||
.spacing(8)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
boxie.append(&label_datum);
|
|
||||||
boxie.append(&label_status);
|
|
||||||
|
|
||||||
_self.list_box_history.append(&boxie);
|
|
||||||
}
|
|
||||||
|
|
||||||
_self
|
_self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) {
|
fn update(&mut self, _: Self::Input, _: FactorySender<Self>) {}
|
||||||
match message {
|
}
|
||||||
ViewInput::ToggleExpand => {
|
|
||||||
self.expanded = !self.expanded;
|
impl ShipmentView {
|
||||||
|
fn maintitle(&self) -> String {
|
||||||
|
// TODO: gettext
|
||||||
|
if let Some(shipment_name) = &self.model.special.shipment_name {
|
||||||
|
shipment_name.clone()
|
||||||
|
} else if let Some(product_name) = &self.model.special.product_name {
|
||||||
|
product_name.clone()
|
||||||
|
} else if let Some(quelle) = &self.model.quelle {
|
||||||
|
match quelle {
|
||||||
|
libpaket::tracking::SendungsQuelle::TTBRIEF => i18n("Letter"),
|
||||||
|
libpaket::tracking::SendungsQuelle::PAKET => i18n("Parcel"),
|
||||||
|
libpaket::tracking::SendungsQuelle::SVB => i18n("SVB"),
|
||||||
|
libpaket::tracking::SendungsQuelle::OPTIMA => i18n("OPTIMA"),
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
self.model.id.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_subtitle(&self) -> bool {
|
||||||
|
self.model.special.shipment_name.is_some()
|
||||||
|
|| self.model.special.product_name.is_some()
|
||||||
|
|| self.model.quelle.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subtitle(&self) -> Option<&str> {
|
||||||
|
if self.has_subtitle() {
|
||||||
|
Some(self.model.id.as_str())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
46
src/utils.rs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
use crate::i18n::i18n;
|
||||||
|
use relm4::WidgetRef;
|
||||||
|
|
||||||
|
pub fn back_button() -> gtk::Button {
|
||||||
|
gtk::Button::builder()
|
||||||
|
.vexpand_set(false)
|
||||||
|
.css_classes([relm4::css::PILL])
|
||||||
|
.action_name("navigation.pop")
|
||||||
|
.label(i18n("Return to previous page"))
|
||||||
|
.halign(gtk::Align::BaselineCenter)
|
||||||
|
.valign(gtk::Align::BaselineCenter)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn status_page_with_back_button(title: &str, description: Option<&str>) -> adw::StatusPage {
|
||||||
|
status_page(title, description, Some(back_button().widget_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn status_page(
|
||||||
|
title: &str,
|
||||||
|
description: Option<&str>,
|
||||||
|
child: Option<>k::Widget>,
|
||||||
|
) -> adw::StatusPage {
|
||||||
|
let mut builder = adw::StatusPage::builder().title(title);
|
||||||
|
|
||||||
|
if let Some(description) = description {
|
||||||
|
builder = builder.description(description);
|
||||||
|
}
|
||||||
|
if let Some(child) = child {
|
||||||
|
builder = builder.child(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn status_page_loading(title: &str, description: Option<&str>) -> adw::StatusPage {
|
||||||
|
let mut builder = adw::StatusPage::builder().title(title);
|
||||||
|
|
||||||
|
if let Some(description) = description {
|
||||||
|
builder = builder.description(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
let status_page = builder.build();
|
||||||
|
status_page.set_paintable(Some(adw::SpinnerPaintable::new(Some(&status_page))).as_ref());
|
||||||
|
status_page
|
||||||
|
}
|