diff --git a/paket/Cargo.toml b/paket/Cargo.toml index 040853c..e0bfb40 100644 --- a/paket/Cargo.toml +++ b/paket/Cargo.toml @@ -16,4 +16,5 @@ reqwest = "0.12" libpaket = { path = "../libpaket" } glycin = { version = "2.0.0-beta", features = ["gdk4"] } oo7 = { version = "0.3" } -relm4-icons = { version = "0.9" } \ No newline at end of file +relm4-icons = { version = "0.9" } +gtk = { package = "gtk4", version = "0.9", features = ["v4_16"]} \ No newline at end of file diff --git a/paket/src/advice.rs b/paket/src/advice.rs new file mode 100644 index 0000000..00eeee6 --- /dev/null +++ b/paket/src/advice.rs @@ -0,0 +1,119 @@ +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), +} + +#[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, +} + +#[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 = >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(), + }, + + #[wrap(Some)] + set_child = >k::Picture { + #[track(self.changed_texture())] + set_paintable: self.texture.as_ref() + } + } + } + } + + fn init_model(value: Self::Init, _index: &DynamicIndex, sender: FactorySender) -> 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) { + match message { + AdviceCardCmds::GotTexture(texture) => self.set_texture(Some(texture)), + AdviceCardCmds::Error(err) => todo!(), + }; + } +} diff --git a/paket/src/advices.rs b/paket/src/advices.rs index 3d43077..43eee1c 100644 --- a/paket/src/advices.rs +++ b/paket/src/advices.rs @@ -1,123 +1,293 @@ -use adw::{gio, glib}; -use gtk::gdk; -use libpaket::advices::UatToken; -use libpaket::LibraryError; -use relm4::gtk; +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}; + +struct AdviceClientImpl { + uat_token: libpaket::advices::UatToken, + client: libpaket::AdviceClient, +} + +#[derive(Clone, Debug)] +pub struct AdviceClient(Arc>); + +impl AdviceClient { + pub async fn get_image(&self, advice: &libpaket::advices::Advice) -> LibraryResult> { + 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, +} + #[derive(Debug)] -pub struct AppAdviceMetadata { +pub struct AdvicesForDay { + pub advices: Vec, pub date: String, - pub advice: libpaket::advices::Advice, -} - -#[tracker::track] -pub struct AppAdvice { - #[do_not_track] - metadata: AppAdviceMetadata, - texture: Option, -} - -#[derive(Debug)] -pub enum AppAdviceCmds { - GotTexture(gdk::Texture), - Error(LibraryError), } #[relm4::factory(pub)] -impl FactoryComponent for AppAdvice { - type Init = (AppAdviceMetadata, UatToken); +impl FactoryComponent for AdvicesDayView { + type Init = AdvicesForDay; type Input = (); type Output = (); - type CommandOutput = AppAdviceCmds; - type ParentWidget = adw::Carousel; + type CommandOutput = (); + type ParentWidget = gtk::Box; view! { - #[root] - gtk::Overlay { - add_overlay = >k::Spinner { - start: (), - set_align: gtk::Align::Center, + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_margin_all: 16, - #[track(self.changed_texture())] - set_visible: self.texture.is_none(), + 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(), }, - add_overlay = >k::Box { - add_css_class: relm4::css::OSD, - add_css_class: relm4::css::TOOLBAR, - add_css_class: relm4::css::NUMERIC, + self.factory.widget() -> >k::FlowBox { - set_valign: gtk::Align::End, - set_halign: gtk::Align::End, + }, + } + } - set_margin_all: 8, + fn init_model(value: Self::Init, _: &DynamicIndex, sender: FactorySender) -> Self { + let mut factory = FactoryVecDeque::builder().launch_default().detach(); - gtk::Label { - set_label: self.metadata.date.as_str(), + { + 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] +pub struct AdvicesView { + #[do_not_track] + factory: FactoryVecDeque, + state: AdvicesViewState, + + #[do_not_track] + login: crate::LoginSharedState, +} + +#[derive(Debug)] +pub enum AdvicesViewInput { + Fetch, +} + +#[derive(Debug)] +pub enum AdvicesViewCommands { + GotAdvices(LibraryResult>), +} + +#[relm4::component(async, pub)] +impl AsyncComponent for AdvicesView { + type Init = crate::LoginSharedState; + type Input = AdvicesViewInput; + type Output = (); + type CommandOutput = AdvicesViewCommands; + + view! { + gtk::ScrolledWindow { + adw::Clamp { + #[wrap(Some)] + 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 = >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::(), + AdvicesViewState::HaveNone => advices_page_no_available.upcast_ref::(), + AdvicesViewState::HaveSome => advices_page_have_some.upcast_ref::(), + }; + page + }, }, - }, - - #[wrap(Some)] - set_child = >k::Picture { - #[track(self.changed_texture())] - set_paintable: self.texture.as_ref() } } } - fn init_model(value: Self::Init, _index: &DynamicIndex, sender: FactorySender) -> Self { - let _self = Self { - metadata: value.0, - texture: None, + async fn init( + init: Self::Init, + root: Self::Root, + sender: AsyncComponentSender, + ) -> AsyncComponentParts { + let factory = FactoryVecDeque::builder().launch_default().detach(); + + let model = AdvicesView { + factory, + state: AdvicesViewState::Loading, tracker: 0, + login: init, }; - let advice = _self.metadata.advice.clone(); - let uat = value.1; + let widgets = view_output!(); - sender.oneshot_command(async move { - let res = libpaket::advices::AdviceClient::new() - .fetch_advice_image(&advice, &uat) - .await; - - let res = match res { - Ok(res) => res, - Err(err) => return AppAdviceCmds::Error(err), - }; - - 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"); - - AppAdviceCmds::GotTexture(frame.texture()) - }); - - _self + AsyncComponentParts { model, widgets } } - fn update_cmd(&mut self, message: Self::CommandOutput, sender: FactorySender) { + async fn update( + &mut self, + message: Self::Input, + sender: AsyncComponentSender, + root: &Self::Root, + ) { match message { - AppAdviceCmds::GotTexture(texture) => self.set_texture(Some(texture)), - AppAdviceCmds::Error(err) => todo!(), + AdvicesViewInput::Fetch => { + self.set_state(AdvicesViewState::Loading); + + let token = self.login.clone(); + 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 AdvicesViewCommands::GotAdvices(Err(err)), + }; + + let client = libpaket::advices::AdviceClient::new(); + if !advices.has_any_advices() { + return AdvicesViewCommands::GotAdvices(Ok(Vec::new())); + } + + let uat_token = match client.access_token(&advices).await { + Ok(oki) => oki, + Err(err) => return AdvicesViewCommands::GotAdvices(Err(err)), + }; + + let mut arr = Vec::new(); + + let client = AdviceClient::new(uat_token); + + if let Some(current) = advices.get_current_advice() { + let mut advices_arr = Vec::new(); + for item in ¤t.list { + advices_arr.push(Advice::Prod(AdviceProd { + client: client.clone(), + model: item.clone(), + })) + } + + arr.push(AdvicesForDay { + date: current.date.clone(), + advices: advices_arr, + }); + } + + for old_n in advices.get_old_advices() { + 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, + root: &Self::Root, + ) { + match message { + AdvicesViewCommands::GotAdvices(res) => match res { + Ok(arr) => { + { + 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); + } + }, + } + } } diff --git a/paket/src/main.rs b/paket/src/bin/paket.rs similarity index 97% rename from paket/src/main.rs rename to paket/src/bin/paket.rs index 012de53..c385069 100644 --- a/paket/src/main.rs +++ b/paket/src/bin/paket.rs @@ -1,7 +1,7 @@ use std::sync::Arc; -use login::{Login, LoginOutput, LoginSharedState}; -use ready::{Ready, ReadyOutput}; +use paket::login::{new_login_shared_state, Login, LoginOutput}; +use paket::ready::{Ready, ReadyOutput}; use relm4::{ RELM_THREADS, adw, gtk, main_adw_application, prelude::*, tokio::sync::Mutex, @@ -10,11 +10,6 @@ use relm4::{ use gtk::prelude::*; use adw::{glib, prelude::*}; -mod advices; -mod constants; -mod login; -mod ready; -mod tracking; #[derive(Debug, PartialEq)] enum AppState { @@ -172,7 +167,7 @@ impl AsyncComponent for App { root: Self::Root, sender: AsyncComponentSender, ) -> AsyncComponentParts { - let login_shared_state = Arc::new(Mutex::new(Arc::new(SharedState::new()))); + let login_shared_state = new_login_shared_state(); let ready = Ready::builder() .launch(login_shared_state.clone()) @@ -297,6 +292,6 @@ fn main() { 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(constants::APP_ID); + let app = RelmApp::new(paket::constants::APP_ID); app.run_async::(()); } diff --git a/paket/src/lib.rs b/paket/src/lib.rs new file mode 100644 index 0000000..adf372c --- /dev/null +++ b/paket/src/lib.rs @@ -0,0 +1,8 @@ +pub mod advice; +pub mod advices; +pub mod constants; +pub mod login; +pub mod ready; +pub mod tracking; + +pub use login::LoginSharedState; diff --git a/paket/src/login.rs b/paket/src/login.rs index 903cddf..6bb23f2 100644 --- a/paket/src/login.rs +++ b/paket/src/login.rs @@ -42,6 +42,10 @@ pub struct LoginFlowModel { pub type LoginSharedState = Arc>>>>; +pub fn new_login_shared_state() -> LoginSharedState { + Arc::new(Mutex::new(Arc::new(SharedState::new()))) +} + pub async fn get_id_token(value: &LoginSharedState) -> Option { let mutex_guard = value.lock().await; let shared_state_guard = mutex_guard.read(); diff --git a/paket/src/ready.rs b/paket/src/ready.rs index f5d4377..17127f1 100644 --- a/paket/src/ready.rs +++ b/paket/src/ready.rs @@ -17,14 +17,9 @@ use relm4::{ prelude::*, }; -use crate::advices::AppAdviceMetadata; +use crate::advices::{AdvicesViewInput, AdvicesView}; + -#[derive(Debug, PartialEq)] -pub enum ReadyAdvicesState { - Loading, - HaveNone, - HaveSome, -} #[tracker::track] pub struct Ready { @@ -34,9 +29,7 @@ pub struct Ready { have_service_advices: bool, #[do_not_track] - advices_factory: FactoryVecDeque, - advices_state: ReadyAdvicesState, - + advices_component: AsyncController, #[do_not_track] tracking_factory: FactoryHashMap, } @@ -55,8 +48,6 @@ pub enum ReadyCmds { LoggedIn, LoggedOut, GotCustomerDataFull(LibraryResult), - RetryAdvices, - GotAdvices((LibraryResult>, Option)), GotTracking(LibraryResult>), } @@ -79,48 +70,10 @@ impl Component for Ready { view! { #[root] adw::ViewStack { - add = &adw::Bin { - #[wrap(Some)] - set_child = &adw::ViewStack { - #[name = "advices_page_loading"] - add = &adw::StatusPage { - set_title: "Loading mail notifications...", + add = model.advices_component.widget() -> >k::ScrolledWindow { + /*#[track(model.changed_have_service_advices())] + set_visible: model.have_service_advices,*/ - }, - #[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::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: >k::Widget = match model.advices_state { - ReadyAdvicesState::Loading => advices_page_loading.upcast_ref::(), - ReadyAdvicesState::HaveNone => advices_page_no_available.upcast_ref::(), - ReadyAdvicesState::HaveSome => advices_page_have_some.upcast_ref::(), - }; - page - }, - }, } -> /*page_advices: adw::ViewStackPage*/ { set_title: Some("Mail notification"), set_name: Some("page_advices"), @@ -157,7 +110,7 @@ impl Component for Ready { }, } } - } -> /*page_tracking: adw::ViewStackPage*/ { + } -> { set_title: Some("Shipment tracking"), set_name: Some("page_tracking"), }, @@ -169,21 +122,18 @@ impl Component for Ready { root: Self::Root, sender: ComponentSender, ) -> ComponentParts { - let advices_factory = FactoryVecDeque::builder().launch_default().detach(); - let tracking_factory = FactoryHashMap::builder().launch_default().detach(); + let advices_component = AdvicesView::builder().launch(init.clone()).detach(); let model = Ready { have_service_advices: false, - advices_factory, - advices_state: ReadyAdvicesState::Loading, login: init.clone(), activate: false, tracking_factory, + advices_component, tracker: 0, }; - let advices_carousel = model.advices_factory.widget(); let tracking_box = model.tracking_factory.widget(); let widgets = view_output!(); @@ -280,38 +230,8 @@ impl Component for Ready { }); } ReadyInput::HaveAdvicesService => { - let token = self.login.clone(); - 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)), - } - }) + self.have_service_advices = true; + self.advices_component.emit(AdvicesViewInput::Fetch); } } } @@ -403,48 +323,7 @@ impl Component for Ready { 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, - libpaket_advice: &AdvicesList, -) { - for i in &libpaket_advice.list { - vec.push(AppAdviceMetadata { - date: libpaket_advice.date.clone(), - advice: i.clone(), - }); - } -}