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 = [
|
||||
"libpaket",
|
||||
"paket",
|
||||
"paket-utils",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
@ -13,12 +13,64 @@ license = "AGPL-3.0-only"
|
|||
version = "0.1.0"
|
||||
|
||||
[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"] }
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
serde_json = "1.0.111"
|
||||
uuid = { version = "1.7.0", features = ["v4"] }
|
||||
relm4 = { git = "https://github.com/Relm4/Relm4.git", features = [
|
||||
"libadwaita",
|
||||
"macros",
|
||||
] }
|
||||
secrecy = { version = "0.10", features = ["serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_ignored = "0.1"
|
||||
serde_json = "1"
|
||||
serde_newtype = "0.1"
|
||||
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
|
||||
package = {
|
||||
rustPlatform,
|
||||
nix-gitignore,
|
||||
pkg-config,
|
||||
openssl,
|
||||
glib,
|
||||
gdk-pixbuf,
|
||||
graphene,
|
||||
cairo,
|
||||
pango,
|
||||
gtk4,
|
||||
libsoup_3,
|
||||
libadwaita,
|
||||
webkitgtk_6_0,
|
||||
libseccomp,
|
||||
wrapGAppsHook4,
|
||||
gdk-pixbuf,
|
||||
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 {
|
||||
pname = "paket";
|
||||
version = "unstable-2024-09-28";
|
||||
version = "unstable-2025-01-22";
|
||||
|
||||
src = nix-gitignore.gitignoreSource [] ./.;
|
||||
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
outputHashes = {
|
||||
"relm4-0.9.0" = "sha256-iFxi2ZWdzWtui85IOfMIfyuPDbQO69u5VLk0a9ebatM=";
|
||||
"relm4-icons-0.9.0" = "sha256-UUo1wIvJL2MryUFICnmVq6LoPuNaZ9nKcNGCCF8cx+k=";
|
||||
};
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
|
@ -37,6 +36,13 @@
|
|||
wrapGAppsHook4
|
||||
];
|
||||
|
||||
preFixup = ''
|
||||
gappsWrapperArgs+=(
|
||||
# vp8enc preset
|
||||
--prefix GST_PRESET_PATH : "${gst_all_1.gst-plugins-good}/share/gstreamer-1.0/presets"
|
||||
)
|
||||
'';
|
||||
|
||||
buildInputs = [
|
||||
# Building
|
||||
openssl
|
||||
|
@ -48,6 +54,15 @@
|
|||
libadwaita
|
||||
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
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
aes-gcm = { version = "0.10.3", optional = true }
|
||||
ed25519-dalek = { version = "2.1.0", optional = true }
|
||||
hmac = { version = "0.12.1", optional = true }
|
||||
num_enum = { version = "0.7", optional = true }
|
||||
aes-gcm = { workspace = true, optional = true }
|
||||
ed25519-dalek = { workspace = true, optional = true }
|
||||
hmac = { workspace = true, optional = true }
|
||||
num_enum = { workspace = true, optional = true }
|
||||
|
||||
# TODO: Consolidate?
|
||||
rand = "0.8.5"
|
||||
random-string = "1.1.0"
|
||||
rand = { workspace = true }
|
||||
random-string = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
secrecy = { workspace = true}
|
||||
secrecy = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_repr = { version = "0.1.18", optional = true }
|
||||
serde_ignored = "0.1"
|
||||
url = "2.5.0"
|
||||
base64 = "0.22"
|
||||
serde_repr = { workspace = true, optional = true }
|
||||
serde_ignored = { workspace = true }
|
||||
url = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
|
||||
# TODO: consider splitting login.rs refresh_token and authorization_token
|
||||
# (sha2 and urlencoding only used with authorization_token)
|
||||
# sha2 also used in briefankuendigung and packstation_register_regtoken
|
||||
sha2 = "0.10.8"
|
||||
urlencoding = "2.1.3"
|
||||
sha2 = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde"], optional = true }
|
||||
|
||||
serde_newtype = "0.1.1"
|
||||
thiserror = "1.0.56"
|
||||
serde_newtype = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = [
|
||||
|
@ -42,6 +42,8 @@ default = [
|
|||
|
||||
unstable = []
|
||||
|
||||
private_tests = []
|
||||
|
||||
advices = [
|
||||
#"dep:sha2",
|
||||
"dep:uuid",
|
||||
|
@ -72,7 +74,6 @@ locker_register_base = [
|
|||
"locker_base",
|
||||
"dep:hmac",
|
||||
#"dep:sha2",
|
||||
|
||||
]
|
||||
|
||||
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.
|
||||
|
||||
### Getting mail notifications (Briefankündigung)
|
||||
|
||||
```rust
|
||||
// Requires a logged-in user.
|
||||
let token: libpaket::login::DHLIdToken;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
mod www;
|
||||
mod briefankuendigung;
|
||||
mod www;
|
||||
|
||||
pub use www::*;
|
||||
pub use briefankuendigung::*;
|
||||
pub use www::*;
|
||||
|
|
|
@ -32,27 +32,28 @@ newtype! {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdvicesResponse {
|
||||
// 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) basic_auth: Option<String>,
|
||||
current_advice: Option<AdvicesList>,
|
||||
current_advice: Vec<AdvicesList>,
|
||||
pub(super) grant_token: Option<String>,
|
||||
old_advices: Vec<AdvicesList>,
|
||||
}
|
||||
|
||||
impl AdvicesResponse {
|
||||
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 {
|
||||
self.current_advice.is_some()
|
||||
pub fn has_current_advices(&self) -> bool {
|
||||
self.current_advice.len() > 0
|
||||
}
|
||||
|
||||
pub fn has_old_advices(&self) -> bool {
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -67,20 +68,32 @@ fn endpoint_advices() -> url::Url {
|
|||
|
||||
impl crate::www::WebClient {
|
||||
// FIXME: more error parsing
|
||||
pub async fn advices(&self, 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())
|
||||
pub async fn advices(
|
||||
&self,
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
pub fn app_version() -> &'static str {
|
||||
"9.9.1.95 (a72fec7be)"
|
||||
}
|
||||
|
@ -8,7 +7,11 @@ pub fn linux_android_version() -> &'static str {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -28,8 +28,9 @@ pub mod tracking;
|
|||
#[cfg(feature = "locker_ble")]
|
||||
pub use locker::LockerClient;
|
||||
|
||||
/*#[cfg(test)]
|
||||
pub(crate) mod private;*/
|
||||
#[cfg(feature = "private_tests")]
|
||||
#[cfg(test)]
|
||||
pub(crate) mod private;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
|
@ -64,7 +65,7 @@ impl From<reqwest::Error> for LibraryError {
|
|||
impl From<common::APIError> for LibraryError {
|
||||
fn from(value: common::APIError) -> Self {
|
||||
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 uuid::Uuid;
|
||||
use super::utils::{PrimitiveBuilder, PrimitiveReader};
|
||||
|
||||
use crate::{LibraryError, LibraryResult};
|
||||
|
||||
|
@ -45,7 +45,7 @@ const REQUESTS: [CommandType; 10] = [
|
|||
];
|
||||
|
||||
struct ResponseInitSession {
|
||||
raw_command: Command
|
||||
raw_command: Command,
|
||||
}
|
||||
|
||||
enum Response {
|
||||
|
@ -57,7 +57,10 @@ impl TryFrom<Command> for Response {
|
|||
|
||||
fn try_from(value: Command) -> Result<Self, Self::Error> {
|
||||
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!()
|
||||
}
|
||||
|
@ -74,12 +77,11 @@ impl TryFrom<Command> for Response {
|
|||
|
||||
// Checksum function
|
||||
|
||||
|
||||
pub struct Command {
|
||||
r#type: CommandType,
|
||||
payload: Vec<u8>,
|
||||
init_vector: Vec<u8>,
|
||||
metadata: Vec<u8>
|
||||
metadata: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
|
@ -105,9 +107,11 @@ impl Command {
|
|||
|
||||
pub fn parse(bin: Vec<u8>) -> LibraryResult<Self> {
|
||||
// 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 {
|
||||
return Err(to_few_bytes)
|
||||
return Err(to_few_bytes);
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -115,11 +119,14 @@ impl Command {
|
|||
PrimitiveReader {
|
||||
offset: bin.len() - 2,
|
||||
vec: bin.as_slice(),
|
||||
}.read_u16()
|
||||
}
|
||||
.read_u16()
|
||||
};
|
||||
|
||||
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,7 +137,9 @@ impl Command {
|
|||
|
||||
let r#type = reader.read_u8();
|
||||
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;
|
||||
|
@ -152,21 +161,21 @@ impl Command {
|
|||
|
||||
pub fn finish(self) -> Vec<u8> {
|
||||
let vec1 = PrimitiveBuilder::new()
|
||||
.write_array_with_len(&self.payload)
|
||||
.write_array_with_len(&self.init_vector)
|
||||
.write_array_with_len(&self.metadata)
|
||||
.finish();
|
||||
.write_array_with_len(&self.payload)
|
||||
.write_array_with_len(&self.init_vector)
|
||||
.write_array_with_len(&self.metadata)
|
||||
.finish();
|
||||
|
||||
let vec2 = PrimitiveBuilder::new()
|
||||
.write_u8(self.r#type as u8)
|
||||
.write_u32(vec1.len() as u32 + 2)
|
||||
.write_array(&vec1)
|
||||
.finish();
|
||||
.write_u8(self.r#type as u8)
|
||||
.write_u32(vec1.len() as u32 + 2)
|
||||
.write_array(&vec1)
|
||||
.finish();
|
||||
|
||||
PrimitiveBuilder::new()
|
||||
.write_array(&vec2)
|
||||
.write_u16(Self::checksum(vec2.as_slice()))
|
||||
.finish()
|
||||
.write_array(&vec2)
|
||||
.write_u16(Self::checksum(vec2.as_slice()))
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn assert_only_decimal(str: &String) {
|
||||
|
@ -183,15 +192,15 @@ impl Command {
|
|||
|
||||
// de.dhl.paket does some kinky string conversion
|
||||
|
||||
let vec = Vec::<u8>::new().into_iter()
|
||||
.chain(fields.0.to_be_bytes().into_iter())
|
||||
.chain(fields.1.to_be_bytes().into_iter())
|
||||
.chain(fields.2.to_be_bytes().into_iter())
|
||||
.chain(fields.3.to_vec().into_iter())
|
||||
.collect::<Vec<u8>>();
|
||||
let vec = Vec::<u8>::new()
|
||||
.into_iter()
|
||||
.chain(fields.0.to_be_bytes().into_iter())
|
||||
.chain(fields.1.to_be_bytes().into_iter())
|
||||
.chain(fields.2.to_be_bytes().into_iter())
|
||||
.chain(fields.3.to_vec().into_iter())
|
||||
.collect::<Vec<u8>>();
|
||||
|
||||
let new_vec = PrimitiveBuilder::new()
|
||||
.write_array_with_len(&vec).finish();
|
||||
let new_vec = PrimitiveBuilder::new().write_array_with_len(&vec).finish();
|
||||
|
||||
Ok(Command {
|
||||
r#type: CommandType::InitSessionRequest,
|
||||
|
@ -202,9 +211,12 @@ impl Command {
|
|||
}
|
||||
}
|
||||
|
||||
/*#[cfg(test)]
|
||||
#[cfg(feature = "private_tests")]
|
||||
#[cfg(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};
|
||||
|
||||
|
@ -215,7 +227,10 @@ mod test {
|
|||
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())
|
||||
}
|
||||
|
@ -229,5 +244,4 @@ mod test {
|
|||
|
||||
let command = Command::parse(corrected);
|
||||
}
|
||||
|
||||
}*/
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ impl Seed {
|
|||
let mut rng = rand::thread_rng();
|
||||
let mut bytes: Vec<u8> = vec![0; 32];
|
||||
rng.fill_bytes(bytes.as_mut_slice());
|
||||
Seed (bytes)
|
||||
Seed(bytes)
|
||||
}
|
||||
|
||||
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 {
|
||||
CustomerKeySeed {
|
||||
postnumber: postnumber.clone(),
|
||||
|
|
|
@ -68,7 +68,6 @@ pub enum APIRegisterError {
|
|||
UserCanNotYetRegister = 21,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) struct RegistrationCommonDevice {
|
||||
|
|
|
@ -4,8 +4,11 @@ use hmac::{digest::CtOutput, Mac, SimpleHmac};
|
|||
use sha2::Sha256;
|
||||
|
||||
use crate::common::APIResult;
|
||||
use crate::locker::register::{DeviceMetadata, DeviceRegistrationResponse, RegToken, RegistrationCommonDevice, RegistrationOption, RegistrationPayload};
|
||||
use crate::locker::crypto::CustomerKeySeed;
|
||||
use crate::locker::register::{
|
||||
DeviceMetadata, DeviceRegistrationResponse, RegToken, RegistrationCommonDevice,
|
||||
RegistrationOption, RegistrationPayload,
|
||||
};
|
||||
use crate::login::DHLIdToken;
|
||||
use crate::LibraryResult;
|
||||
|
||||
|
@ -28,7 +31,9 @@ impl RegistrationRegToken {
|
|||
reg_token: &RegToken,
|
||||
device_metadata: DeviceMetadata,
|
||||
) -> 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();
|
||||
mac.update(challange_bin.as_slice());
|
||||
|
@ -52,7 +57,6 @@ impl RegistrationRegToken {
|
|||
}
|
||||
|
||||
impl crate::StammdatenClient {
|
||||
|
||||
pub async fn register_by_regtoken(
|
||||
&self,
|
||||
dhli: &DHLIdToken,
|
||||
|
@ -61,7 +65,6 @@ impl crate::StammdatenClient {
|
|||
device_metadata: DeviceMetadata,
|
||||
reg_token: &crate::locker::register::RegToken,
|
||||
) -> LibraryResult<DeviceRegistrationResponse> {
|
||||
|
||||
let mut valid = false;
|
||||
for option in registration_payload.registration_options.iter() {
|
||||
if *option == RegistrationOption::ByRegToken {
|
||||
|
@ -74,7 +77,7 @@ impl crate::StammdatenClient {
|
|||
customer_key_seed,
|
||||
registration_payload,
|
||||
reg_token,
|
||||
device_metadata
|
||||
device_metadata,
|
||||
);
|
||||
|
||||
let body = serde_json::to_string(&body).unwrap();
|
||||
|
@ -103,16 +106,15 @@ impl crate::StammdatenClient {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
fn endpoint_devices_register_by_regtoken() -> &'static str {
|
||||
"https://www.dhl.de/int-stammdaten/public/devices/registerByRegToken"
|
||||
}
|
||||
|
||||
|
||||
/*#[cfg(test)]
|
||||
#[cfg(feature = "private_tests")]
|
||||
#[cfg(test)]
|
||||
mod registration_api {
|
||||
use std::str::FromStr as _;
|
||||
use base64::Engine as _;
|
||||
use std::str::FromStr as _;
|
||||
|
||||
use crate::private::init as private;
|
||||
|
||||
|
@ -120,16 +122,19 @@ mod registration_api {
|
|||
fn regtoken_customer_password() {
|
||||
let regtoken = super::RegToken::parse_from_qrcode_uri(&private::IN_REGTOKEN).unwrap();
|
||||
assert!(regtoken.customer_password() == private::OUT_CUSTOMER_PASSWORD);
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_via_regtoken(){
|
||||
fn register_via_regtoken() {
|
||||
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(),
|
||||
base64::engine::general_purpose::STANDARD.decode(&private::IN_SEED_BASE64).unwrap(),
|
||||
&uuid::Uuid::from_str(&private::IN_KEY_ID).unwrap());
|
||||
let customer_key_seed = crate::locker::crypto::CustomerKeySeed::from_partial(
|
||||
&private::IN_POSTNUMBER.to_string(),
|
||||
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 {
|
||||
challenge: private::IN_CHALLENGE.to_string(),
|
||||
|
@ -140,13 +145,16 @@ mod registration_api {
|
|||
&customer_key_seed,
|
||||
registration_payload,
|
||||
®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_signature == private::OUT_VERIFIERSIGNATURE);
|
||||
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 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>);
|
||||
impl secrecy::zeroize::Zeroize for RegToken {
|
||||
|
@ -12,23 +32,23 @@ impl secrecy::zeroize::Zeroize for 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 {
|
||||
return Err(LibraryError::DecodeError("RegTokenUri too short".to_string()));
|
||||
return Err(RegTokenDecodeError::LengthTooShort);
|
||||
}
|
||||
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 = general_purpose::STANDARD.decode(token);
|
||||
|
||||
let Ok(mut token) = token else {
|
||||
return Err(LibraryError::DecodeError("RegTokenUri not decodeable (base64)".to_string()));
|
||||
return Err(RegTokenDecodeError::NotBase64);
|
||||
};
|
||||
|
||||
if token.len() > 64 {
|
||||
return Err(LibraryError::DecodeError("RegToken longer than expected".to_string()));
|
||||
return Err(RegTokenDecodeError::LengthTooLong);
|
||||
}
|
||||
|
||||
if token.len() < 32 {
|
||||
|
|
|
@ -3,7 +3,13 @@ use crate::{LibraryError, LibraryResult};
|
|||
|
||||
// 601e7028-0565-
|
||||
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 {
|
||||
Keba,
|
||||
|
@ -38,10 +44,7 @@ pub struct LockerDevice {
|
|||
}
|
||||
|
||||
impl LockerDevice {
|
||||
pub fn new(
|
||||
service_uuid: LockerServiceUUID,
|
||||
service_data: &Vec<u8>,
|
||||
) -> LibraryResult<Self> {
|
||||
pub fn new(service_uuid: LockerServiceUUID, service_data: &Vec<u8>) -> LibraryResult<Self> {
|
||||
mini_assert_inval!(service_data.len() == 14);
|
||||
|
||||
let mut reader = PrimitiveReader {
|
||||
|
|
|
@ -40,9 +40,7 @@ pub mod token {
|
|||
]
|
||||
}
|
||||
|
||||
pub fn revoke_form(
|
||||
token: String
|
||||
) -> Vec<(String, String)> {
|
||||
pub fn revoke_form(token: String) -> Vec<(String, String)> {
|
||||
vec![
|
||||
("token".into(), token),
|
||||
("client_id".into(), client_id().into()),
|
||||
|
@ -81,10 +79,13 @@ pub mod token {
|
|||
}
|
||||
|
||||
pub mod logout {
|
||||
use crate::{constants::web_user_agent, login::{
|
||||
constants::{client_id, redirect_uri_logout},
|
||||
DHLIdToken,
|
||||
}};
|
||||
use crate::{
|
||||
constants::web_user_agent,
|
||||
login::{
|
||||
constants::{client_id, redirect_uri_logout},
|
||||
DHLIdToken,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn form(id_token: &DHLIdToken) -> Vec<(String, String)> {
|
||||
vec![
|
||||
|
|
|
@ -52,7 +52,8 @@ impl<T> OpenIDToken<T> {
|
|||
|
||||
// requires valid system time
|
||||
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 {
|
||||
return true;
|
||||
|
@ -104,14 +105,14 @@ where
|
|||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
|
||||
let splits = s.split_terminator(".").collect::<Vec<&str>>();
|
||||
let claims = serde_json::from_slice::<Claims<T>>(
|
||||
general_purpose::URL_SAFE_NO_PAD
|
||||
.decode(splits[1])
|
||||
.unwrap()
|
||||
.as_slice(),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Ok(OpenIDToken {
|
||||
token: s.to_string(),
|
||||
|
|
|
@ -142,6 +142,5 @@ pub struct CustomerDataFull {
|
|||
|
||||
pub requested_services: Option<Vec<CustomerDataService>>,
|
||||
//pub customer_actions: Option,
|
||||
|
||||
pub address: CustomerAddress,
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ struct SendungEmpfaenger {
|
|||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SendungsInfo {
|
||||
gesuchte_sendungsnummer: String,
|
||||
gesuchte_sendungsnummer: Option<String>,
|
||||
sendungsrichtung: String,
|
||||
sendungsname: Option<String>,
|
||||
sendungsliste: Option<String>,
|
||||
|
@ -169,16 +169,22 @@ struct Nachhaltigkeitsstatus {
|
|||
klimafreundlicher_empfang: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Sendungsnummern {
|
||||
sendungsnummer: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SendungDetails {
|
||||
quelle: SendungsQuelle, // PAKET
|
||||
quelle: Option<SendungsQuelle>, // PAKET
|
||||
express_sendung: Option<bool>,
|
||||
is_shipper_plz: Option<bool>,
|
||||
ist_zugestellt: Option<bool>,
|
||||
retoure: Option<bool>,
|
||||
ruecksendung: Option<bool>,
|
||||
sendungsverlauf: SendungsVerlauf,
|
||||
sendungsverlauf: Option<SendungsVerlauf>,
|
||||
show_quality_level_hint: Option<bool>,
|
||||
two_man_handling: Option<bool>,
|
||||
unplausibel: Option<bool>,
|
||||
|
@ -192,7 +198,7 @@ struct SendungDetails {
|
|||
international: Option<bool>,
|
||||
pan_empfaenger: Option<SendungEmpfaenger>,
|
||||
produkt_name: Option<String>,
|
||||
//sendungsnummern: (),
|
||||
sendungsnummern: Option<Sendungsnummern>,
|
||||
services: Option<SendungServiceStatusBenachrichtigung>,
|
||||
show_digital_notification_cta_hint: Option<bool>,
|
||||
warenpost: Option<bool>,
|
||||
|
@ -321,7 +327,7 @@ pub struct Shipment {
|
|||
pub needs_shipment_date: bool,
|
||||
pub needs_plz: bool,
|
||||
|
||||
pub quelle: SendungsQuelle,
|
||||
pub quelle: Option<SendungsQuelle>,
|
||||
|
||||
// probably not optional
|
||||
pub international: Option<bool>,
|
||||
|
@ -330,7 +336,7 @@ pub struct Shipment {
|
|||
|
||||
pub special: ShipmentSpecialDetails,
|
||||
|
||||
pub history: SendungsVerlauf,
|
||||
pub history: Option<SendungsVerlauf>,
|
||||
pub error: Option<ShipmentNotFoundError>,
|
||||
}
|
||||
|
||||
|
|
|
@ -27,8 +27,13 @@ impl CookieHeaderValueBuilder {
|
|||
}
|
||||
|
||||
pub fn add_key_value(mut self, key: String, value: String) -> Self {
|
||||
self.list
|
||||
.push((key, value));
|
||||
self.list.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
|
||||
}
|
||||
|
@ -188,6 +193,7 @@ macro_rules! parse_json_response {
|
|||
($res: expr, $type: ty) => {{
|
||||
let status = $res.status();
|
||||
let res: String = $res.text().await.unwrap();
|
||||
println!("text: {}", res);
|
||||
// Catch HTML Response early
|
||||
if status == 200 {
|
||||
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 relm4::{Component, ComponentParts};
|
||||
|
||||
use crate::{send_log_out, LoginSharedState};
|
||||
use crate::{i18n::i18n, send_log_out, LoginSharedState};
|
||||
|
||||
#[tracker::track]
|
||||
pub struct AccountView {
|
||||
|
@ -92,7 +92,7 @@ impl Component for AccountView {
|
|||
|
||||
append = >k::Button {
|
||||
add_css_class: relm4::css::DESTRUCTIVE_ACTION,
|
||||
set_label: "Log out",
|
||||
set_label: &i18n("Log out"),
|
||||
|
||||
connect_clicked => AccountInput::LogOut,
|
||||
},
|
||||
|
@ -101,7 +101,7 @@ impl Component for AccountView {
|
|||
// Postnumber
|
||||
add = &adw::ActionRow {
|
||||
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())]
|
||||
set_title?: get_str_from_customer_data!(model, post_number).as_ref(),
|
||||
|
@ -112,8 +112,8 @@ impl Component for AccountView {
|
|||
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::ButtonContent {
|
||||
set_icon_name: relm4_icons::icon_names::COPY,
|
||||
set_label: "Copy",
|
||||
set_icon_name: "copy-symbolic",
|
||||
set_label: &i18n("Copy"),
|
||||
},
|
||||
|
||||
connect_clicked => AccountInput::Copy(CopyTargets::PostNumber),
|
||||
|
@ -211,10 +211,11 @@ impl Component for AccountView {
|
|||
for service in services {
|
||||
match service {
|
||||
libpaket::stammdaten::CustomerDataService::Packstation => {
|
||||
sender.output(AccountOutput::HaveService(
|
||||
AccountServices::PackstationAvailable,
|
||||
))
|
||||
.unwrap();
|
||||
sender
|
||||
.output(AccountOutput::HaveService(
|
||||
AccountServices::PackstationAvailable,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
_ => (),
|
||||
};
|
|
@ -39,6 +39,15 @@ impl FactoryComponent for AdviceCard {
|
|||
type CommandOutput = AdviceCardCmds;
|
||||
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! {
|
||||
#[root]
|
||||
gtk::FlowBoxChild {
|
||||
|
@ -46,21 +55,36 @@ impl FactoryComponent for AdviceCard {
|
|||
set_valign: gtk::Align::Start,
|
||||
|
||||
#[wrap(Some)]
|
||||
set_child = >k::Overlay {
|
||||
set_margin_all: 8,
|
||||
|
||||
add_overlay = >k::Spinner {
|
||||
start: (),
|
||||
set_align: gtk::Align::Center,
|
||||
|
||||
#[track(self.changed_texture())]
|
||||
set_visible: self.texture.is_none(),
|
||||
},
|
||||
|
||||
set_child = &adw::Clamp {
|
||||
#[wrap(Some)]
|
||||
set_child = >k::Picture {
|
||||
#[track(self.changed_texture())]
|
||||
set_paintable: self.texture.as_ref()
|
||||
set_child = >k::Overlay {
|
||||
set_margin_all: 8,
|
||||
|
||||
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 futures::lock::Mutex;
|
||||
use libpaket::{AdviceClient as LibraryAdviceClient, LibraryResult};
|
||||
|
||||
use adw::prelude::*;
|
||||
use gio::prelude::*;
|
||||
use glib::prelude::*;
|
||||
use gtk::{gio, glib};
|
||||
use relm4::prelude::*;
|
||||
use relm4::prelude::*;
|
||||
|
||||
use relm4::factory::FactoryVecDeque;
|
||||
|
||||
use crate::advice::{Advice, AdviceCard, AdviceProd};
|
||||
use crate::i18n::i18n;
|
||||
use crate::utils;
|
||||
|
||||
struct AdviceClientImpl {
|
||||
uat_token: libpaket::advices::UatToken,
|
||||
|
@ -72,9 +69,7 @@ impl FactoryComponent for AdvicesDayView {
|
|||
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;
|
||||
|
||||
view! {
|
||||
gtk::ScrolledWindow {
|
||||
adw::Clamp {
|
||||
adw::ViewStack {
|
||||
#[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)]
|
||||
set_child = &adw::ViewStack {
|
||||
#[name = "advices_page_loading"]
|
||||
add = &adw::StatusPage {
|
||||
set_title: "Loading mail notifications...",
|
||||
},
|
||||
set_child = >k::Viewport {
|
||||
set_vscroll_policy: gtk::ScrollablePolicy::Natural,
|
||||
|
||||
#[name = "advices_page_no_available"]
|
||||
add = &adw::StatusPage {
|
||||
set_title: "No mail notifications available."
|
||||
},
|
||||
|
||||
#[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
|
||||
#[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
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
async fn init(
|
||||
|
@ -204,7 +191,7 @@ impl AsyncComponent for AdvicesView {
|
|||
AdvicesViewInput::Reset => {
|
||||
self.set_state(AdvicesViewState::Loading);
|
||||
self.factory.guard().clear();
|
||||
},
|
||||
}
|
||||
AdvicesViewInput::Fetch => {
|
||||
self.set_state(AdvicesViewState::Loading);
|
||||
|
||||
|
@ -232,9 +219,9 @@ impl AsyncComponent for AdvicesView {
|
|||
|
||||
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();
|
||||
for item in ¤t.list {
|
||||
for item in &new_n.list {
|
||||
advices_arr.push(Advice::Prod(AdviceProd {
|
||||
client: client.clone(),
|
||||
model: item.clone(),
|
||||
|
@ -242,7 +229,7 @@ impl AsyncComponent for AdvicesView {
|
|||
}
|
||||
|
||||
arr.push(AdvicesForDay {
|
||||
date: current.date.clone(),
|
||||
date: new_n.date.clone(),
|
||||
advices: advices_arr,
|
||||
});
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
use adw::{self, glib, prelude::*};
|
||||
use gtk;
|
||||
use paket::login::{new_login_shared_state, Login, LoginOutput};
|
||||
use paket::ready::{Ready, ReadyOutput};
|
||||
use relm4::{main_adw_application, prelude::*, AsyncComponentSender, RELM_THREADS};
|
||||
use crate::i18n::i18n;
|
||||
use crate::login::{new_login_shared_state, Login, LoginOutput};
|
||||
use crate::ready::{Ready, ReadyOutput};
|
||||
use adw::{self, prelude::*};
|
||||
use relm4::{main_adw_application, prelude::*, AsyncComponentSender};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum AppState {
|
||||
|
@ -13,13 +13,13 @@ enum AppState {
|
|||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AppError {
|
||||
pub struct AppError {
|
||||
short: String,
|
||||
long: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum AppInput {
|
||||
pub enum AppInput {
|
||||
AddBreakpoint(adw::Breakpoint),
|
||||
SwitchToLogin,
|
||||
SwitchToLoading,
|
||||
|
@ -28,7 +28,7 @@ enum AppInput {
|
|||
}
|
||||
|
||||
#[tracker::track]
|
||||
struct App {
|
||||
pub struct App {
|
||||
state: AppState,
|
||||
_network_fail: bool,
|
||||
|
||||
|
@ -38,7 +38,7 @@ struct App {
|
|||
ready: Controller<Ready>,
|
||||
}
|
||||
|
||||
#[relm4::component(async)]
|
||||
#[relm4::component(async, pub)]
|
||||
impl AsyncComponent for App {
|
||||
type Input = AppInput;
|
||||
type Output = ();
|
||||
|
@ -111,7 +111,7 @@ impl AsyncComponent for App {
|
|||
.forward(sender.input_sender(), convert_ready_response);
|
||||
|
||||
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);
|
||||
|
||||
let model = App {
|
||||
|
@ -166,11 +166,11 @@ fn convert_login_response(response: LoginOutput) -> AppInput {
|
|||
LoginOutput::RequiresLogin => AppInput::SwitchToLogin,
|
||||
LoginOutput::RequiresLoading => AppInput::SwitchToLoading,
|
||||
LoginOutput::Error(library_error) => AppInput::FatalErr(AppError {
|
||||
short: "Unhandled API error".to_string(),
|
||||
short: i18n("Unhandled API error"),
|
||||
long: library_error.to_string(),
|
||||
}),
|
||||
LoginOutput::KeyringError(error) => AppInput::FatalErr(AppError {
|
||||
short: "Keyring usage failed".to_string(),
|
||||
short: i18n("Keyring usage failed"),
|
||||
long: error.to_string(),
|
||||
}),
|
||||
}
|
||||
|
@ -182,17 +182,3 @@ fn convert_ready_response(response: ReadyOutput) -> AppInput {
|
|||
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 secrecy::{zeroize::Zeroize, ExposeSecret, SecretBox};
|
||||
|
||||
use crate::i18n::i18n;
|
||||
|
||||
pub static KEYRING: OnceLock<oo7::Keyring> = OnceLock::new();
|
||||
|
||||
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)> {
|
||||
|
@ -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<()> {
|
||||
get_keyring()
|
||||
.create_item(
|
||||
"Paket: Login credentials",
|
||||
&i18n("Paket: Login credentials"),
|
||||
&get_keyring_attributes_refresh_token(),
|
||||
value,
|
||||
true,
|
||||
|
@ -168,7 +170,7 @@ pub async fn keyring_set_packstation(data: &CustomerKeySeed) -> KeyringResult<()
|
|||
|
||||
Ok(get_keyring()
|
||||
.create_item(
|
||||
"Paket: Device keys",
|
||||
&i18n("Paket: Device keys for parcel locker service"),
|
||||
&get_keyring_attributes_packstation(),
|
||||
string.expose_secret(),
|
||||
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 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)]
|
||||
pub enum LoginInput {
|
||||
|
@ -109,7 +113,7 @@ impl AsyncComponent for Login {
|
|||
add = &adw::Bin {
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::StatusPage {
|
||||
set_title: "Welcome to Paket!",
|
||||
set_title: &i18n("Welcome to Paket!"),
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::Clamp {
|
||||
set_maximum_size: 260,
|
||||
|
@ -119,7 +123,7 @@ impl AsyncComponent for Login {
|
|||
add_css_class: relm4::css::SUGGESTED_ACTION,
|
||||
add_css_class: relm4::css::PILL,
|
||||
|
||||
set_label: "Log In",
|
||||
set_label: &i18n("Log In"),
|
||||
|
||||
connect_clicked => LoginInput::ConsentToLogin,
|
||||
},
|
||||
|
@ -150,9 +154,7 @@ impl AsyncComponent for Login {
|
|||
},
|
||||
|
||||
#[name = "page_offline"]
|
||||
add = &adw::Bin {
|
||||
|
||||
},
|
||||
add = &status_page(&i18n("Internet unavailable"), None, None) -> adw::StatusPage {},
|
||||
|
||||
#[track(model.changed_state())]
|
||||
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 secrecy::{ExposeSecret, SecretBox};
|
||||
|
||||
use crate::i18n::i18n;
|
||||
use crate::keyring::{keyring_get_packstation, keyring_set_packstation};
|
||||
use crate::login::get_id_token;
|
||||
use crate::{
|
||||
|
@ -78,8 +79,8 @@ impl Component for PackstationView {
|
|||
add = page_registration = &adw::ViewStack {
|
||||
#[name = "registration_page_beginning"]
|
||||
add = &adw::StatusPage {
|
||||
set_title: "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_title: &i18n("Register your device"),
|
||||
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)]
|
||||
set_child = &adw::Clamp {
|
||||
|
@ -92,8 +93,8 @@ impl Component for PackstationView {
|
|||
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::ButtonContent {
|
||||
set_label: "Register",
|
||||
set_icon_name: relm4_icons::icon_names::QR_CODE_SCANNER,
|
||||
set_label: &i18n("Register"),
|
||||
set_icon_name: "qr-code-scanner-symbolic",
|
||||
},
|
||||
|
||||
connect_clicked[sender = sender.clone()] => move |_| {
|
||||
|
@ -108,7 +109,7 @@ impl Component for PackstationView {
|
|||
#[wrap(Some)]
|
||||
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())]
|
||||
|
@ -197,7 +198,7 @@ impl Component for PackstationView {
|
|||
Ok(value) => {
|
||||
keyring_set_packstation(&value).await.unwrap();
|
||||
PackstationViewCommand::GotDeviceCredentials(value)
|
||||
},
|
||||
}
|
||||
Err(err) => todo!(),
|
||||
},
|
||||
Err(err) => todo!(),
|
||||
|
@ -223,20 +224,18 @@ impl Component for PackstationView {
|
|||
}
|
||||
PackstationViewCommand::GotNothing => {
|
||||
self.set_state(State::RegisterWizard(RegisterState::Beginning));
|
||||
},
|
||||
}
|
||||
PackstationViewCommand::GotAPIRegisterError(error) => {
|
||||
todo!()
|
||||
},
|
||||
PackstationViewCommand::GotLibraryError(error) => {
|
||||
match error {
|
||||
libpaket::LibraryError::Unauthorized => todo!(),
|
||||
libpaket::LibraryError::DecodeError(_) => todo!(),
|
||||
libpaket::LibraryError::APIChange => todo!(),
|
||||
libpaket::LibraryError::Deprecated => todo!(),
|
||||
}
|
||||
PackstationViewCommand::GotLibraryError(error) => match error {
|
||||
libpaket::LibraryError::Unauthorized => todo!(),
|
||||
libpaket::LibraryError::DecodeError(_) => todo!(),
|
||||
libpaket::LibraryError::APIChange => todo!(),
|
||||
libpaket::LibraryError::Deprecated => todo!(),
|
||||
|
||||
libpaket::LibraryError::InvalidArgument(error) => panic!("{}", error),
|
||||
libpaket::LibraryError::NetworkFetch => panic!(),
|
||||
}
|
||||
libpaket::LibraryError::InvalidArgument(error) => panic!("{}", error),
|
||||
libpaket::LibraryError::NetworkFetch => panic!(),
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,8 +1,12 @@
|
|||
use crate::i18n::i18n;
|
||||
use adw::prelude::*;
|
||||
use relm4::prelude::*;
|
||||
|
||||
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]
|
||||
|
@ -73,14 +77,14 @@ impl Component for Ready {
|
|||
#[wrap(Some)]
|
||||
#[name = "ready_view_stack"]
|
||||
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())]
|
||||
set_visible: model.have_service_advices,
|
||||
|
||||
} -> page_advices: adw::ViewStackPage {
|
||||
set_title: Some("Mail notification"),
|
||||
set_name: Some("page_advices"),
|
||||
set_icon_name: Some(relm4_icons::icon_names::MAIL),
|
||||
set_title: Some(&i18n("Mail")),
|
||||
set_name: Some(&i18n("page_advices")),
|
||||
set_icon_name: Some(&i18n("mail-symbolic")),
|
||||
|
||||
#[track(model.changed_have_service_advices())]
|
||||
set_visible: model.have_service_advices,
|
||||
|
@ -90,9 +94,9 @@ impl Component for Ready {
|
|||
#[track(model.changed_have_service_tracking())]
|
||||
set_visible: model.have_service_tracking,
|
||||
} -> page_tracking: adw::ViewStackPage {
|
||||
set_title: Some("Shipment tracking"),
|
||||
set_title: Some(&i18n("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())]
|
||||
set_visible: model.have_service_tracking,
|
||||
|
@ -102,8 +106,9 @@ impl Component for Ready {
|
|||
#[track(model.changed_have_service_packstation())]
|
||||
set_visible: model.have_service_packstation,
|
||||
} -> page_packstation: adw::ViewStackPage {
|
||||
set_title: Some("Packstation"),
|
||||
set_title: Some(&i18n("Locker")),
|
||||
set_name: Some("page_packstation"),
|
||||
set_icon_name: Some("parcel-locker-symbolic"),
|
||||
|
||||
#[track(model.changed_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 {
|
||||
} -> page_account: adw::ViewStackPage {
|
||||
set_title: Some("Account"),
|
||||
set_title: Some(&i18n("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 => {
|
||||
self.set_have_service_tracking(true);
|
||||
self.tracking_component.emit(TrackingInput::Search(None))
|
||||
self.tracking_component.emit(TrackingInput::Init);
|
||||
}
|
||||
AccountServices::PackstationAvailable => {
|
||||
self.set_have_service_packstation(true);
|
||||
|
@ -213,6 +218,7 @@ impl Component for Ready {
|
|||
}
|
||||
},
|
||||
ReadyInput::ServiceBorked(service) => match service {
|
||||
// TODO: localization
|
||||
AccountServices::Advices => {
|
||||
self.toast_overlay.add_toast(
|
||||
adw::Toast::builder()
|
||||
|
@ -221,7 +227,7 @@ impl Component for Ready {
|
|||
.build(),
|
||||
);
|
||||
self.set_have_service_advices(false);
|
||||
},
|
||||
}
|
||||
AccountServices::SendungVerfolgung => {
|
||||
self.toast_overlay.add_toast(
|
||||
adw::Toast::builder()
|
||||
|
@ -230,7 +236,7 @@ impl Component for Ready {
|
|||
.build(),
|
||||
);
|
||||
self.set_have_service_tracking(false);
|
||||
},
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
ReadyInput::NavigationPageTemp(page) => {
|
||||
|
@ -256,6 +262,7 @@ impl Component for Ready {
|
|||
fn convert_tracking_output(value: TrackingOutput) -> ReadyInput {
|
||||
match value {
|
||||
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 {
|
||||
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 {
|
||||
use crate::i18n::{i18n, i18n_f};
|
||||
use crate::utils;
|
||||
use adw::prelude::*;
|
||||
use relm4::prelude::*;
|
||||
use relm4::WidgetRef;
|
||||
|
@ -39,6 +41,8 @@ mod r#impl {
|
|||
pub struct Scanner {
|
||||
state: State,
|
||||
in_activation: bool,
|
||||
#[no_eq]
|
||||
error: Option<ErrorInternal>,
|
||||
|
||||
#[do_not_track]
|
||||
camera: Option<aperture::Camera>,
|
||||
|
@ -95,13 +99,8 @@ mod r#impl {
|
|||
add = &page_view_finder -> aperture::Viewfinder {
|
||||
set_detect_codes: true,
|
||||
|
||||
connect_code_detected[sender = sender.clone()] => move |_, code_type, value| {
|
||||
match code_type {
|
||||
aperture::CodeType::Qr => {
|
||||
let _ = sender.output(ScannerOutput::CodeDetected(value.to_string()));
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
connect_code_detected[sender = sender.clone()] => move |_, bytes| {
|
||||
println!("Got connect_code_detected");
|
||||
},
|
||||
|
||||
connect_state_notify[sender = sender.clone()] => move |view_finder|{
|
||||
|
@ -119,17 +118,19 @@ mod r#impl {
|
|||
#[name = "page_error"]
|
||||
add = &adw::Bin {
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::StatusPage {
|
||||
set_title: "Error occured",
|
||||
set_child = &utils::status_page_with_back_button("An error occured", None) -> adw::StatusPage {
|
||||
#[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"]
|
||||
add = &adw::Bin {
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::StatusPage {
|
||||
set_title: "No cameras detected",
|
||||
},
|
||||
set_child: Some(
|
||||
&utils::status_page_with_back_button("No cameras found", None)
|
||||
),
|
||||
},
|
||||
|
||||
#[track(model.changed_state())]
|
||||
|
@ -169,6 +170,7 @@ mod r#impl {
|
|||
let model = Scanner {
|
||||
state: State::Nothing,
|
||||
view_finder,
|
||||
error: None,
|
||||
camera: None,
|
||||
in_activation: false,
|
||||
tracker: 0,
|
||||
|
@ -301,4 +303,32 @@ mod r#impl {
|
|||
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::prelude::*;
|
||||
|
||||
use crate::i18n::{i18n, i18n_f};
|
||||
use crate::login::get_id_token;
|
||||
use crate::LoginSharedState;
|
||||
|
||||
#[tracker::track]
|
||||
pub struct TrackingView {
|
||||
#[do_not_track]
|
||||
factory: FactoryHashMap<String, crate::tracking::ShipmentView>,
|
||||
#[do_not_track]
|
||||
login: LoginSharedState,
|
||||
|
||||
is_init: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TrackingInput {
|
||||
Init,
|
||||
Search(Option<String>),
|
||||
Notification(String),
|
||||
Reset,
|
||||
|
@ -27,6 +34,7 @@ pub enum TrackingCmds {
|
|||
#[derive(Debug)]
|
||||
pub enum TrackingOutput {
|
||||
Borked,
|
||||
NavigationPageTemp(adw::NavigationPage),
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
|
@ -40,27 +48,24 @@ impl Component for TrackingView {
|
|||
#[root]
|
||||
adw::ToastOverlay {
|
||||
#[wrap(Some)]
|
||||
set_child = >k::ScrolledWindow {
|
||||
set_propagate_natural_width: true,
|
||||
#[wrap(Some)]
|
||||
set_child = &adw::Clamp {
|
||||
set_maximum_size: 1800,
|
||||
set_child = >k::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
|
||||
gtk::ScrolledWindow {
|
||||
set_propagate_natural_width: true,
|
||||
|
||||
#[wrap(Some)]
|
||||
set_child = >k::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_margin_start: 6,
|
||||
set_margin_end: 6,
|
||||
set_child = &adw::Clamp {
|
||||
#[wrap(Some)]
|
||||
set_child = >k::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_margin_start: 6,
|
||||
set_margin_end: 6,
|
||||
|
||||
set_margin_bottom: 12,
|
||||
|
||||
adw::Clamp {
|
||||
#[wrap(Some)]
|
||||
set_child = >k::Box {
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_margin_all: 2,
|
||||
|
||||
add_css_class: relm4::css::TOOLBAR,
|
||||
add_css_class: relm4::css::LINKED,
|
||||
set_margin_bottom: 12,
|
||||
|
||||
#[name = "tracking_entry"]
|
||||
gtk::Entry {
|
||||
|
@ -70,21 +75,22 @@ impl Component for TrackingView {
|
|||
|
||||
#[name = "tracking_entry_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]
|
||||
tracking_box -> gtk::Box {
|
||||
set_spacing: 8,
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
},
|
||||
}
|
||||
#[local_ref]
|
||||
tracking_box -> gtk::ListBox {
|
||||
add_css_class: relm4::css::BOXED_LIST,
|
||||
set_selection_mode: gtk::SelectionMode::None,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,6 +104,8 @@ impl Component for TrackingView {
|
|||
let model = TrackingView {
|
||||
factory,
|
||||
login: init,
|
||||
tracker: 0,
|
||||
is_init: false,
|
||||
};
|
||||
|
||||
let tracking_box = model.factory.widget();
|
||||
|
@ -119,12 +127,20 @@ impl Component for TrackingView {
|
|||
});
|
||||
}
|
||||
|
||||
sender.input(TrackingInput::Reset);
|
||||
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>, root: &Self::Root) {
|
||||
self.reset();
|
||||
|
||||
match message {
|
||||
TrackingInput::Init => {
|
||||
self.set_is_init(true);
|
||||
}
|
||||
TrackingInput::Reset => {
|
||||
self.set_is_init(false);
|
||||
self.factory.clear();
|
||||
}
|
||||
TrackingInput::Search(value) => {
|
||||
|
@ -132,9 +148,9 @@ impl Component for TrackingView {
|
|||
if let Some(value) = &value {
|
||||
// https://www.pakete-verfolgen.de/dhl-sendungsnummer/
|
||||
if value.len() < 8 {
|
||||
sender.input(TrackingInput::Notification(
|
||||
"The id is too short to be valid.".to_string(),
|
||||
));
|
||||
sender.input(TrackingInput::Notification(i18n(
|
||||
"The id is too short to be valid.",
|
||||
)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -172,41 +188,46 @@ impl Component for TrackingView {
|
|||
sender: ComponentSender<Self>,
|
||||
root: &Self::Root,
|
||||
) {
|
||||
self.reset();
|
||||
|
||||
match message {
|
||||
TrackingCmds::GotTracking(res) => match res {
|
||||
Ok(shipment_vec) => {
|
||||
if self.is_init {
|
||||
self.set_is_init(false);
|
||||
}
|
||||
for item in shipment_vec {
|
||||
if let Some(err) = item.error {
|
||||
// TODO: gettext
|
||||
if err.id_invalid {
|
||||
sender.input(TrackingInput::Notification(format!(
|
||||
sender.input(TrackingInput::Notification(i18n_f(
|
||||
"The id is invalid ({})",
|
||||
item.id
|
||||
&[&item.id],
|
||||
)));
|
||||
} else if err.letter_not_found {
|
||||
sender.input(TrackingInput::Notification(format!(
|
||||
sender.input(TrackingInput::Notification(i18n_f(
|
||||
"The letter wasn't found ({})",
|
||||
item.id
|
||||
&[&item.id],
|
||||
)));
|
||||
} else if err.id_not_searchable {
|
||||
sender.input(TrackingInput::Notification(format!(
|
||||
sender.input(TrackingInput::Notification(i18n_f(
|
||||
"The id is not searchable ({})",
|
||||
item.id
|
||||
&[&item.id],
|
||||
)));
|
||||
} else if err.data_to_old {
|
||||
sender.input(TrackingInput::Notification(format!(
|
||||
"No data available with id ({}) (data expired)",
|
||||
item.id
|
||||
sender.input(TrackingInput::Notification(i18n_f(
|
||||
"Data was removed with the id ({})",
|
||||
&[&item.id],
|
||||
)));
|
||||
} else if err.not_from_dhl {
|
||||
sender.input(TrackingInput::Notification(format!(
|
||||
sender.input(TrackingInput::Notification(i18n_f(
|
||||
"The id is not from DHL ({})",
|
||||
item.id
|
||||
&[&item.id],
|
||||
)));
|
||||
} else if err.no_data_available {
|
||||
sender.input(TrackingInput::Notification(format!(
|
||||
sender.input(TrackingInput::Notification(i18n_f(
|
||||
"No data available with id ({})",
|
||||
item.id
|
||||
&[&item.id],
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
|
@ -216,9 +237,11 @@ impl Component for TrackingView {
|
|||
}
|
||||
Err(err) => {
|
||||
if err == LibraryError::APIChange {
|
||||
println!("Upstream API for parcel tracking broke");
|
||||
sender.output(TrackingOutput::Borked).unwrap();
|
||||
println!(
|
||||
"Upstream API for parcel tracking broke, assuming Captcha failure"
|
||||
);
|
||||
} else {
|
||||
// TODO: Localize strings
|
||||
sender.input(TrackingInput::Notification(format!(
|
||||
"Unknown Error: {}",
|
||||
err.to_string()
|
||||
|
@ -232,21 +255,10 @@ impl Component for TrackingView {
|
|||
|
||||
struct ShipmentView {
|
||||
model: Shipment,
|
||||
|
||||
// model abstraction
|
||||
have_events: bool,
|
||||
|
||||
// state
|
||||
expanded: bool,
|
||||
|
||||
// workarounds
|
||||
list_box_history: gtk::Box,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ViewInput {
|
||||
ToggleExpand,
|
||||
}
|
||||
pub enum ViewInput {}
|
||||
|
||||
#[relm4::factory]
|
||||
impl FactoryComponent for ShipmentView {
|
||||
|
@ -254,19 +266,15 @@ impl FactoryComponent for ShipmentView {
|
|||
type Init = Shipment;
|
||||
type Output = ();
|
||||
type Input = ViewInput;
|
||||
type ParentWidget = gtk::Box;
|
||||
type ParentWidget = gtk::ListBox;
|
||||
type Index = String;
|
||||
|
||||
view! {
|
||||
#[root]
|
||||
gtk::Box {
|
||||
add_css_class: relm4::css::CARD,
|
||||
set_hexpand: true,
|
||||
set_margin_all: 8,
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
inline_css: "border-radius: 12px 12px 0px 0px",
|
||||
|
||||
// title box
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_hexpand: true,
|
||||
|
@ -282,7 +290,8 @@ impl FactoryComponent for ShipmentView {
|
|||
add_css_class: relm4::css::CAPTION_HEADING,
|
||||
set_halign: gtk::Align::Start,
|
||||
|
||||
set_label: &self.model.id,
|
||||
set_label?: self.subtitle(),
|
||||
set_visible: self.has_subtitle(),
|
||||
},
|
||||
|
||||
gtk::Label {
|
||||
|
@ -290,126 +299,53 @@ impl FactoryComponent for ShipmentView {
|
|||
set_halign: gtk::Align::Start,
|
||||
set_wrap: true,
|
||||
|
||||
set_label: {
|
||||
// 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",
|
||||
}
|
||||
}
|
||||
}
|
||||
set_label: &self.maintitle(),
|
||||
}
|
||||
},
|
||||
|
||||
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(
|
||||
init: Self::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);
|
||||
}
|
||||
fn init_model(init: Self::Init, _: &Self::Index, _: relm4::FactorySender<Self>) -> Self {
|
||||
let _self = ShipmentView { model: init };
|
||||
|
||||
_self
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Self::Input, sender: FactorySender<Self>) {
|
||||
match message {
|
||||
ViewInput::ToggleExpand => {
|
||||
self.expanded = !self.expanded;
|
||||
fn update(&mut self, _: Self::Input, _: FactorySender<Self>) {}
|
||||
}
|
||||
|
||||
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
|
||||
}
|