Compare commits

..

No commits in common. "ea41aa43d83fb398816ddc8d4360da4c172c6b8f" and "0b2ae19085db8dfe0278141bed85acb6ac320533" have entirely different histories.

8 changed files with 242 additions and 418 deletions

View file

@ -184,7 +184,7 @@ macro_rules! parse_json_response {
let mut unused = std::collections::BTreeSet::new(); let mut unused = std::collections::BTreeSet::new();
println!("res({}): {}", stringify!($type), res); println!("res({}): {}", stringify!($type), res);
let res: Result<$type, _> = serde_ignored::deserialize(jd, |path| { let res: Result<$type,_> = serde_ignored::deserialize(jd, |path| {
unused.insert(path.to_string()); unused.insert(path.to_string());
}); });
println!("res({}): {:?}", stringify!($type), unused); println!("res({}): {:?}", stringify!($type), unused);
@ -205,7 +205,7 @@ macro_rules! parse_json_response_from_apiresult {
let mut unused = std::collections::BTreeSet::new(); let mut unused = std::collections::BTreeSet::new();
println!("res({}): {}", stringify!($type), res); println!("res({}): {}", stringify!($type), res);
let res: Result<APIResult<$type>, _> = serde_ignored::deserialize(jd, |path| { let res: Result<APIResult<$type>,_> = serde_ignored::deserialize(jd, |path| {
unused.insert(path.to_string()); unused.insert(path.to_string());
}); });
println!("res({}): {:?}", stringify!($type), unused); println!("res({}): {:?}", stringify!($type), unused);

View file

@ -17,4 +17,3 @@ libpaket = { path = "../libpaket" }
glycin = { version = "2.0.0-beta", features = ["gdk4"] } glycin = { version = "2.0.0-beta", features = ["gdk4"] }
oo7 = { version = "0.3" } oo7 = { version = "0.3" }
relm4-icons = { version = "0.9" } relm4-icons = { version = "0.9" }
gtk = { package = "gtk4", version = "0.9", features = ["v4_16"]}

View file

@ -1,119 +0,0 @@
use libpaket::LibraryError;
use relm4::{adw, gtk, gtk::gdk, gtk::gio, gtk::glib};
use adw::prelude::*;
use relm4::prelude::*;
use crate::advices::AdviceClient;
#[derive(Clone, Debug)]
pub enum Advice {
Prod(AdviceProd),
UITest(Vec<u8>),
}
#[derive(Clone, Debug)]
pub struct AdviceProd {
pub client: AdviceClient,
pub model: libpaket::advices::Advice,
}
#[tracker::track]
pub struct AdviceCard {
#[do_not_track]
metadata: Advice,
texture: Option<gdk::Texture>,
}
#[derive(Debug)]
pub enum AdviceCardCmds {
GotTexture(gdk::Texture),
Error(LibraryError),
}
#[relm4::factory(pub)]
impl FactoryComponent for AdviceCard {
type Init = Advice;
type Input = ();
type Output = ();
type CommandOutput = AdviceCardCmds;
type ParentWidget = gtk::FlowBox;
view! {
#[root]
gtk::FlowBoxChild {
set_halign: gtk::Align::Start,
set_valign: gtk::Align::Start,
#[wrap(Some)]
set_child = &gtk::Overlay {
set_margin_all: 8,
add_overlay = &gtk::Spinner {
start: (),
set_align: gtk::Align::Center,
#[track(self.changed_texture())]
set_visible: self.texture.is_none(),
},
#[wrap(Some)]
set_child = &gtk::Picture {
#[track(self.changed_texture())]
set_paintable: self.texture.as_ref()
}
}
}
}
fn init_model(value: Self::Init, _index: &DynamicIndex, sender: FactorySender<Self>) -> Self {
let _self = Self {
metadata: value.clone(),
texture: None,
tracker: 0,
};
sender.oneshot_command(async move {
let res = match value {
Advice::Prod(value) => {
let res = value.client.get_image(&value.model).await;
match res {
Ok(res) => res,
Err(err) => return AdviceCardCmds::Error(err),
}
}
Advice::UITest(value) => value,
};
let file = {
let (file, io_stream) = gio::File::new_tmp(None::<&std::path::Path>).unwrap();
let output_stream = io_stream.output_stream();
output_stream
.write(res.as_slice(), None::<&gio::Cancellable>)
.unwrap();
file
};
let image = glycin::Loader::new(file)
.load()
.await
.expect("Image decoding failed");
let frame = image
.next_frame()
.await
.expect("Image frame decoding failed");
AdviceCardCmds::GotTexture(frame.texture())
});
_self
}
fn update_cmd(&mut self, message: Self::CommandOutput, sender: FactorySender<Self>) {
match message {
AdviceCardCmds::GotTexture(texture) => self.set_texture(Some(texture)),
AdviceCardCmds::Error(err) => todo!(),
};
}
}

View file

@ -1,293 +1,123 @@
use std::collections::HashMap; use adw::{gio, glib};
use std::sync::Arc; use gtk::gdk;
use libpaket::advices::UatToken;
use futures::lock::Mutex; use libpaket::LibraryError;
use libpaket::{AdviceClient as LibraryAdviceClient, LibraryResult}; use relm4::gtk;
use adw::prelude::*; use adw::prelude::*;
use gio::prelude::*; use gio::prelude::*;
use glib::prelude::*; use glib::prelude::*;
use gtk::{gio, glib};
use relm4::prelude::*; use relm4::prelude::*;
use relm4::prelude::*;
use relm4::factory::FactoryVecDeque;
use crate::advice::{Advice, AdviceCard, AdviceProd};
struct AdviceClientImpl {
uat_token: libpaket::advices::UatToken,
client: libpaket::AdviceClient,
}
#[derive(Clone, Debug)]
pub struct AdviceClient(Arc<Mutex<AdviceClientImpl>>);
impl AdviceClient {
pub async fn get_image(&self, advice: &libpaket::advices::Advice) -> LibraryResult<Vec<u8>> {
let lock = self.0.lock().await;
lock.client
.fetch_advice_image(advice, &lock.uat_token)
.await
}
fn new(uat_token: libpaket::advices::UatToken) -> Self {
Self(Arc::new(Mutex::new(AdviceClientImpl {
uat_token,
client: LibraryAdviceClient::new(),
})))
}
}
pub struct AdvicesDayView {
date: String,
factory: FactoryVecDeque<AdviceCard>,
}
#[derive(Debug)] #[derive(Debug)]
pub struct AdvicesForDay { pub struct AppAdviceMetadata {
pub advices: Vec<Advice>,
pub date: String, pub date: String,
} pub advice: libpaket::advices::Advice,
#[relm4::factory(pub)]
impl FactoryComponent for AdvicesDayView {
type Init = AdvicesForDay;
type Input = ();
type Output = ();
type CommandOutput = ();
type ParentWidget = gtk::Box;
view! {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_margin_all: 16,
gtk::Label {
add_css_class: relm4::css::TITLE_4,
set_halign: gtk::Align::Start,
set_margin_bottom: 4,
set_margin_start: 8,
set_label: self.date.as_str(),
},
self.factory.widget() -> &gtk::FlowBox {
},
}
}
fn init_model(value: Self::Init, _: &DynamicIndex, sender: FactorySender<Self>) -> Self {
let mut factory = FactoryVecDeque::builder().launch_default().detach();
{
let mut guard = factory.guard();
for item in value.advices {
guard.push_back(item);
}
};
let _self = Self {
date: value.date,
factory,
};
_self
}
}
#[derive(Debug, PartialEq)]
pub enum AdvicesViewState {
Loading,
HaveNone,
HaveSome,
} }
#[tracker::track] #[tracker::track]
pub struct AdvicesView { pub struct AppAdvice {
#[do_not_track] #[do_not_track]
factory: FactoryVecDeque<AdvicesDayView>, metadata: AppAdviceMetadata,
state: AdvicesViewState, texture: Option<gdk::Texture>,
#[do_not_track]
login: crate::LoginSharedState,
} }
#[derive(Debug)] #[derive(Debug)]
pub enum AdvicesViewInput { pub enum AppAdviceCmds {
Fetch, GotTexture(gdk::Texture),
Error(LibraryError),
} }
#[derive(Debug)] #[relm4::factory(pub)]
pub enum AdvicesViewCommands { impl FactoryComponent for AppAdvice {
GotAdvices(LibraryResult<Vec<AdvicesForDay>>), type Init = (AppAdviceMetadata, UatToken);
} type Input = ();
#[relm4::component(async, pub)]
impl AsyncComponent for AdvicesView {
type Init = crate::LoginSharedState;
type Input = AdvicesViewInput;
type Output = (); type Output = ();
type CommandOutput = AdvicesViewCommands; type CommandOutput = AppAdviceCmds;
type ParentWidget = adw::Carousel;
view! { view! {
gtk::ScrolledWindow { #[root]
adw::Clamp { gtk::Overlay {
add_overlay = &gtk::Spinner {
start: (),
set_align: gtk::Align::Center,
#[track(self.changed_texture())]
set_visible: self.texture.is_none(),
},
add_overlay = &gtk::Box {
add_css_class: relm4::css::OSD,
add_css_class: relm4::css::TOOLBAR,
add_css_class: relm4::css::NUMERIC,
set_valign: gtk::Align::End,
set_halign: gtk::Align::End,
set_margin_all: 8,
gtk::Label {
set_label: self.metadata.date.as_str(),
},
},
#[wrap(Some)] #[wrap(Some)]
set_child = &adw::ViewStack { set_child = &gtk::Picture {
#[name = "advices_page_loading"] #[track(self.changed_texture())]
add = &adw::StatusPage { set_paintable: self.texture.as_ref()
set_title: "Loading mail notifications...",
},
#[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 = &gtk::ScrolledWindow {
#[wrap(Some)]
set_child = model.factory.widget() -> &gtk::Box {
set_orientation: gtk::Orientation::Vertical,
},
},
},
#[track(model.changed_state())]
set_visible_child: {
let page: &gtk::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( fn init_model(value: Self::Init, _index: &DynamicIndex, sender: FactorySender<Self>) -> Self {
init: Self::Init, let _self = Self {
root: Self::Root, metadata: value.0,
sender: AsyncComponentSender<Self>, texture: None,
) -> AsyncComponentParts<Self> {
let factory = FactoryVecDeque::builder().launch_default().detach();
let model = AdvicesView {
factory,
state: AdvicesViewState::Loading,
tracker: 0, tracker: 0,
login: init,
}; };
let widgets = view_output!(); let advice = _self.metadata.advice.clone();
let uat = value.1;
AsyncComponentParts { model, widgets }
}
async fn update(
&mut self,
message: Self::Input,
sender: AsyncComponentSender<Self>,
root: &Self::Root,
) {
match message {
AdvicesViewInput::Fetch => {
self.set_state(AdvicesViewState::Loading);
let token = self.login.clone();
sender.oneshot_command(async move { sender.oneshot_command(async move {
// fetching advices let res = libpaket::advices::AdviceClient::new()
let dhli_token = crate::login::get_id_token(&token).await.unwrap(); .fetch_advice_image(&advice, &uat)
let advices = libpaket::WebClient::new().advices(&dhli_token).await; .await;
let advices = match advices {
let res = match res {
Ok(res) => res, Ok(res) => res,
Err(err) => return AdvicesViewCommands::GotAdvices(Err(err)), Err(err) => return AppAdviceCmds::Error(err),
}; };
let client = libpaket::advices::AdviceClient::new(); let file = {
if !advices.has_any_advices() { let (file, io_stream) = gio::File::new_tmp(None::<&std::path::Path>).unwrap();
return AdvicesViewCommands::GotAdvices(Ok(Vec::new())); let output_stream = io_stream.output_stream();
} output_stream
.write(res.as_slice(), None::<&gio::Cancellable>)
let uat_token = match client.access_token(&advices).await { .unwrap();
Ok(oki) => oki, file
Err(err) => return AdvicesViewCommands::GotAdvices(Err(err)),
}; };
let mut arr = Vec::new(); let image = glycin::Loader::new(file)
.load()
.await
.expect("Image decoding failed");
let frame = image
.next_frame()
.await
.expect("Image frame decoding failed");
let client = AdviceClient::new(uat_token); AppAdviceCmds::GotTexture(frame.texture())
if let Some(current) = advices.get_current_advice() {
let mut advices_arr = Vec::new();
for item in &current.list {
advices_arr.push(Advice::Prod(AdviceProd {
client: client.clone(),
model: item.clone(),
}))
}
arr.push(AdvicesForDay {
date: current.date.clone(),
advices: advices_arr,
}); });
_self
} }
for old_n in advices.get_old_advices() { fn update_cmd(&mut self, message: Self::CommandOutput, sender: FactorySender<Self>) {
let mut advices_arr = Vec::new();
for item in &old_n.list {
advices_arr.push(Advice::Prod(AdviceProd {
client: client.clone(),
model: item.clone(),
}))
}
arr.push(AdvicesForDay {
date: old_n.date.clone(),
advices: advices_arr,
});
}
AdvicesViewCommands::GotAdvices(Ok(arr))
})
}
};
}
async fn update_cmd(
&mut self,
message: Self::CommandOutput,
sender: AsyncComponentSender<Self>,
root: &Self::Root,
) {
match message { match message {
AdvicesViewCommands::GotAdvices(res) => match res { AppAdviceCmds::GotTexture(texture) => self.set_texture(Some(texture)),
Ok(arr) => { AppAdviceCmds::Error(err) => todo!(),
{ };
let mut guard = self.factory.guard();
guard.clear();
}
if arr.len() == 0 {
self.set_state(AdvicesViewState::HaveNone);
} else {
self.set_state(AdvicesViewState::HaveSome);
let mut guard = self.factory.guard();
for item in arr {
guard.push_back(item);
}
}
}
Err(err) => {
println!("{:?}", err);
}
},
}
} }
} }

View file

@ -1,8 +0,0 @@
pub mod advice;
pub mod advices;
pub mod constants;
pub mod login;
pub mod ready;
pub mod tracking;
pub use login::LoginSharedState;

View file

@ -42,10 +42,6 @@ pub struct LoginFlowModel {
pub type LoginSharedState = Arc<Mutex<Arc<SharedState<Option<DHLIdToken>>>>>; pub type LoginSharedState = Arc<Mutex<Arc<SharedState<Option<DHLIdToken>>>>>;
pub fn new_login_shared_state() -> LoginSharedState {
Arc::new(Mutex::new(Arc::new(SharedState::new())))
}
pub async fn get_id_token(value: &LoginSharedState) -> Option<DHLIdToken> { pub async fn get_id_token(value: &LoginSharedState) -> Option<DHLIdToken> {
let mutex_guard = value.lock().await; let mutex_guard = value.lock().await;
let shared_state_guard = mutex_guard.read(); let shared_state_guard = mutex_guard.read();

View file

@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use paket::login::{new_login_shared_state, Login, LoginOutput}; use login::{Login, LoginOutput, LoginSharedState};
use paket::ready::{Ready, ReadyOutput}; use ready::{Ready, ReadyOutput};
use relm4::{ use relm4::{
RELM_THREADS, RELM_THREADS,
adw, gtk, main_adw_application, prelude::*, tokio::sync::Mutex, adw, gtk, main_adw_application, prelude::*, tokio::sync::Mutex,
@ -10,6 +10,11 @@ use relm4::{
use gtk::prelude::*; use gtk::prelude::*;
use adw::{glib, prelude::*}; use adw::{glib, prelude::*};
mod advices;
mod constants;
mod login;
mod ready;
mod tracking;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
enum AppState { enum AppState {
@ -167,7 +172,7 @@ impl AsyncComponent for App {
root: Self::Root, root: Self::Root,
sender: AsyncComponentSender<Self>, sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> { ) -> AsyncComponentParts<Self> {
let login_shared_state = new_login_shared_state(); let login_shared_state = Arc::new(Mutex::new(Arc::new(SharedState::new())));
let ready = Ready::builder() let ready = Ready::builder()
.launch(login_shared_state.clone()) .launch(login_shared_state.clone())
@ -292,6 +297,6 @@ fn main() {
theme.add_resource_path("/de/j4ne/Paket/icons/"); theme.add_resource_path("/de/j4ne/Paket/icons/");
theme.add_resource_path("/de/j4ne/Paket/scalable/actions/"); theme.add_resource_path("/de/j4ne/Paket/scalable/actions/");
relm4_icons::initialize_icons(); relm4_icons::initialize_icons();
let app = RelmApp::new(paket::constants::APP_ID); let app = RelmApp::new(constants::APP_ID);
app.run_async::<App>(()); app.run_async::<App>(());
} }

View file

@ -17,9 +17,14 @@ use relm4::{
prelude::*, prelude::*,
}; };
use crate::advices::{AdvicesViewInput, AdvicesView}; use crate::advices::AppAdviceMetadata;
#[derive(Debug, PartialEq)]
pub enum ReadyAdvicesState {
Loading,
HaveNone,
HaveSome,
}
#[tracker::track] #[tracker::track]
pub struct Ready { pub struct Ready {
@ -29,7 +34,9 @@ pub struct Ready {
have_service_advices: bool, have_service_advices: bool,
#[do_not_track] #[do_not_track]
advices_component: AsyncController<AdvicesView>, advices_factory: FactoryVecDeque<crate::advices::AppAdvice>,
advices_state: ReadyAdvicesState,
#[do_not_track] #[do_not_track]
tracking_factory: FactoryHashMap<String, crate::tracking::ShipmentView>, tracking_factory: FactoryHashMap<String, crate::tracking::ShipmentView>,
} }
@ -48,6 +55,8 @@ pub enum ReadyCmds {
LoggedIn, LoggedIn,
LoggedOut, LoggedOut,
GotCustomerDataFull(LibraryResult<libpaket::stammdaten::CustomerDataFull>), GotCustomerDataFull(LibraryResult<libpaket::stammdaten::CustomerDataFull>),
RetryAdvices,
GotAdvices((LibraryResult<Vec<AppAdviceMetadata>>, Option<UatToken>)),
GotTracking(LibraryResult<Vec<Shipment>>), GotTracking(LibraryResult<Vec<Shipment>>),
} }
@ -70,10 +79,48 @@ impl Component for Ready {
view! { view! {
#[root] #[root]
adw::ViewStack { adw::ViewStack {
add = model.advices_component.widget() -> &gtk::ScrolledWindow { add = &adw::Bin {
/*#[track(model.changed_have_service_advices())] #[wrap(Some)]
set_visible: model.have_service_advices,*/ set_child = &adw::ViewStack {
#[name = "advices_page_loading"]
add = &adw::StatusPage {
set_title: "Loading mail notifications...",
},
#[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 = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
#[local_ref]
advices_carousel -> adw::Carousel {
set_orientation: gtk::Orientation::Vertical,
},
adw::CarouselIndicatorDots {
#[watch]
set_carousel: Some(advices_carousel),
set_orientation: gtk::Orientation::Vertical,
}
},
},
#[track(model.changed_advices_state())]
set_visible_child: {
let page: &gtk::Widget = match model.advices_state {
ReadyAdvicesState::Loading => advices_page_loading.upcast_ref::<gtk::Widget>(),
ReadyAdvicesState::HaveNone => advices_page_no_available.upcast_ref::<gtk::Widget>(),
ReadyAdvicesState::HaveSome => advices_page_have_some.upcast_ref::<gtk::Widget>(),
};
page
},
},
} -> /*page_advices: adw::ViewStackPage*/ { } -> /*page_advices: adw::ViewStackPage*/ {
set_title: Some("Mail notification"), set_title: Some("Mail notification"),
set_name: Some("page_advices"), set_name: Some("page_advices"),
@ -110,7 +157,7 @@ impl Component for Ready {
}, },
} }
} }
} -> { } -> /*page_tracking: adw::ViewStackPage*/ {
set_title: Some("Shipment tracking"), set_title: Some("Shipment tracking"),
set_name: Some("page_tracking"), set_name: Some("page_tracking"),
}, },
@ -122,18 +169,21 @@ impl Component for Ready {
root: Self::Root, root: Self::Root,
sender: ComponentSender<Self>, sender: ComponentSender<Self>,
) -> ComponentParts<Self> { ) -> ComponentParts<Self> {
let advices_factory = FactoryVecDeque::builder().launch_default().detach();
let tracking_factory = FactoryHashMap::builder().launch_default().detach(); let tracking_factory = FactoryHashMap::builder().launch_default().detach();
let advices_component = AdvicesView::builder().launch(init.clone()).detach();
let model = Ready { let model = Ready {
have_service_advices: false, have_service_advices: false,
advices_factory,
advices_state: ReadyAdvicesState::Loading,
login: init.clone(), login: init.clone(),
activate: false, activate: false,
tracking_factory, tracking_factory,
advices_component,
tracker: 0, tracker: 0,
}; };
let advices_carousel = model.advices_factory.widget();
let tracking_box = model.tracking_factory.widget(); let tracking_box = model.tracking_factory.widget();
let widgets = view_output!(); let widgets = view_output!();
@ -230,8 +280,38 @@ impl Component for Ready {
}); });
} }
ReadyInput::HaveAdvicesService => { ReadyInput::HaveAdvicesService => {
self.have_service_advices = true; let token = self.login.clone();
self.advices_component.emit(AdvicesViewInput::Fetch); sender.oneshot_command(async move {
// fetching advices
let dhli_token = crate::login::get_id_token(&token).await.unwrap();
let advices = libpaket::WebClient::new().advices(&dhli_token).await;
let advices = match advices {
Ok(res) => res,
Err(err) => return ReadyCmds::GotAdvices((Err(err), None)),
};
let mut advices_vec = Vec::new();
if let Some(current) = advices.get_current_advice() {
push_advice_from_libpaket_advice(&mut advices_vec, current);
}
for old_n in advices.get_old_advices() {
push_advice_from_libpaket_advice(&mut advices_vec, old_n);
}
if advices_vec.len() == 0 {
return ReadyCmds::GotAdvices((Ok(advices_vec), None));
}
match libpaket::advices::AdviceClient::new()
.access_token(&advices)
.await
{
Ok(uat_token) => ReadyCmds::GotAdvices((Ok(advices_vec), Some(uat_token))),
Err(err) => ReadyCmds::GotAdvices((Err(err), None)),
}
})
} }
} }
} }
@ -323,7 +403,48 @@ impl Component for Ready {
sender.output(ReadyOutput::Error(err)).unwrap(); sender.output(ReadyOutput::Error(err)).unwrap();
} }
}, },
ReadyCmds::GotAdvices(res) => match res.0 {
Ok(advices_vec) => {
{
let mut guard = self.advices_factory.guard();
guard.clear();
}
if advices_vec.len() == 0 {
self.set_advices_state(ReadyAdvicesState::HaveNone);
} else {
self.set_advices_state(ReadyAdvicesState::HaveSome);
let uat_token = res.1.unwrap();
let mut guard = self.advices_factory.guard();
guard.clear();
for i in advices_vec {
guard.push_back((i, uat_token.clone()));
}
}
}
Err(err) => {
sender.output(ReadyOutput::Error(err)).unwrap();
sender.oneshot_command(async {
relm4::tokio::time::sleep(Duration::from_secs(30)).await;
ReadyCmds::RetryAdvices
});
}
},
ReadyCmds::RetryAdvices => {
sender.input(ReadyInput::HaveAdvicesService);
}
} }
} }
} }
fn push_advice_from_libpaket_advice(
vec: &mut Vec<AppAdviceMetadata>,
libpaket_advice: &AdvicesList,
) {
for i in &libpaket_advice.list {
vec.push(AppAdviceMetadata {
date: libpaket_advice.date.clone(),
advice: i.clone(),
});
}
}