1use std::fmt::Write as _;
2
3use zng_ext_l10n::l10n;
4use zng_ext_setup::{SETUP, SetupOpStatus, SetupStatus, task::SetupTaskError};
5use zng_wgt::{ICONS, Wgt, align, prelude::*};
6use zng_wgt_container::{Container, child_out_bottom};
7use zng_wgt_fill::background;
8use zng_wgt_filter::opacity;
9use zng_wgt_progress::{Progress, ProgressView};
10use zng_wgt_size_offset::{min_height, size, y};
11use zng_wgt_stack::{Stack, StackDirection};
12use zng_wgt_text::{Text, icon::ico_color};
13use zng_wgt_text_input::selectable::SelectableText;
14use zng_wgt_wizard::{Page, PageArgs};
15
16use crate::{APP_NAME_VAR, SETUP_OP_VAR, SetupOp, page::TaskInfo};
17
18#[non_exhaustive]
20pub struct StatusPage {
21 pub status: Var<SetupStatus>,
25
26 pub task_infos: Vec<TaskInfo>,
34
35 pub task_info_fn: WidgetFn<StatusTaskInfoFnArgs>,
37}
38impl Default for StatusPage {
39 fn default() -> Self {
40 Self {
41 status: SETUP.status(),
42 task_infos: vec![],
43 task_info_fn: WidgetFn::new(default_task_info_fn),
44 }
45 }
46}
47impl StatusPage {
48 pub fn new() -> Self {
50 Self::default()
51 }
52
53 pub fn push_info(&mut self, info: TaskInfo) {
55 if let Some(i) = self.task_infos.iter().position(|t| t.id == info.id) {
56 tracing::debug!("task info for {:?} replaced", info.id);
57 self.task_infos.remove(i);
58 }
59 self.task_infos.push(info);
60 }
61
62 pub fn with_info(mut self, info: TaskInfo) -> Self {
64 self.push_info(info);
65 self
66 }
67
68 pub fn build(self) -> Page {
70 let title = SETUP_OP_VAR.flat_map(|op| match op {
71 SetupOp::Install => l10n!("status/title.install", "Installing"),
72 SetupOp::Update => l10n!("status/title.update", "Updating"),
73 SetupOp::Repair => l10n!("status/title.repair", "Repairing"),
74 SetupOp::Uninstall => l10n!("status/title.uninstall", "Uninstalling"),
75 });
76 let info = SETUP_OP_VAR.flat_map(|op| match op {
77 SetupOp::Install => l10n!(
78 "status/info.install",
79 "Please wait while {$app} is installed on your computer.",
80 app = APP_NAME_VAR
81 ),
82 SetupOp::Update => l10n!(
83 "status/info.update",
84 "Please wait while {$app} is updated on your computer.",
85 app = APP_NAME_VAR
86 ),
87 SetupOp::Repair => l10n!(
88 "status/info.repair",
89 "Please wait while {$app} is repaired on your computer.",
90 app = APP_NAME_VAR
91 ),
92 SetupOp::Uninstall => l10n!(
93 "status/info.uninstall",
94 "Please wait while {$app} is uninstalled from your computer.",
95 app = APP_NAME_VAR
96 ),
97 });
98 let mut pg = Page::new(
99 title,
100 info,
101 wgt_fn!(|_| build(self.status.clone(), self.task_infos.clone(), self.task_info_fn.clone())),
102 );
103 pg.side = WidgetFn::nil();
104 pg.footer = wgt_fn!(|a: PageArgs| {
105 let id = a.wizard_id();
106 zng_wgt_wizard::default_page_footer_cancel(id)
107 });
108 pg
109 }
110}
111
112fn build(status: Var<SetupStatus>, tasks: Vec<TaskInfo>, item_fn: WidgetFn<StatusTaskInfoFnArgs>) -> UiNode {
113 let op_status = expr_var! {
115 match #{SETUP_OP_VAR} {
116 SetupOp::Install | SetupOp::Update | SetupOp::Repair => match #{status} {
117 SetupStatus::PrepareInstall(op) => Some((true, op.clone())),
118 SetupStatus::CommitInstall(op) => Some((false, op.clone())),
119 _ => None,
120 },
121 SetupOp::Uninstall => match #{status} {
122 SetupStatus::ValidateUninstall(op) => Some((true, op.clone())),
123 SetupStatus::Uninstall(op) => Some((false, op.clone())),
124 _ => None,
125 },
126 }
127 };
128
129 if tasks.len() <= 1 {
130 Stack! {
131 direction = StackDirection::top_to_bottom();
132 children = ui_vec![
133 Text! {
134 txt = op_status.map(|s| match s {
135 Some((_, s)) if s.progress.1 > 1 => formatx!("{} / {}", s.progress.0, s.progress.1),
136 _ => "".into(),
137 });
138 txt_align = Align::CENTER;
139 font_size = 1.2.em();
140 },
141 ProgressView! {
142 align = Align::FILL_TOP;
143 progress = op_status.flat_map(|s| match s {
144 Some((_, s)) => s.task_progress.0.clone(),
145 None => const_var(Progress::indeterminate()),
146 });
147 }
148 ];
149 spacing = 5;
150 }
151 } else {
152 Stack! {
153 direction = StackDirection::top_to_bottom();
154 spacing = 5;
155 children = list_presenter_from_iter(
156 tasks,
157 wgt_fn!(|a| item_fn(StatusTaskInfoFnArgs::new(a, &op_status))),
158 );
159 }
160 }
161}
162
163#[derive(Clone)]
165pub struct StatusTaskInfoFnArgs {
166 pub info: TaskInfo,
168
169 status: Var<Option<(bool, SetupOpStatus)>>,
170}
171impl StatusTaskInfoFnArgs {
172 fn new(info: TaskInfo, status: &Var<Option<(bool, SetupOpStatus)>>) -> Self {
173 Self {
174 info,
175 status: status.clone(),
176 }
177 }
178
179 pub fn progress(&self) -> Var<Progress> {
181 let mut progress = const_var(Progress::from_n_of(0, 1));
182 let id = self.info.id.clone();
183 self.status.flat_map(move |s| {
184 if let Some((_, s)) = s
185 && s.task == id
186 {
187 progress = s.task_progress.0.clone();
188 }
189 progress.clone()
190 })
191 }
192
193 pub fn is_preparing(&self) -> Var<bool> {
197 self.status.map(|s| if let Some((is_prep, _)) = s { *is_prep } else { true })
198 }
199
200 pub fn is_cancelling(&self) -> Var<bool> {
202 self.status.map(|s| if let Some((_, s)) = s { s.cancel } else { false })
203 }
204
205 pub fn errors(&self) -> Var<Vec<SetupTaskError>> {
207 let id = self.info.id.clone();
208 self.status.map(move |s| {
209 if let Some((_, s)) = s {
210 s.errors
211 .iter()
212 .filter_map(|((_, ty, name), e)| if *ty == id.0 && *name == id.1 { Some(e.clone()) } else { None })
213 .collect()
214 } else {
215 vec![]
216 }
217 })
218 }
219
220 pub fn is_success(&self) -> Var<bool> {
222 let mut progress = const_var(false);
223 let id = self.info.id.clone();
224 self.status.flat_map(move |s| {
225 if let Some((is_prep, s)) = s
226 && !is_prep
227 {
228 if s.errors.iter().any(|((_, ty, name), _)| *ty == id.0 && *name == id.1) {
229 progress = const_var(false);
230 } else if s.task == id {
231 progress = s.task_progress.map(|p| p.is_complete());
232 }
233 }
234 progress.clone()
235 })
236 }
237}
238
239pub fn default_task_info_fn(args: StatusTaskInfoFnArgs) -> UiNode {
240 let errors = args.errors().map(|e| {
241 let mut r = String::new();
242 let mut sep = "";
243 for e in e {
244 write!(&mut r, "{sep}{e}").unwrap();
245 sep = "\n";
246 }
247 Txt::from(r)
248 });
249 Container! {
250 child_start = Wgt! {
251 background = ICONS.req("circle");
252 y = 5;
253 size = 20;
254
255 #[easing(300.ms())]
256 opacity = 0.1.fct();
257
258 when #{args.is_success()} {
259 opacity = 1.fct();
260 background = ICONS.req("check-circle");
261 }
262 when !#{errors.clone()}.is_empty() {
263 opacity = 1.fct();
264 background = ICONS.req("error");
265 ico_color = light_dark(colors::RED.darken(50.pct()), colors::RED.lighten(50.pct()));
266 }
267 };
268 child_bottom = ProgressView! {
269 progress = args.progress();
270 min_height = 24;
272
273 when !#{errors.clone()}.is_empty() {
274 child_out_bottom = SelectableText! {
276 txt = errors;
277 font_color = light_dark(colors::RED.darken(70.pct()), colors::RED.lighten(70.pct()));
278 };
279 }
280 };
281 child = Text! {
282 txt = args.info.info.0;
283 font_size = 1.1.em();
284 };
285 child_spacing = 5;
286 }
287}