diff --git a/crates/app-hub-app/README.md b/crates/app-hub-app/README.md index b4eea46..4c8f25a 100644 --- a/crates/app-hub-app/README.md +++ b/crates/app-hub-app/README.md @@ -113,6 +113,13 @@ cargo run --release -p octosense-app-hub-app --example preview The preview logs launch requests instead of launching; only a shell opens apps. +On an app's detail page, the primary button stays in the same position: +**Get → Install → Installing… → Open**. Get reveals the permissions and +privacy disclosures below the button; Install confirms that reviewed +catalog entry. Cancel returns the button to Get (or Update for an available +update). Installation results appear below the app header, so they do not +move the primary action. + This crate's default feature, `text-input-state-query`, turns on the same feature in `octosense-appstore`, and that feature needs OctoSense's runtime patches on `../makepad`. Against plain sibling checkouts, add diff --git a/crates/app-hub-app/examples/preview.rs b/crates/app-hub-app/examples/preview.rs index 98e31a4..ead4d9c 100644 --- a/crates/app-hub-app/examples/preview.rs +++ b/crates/app-hub-app/examples/preview.rs @@ -38,8 +38,14 @@ impl AppMain for App { if let Event::Actions(actions) = event { for action in actions { if let Some(action) = action.as_widget_action() { - if let octosense_app_hub_app::AppHubAction::Launch(id) = action.cast() { - log!("apphub-preview: launch {id}"); + match action.cast() { + octosense_app_hub_app::AppHubAction::Launch(id) => { + log!("apphub-preview: launch {id}"); + } + octosense_app_hub_app::AppHubAction::OpenInstalled(id) => { + log!("apphub-preview: open installed {id}"); + } + octosense_app_hub_app::AppHubAction::None => {} } } } diff --git a/crates/app-hub-app/src/view.rs b/crates/app-hub-app/src/view.rs index 962d273..18e6faf 100644 --- a/crates/app-hub-app/src/view.rs +++ b/crates/app-hub-app/src/view.rs @@ -153,7 +153,6 @@ script_mod! { title := Heading{} permissions := Text{} privacy := Meta{} - confirm := Primary{text: "Install"} cancel := Plain{text: "Cancel" width: Fill} } } @@ -493,16 +492,21 @@ impl AppHubView { script_apply_eval!(cx,tab,{draw_icon.color: #(color) draw_text +: {color: #(color) color_hover: #(color) color_down: #(color) color_focus: #(color)} draw_bg +: {color: #(face) color_hover: #(face) color_focus: #(face) border_radius: 12}}); } self.rows.clear(); - if !self.notice.is_empty() { + if !self.notice.is_empty() && self.selected.is_none() { self.rows.push(Row::Notice(self.notice.clone())); } if let Some(entry) = self.selected.clone() { + // Keep the primary action at the same position when consent, + // installation results, or catalog warnings change below it. + self.rows.push(Row::Detail(entry.clone())); + if !self.notice.is_empty() { + self.rows.push(Row::Notice(self.notice.clone())); + } if entry.kind == CatalogKind::Live { if let Some(warning) = self.snapshot.as_ref().and_then(|s| s.warning.clone()) { self.rows.push(Row::Notice(warning)); } } - self.rows.push(Row::Detail(entry.clone())); if self.confirming { self.rows.push(Row::Consent(entry)); } else { @@ -651,10 +655,18 @@ impl AppHubView { } EntryStatus::Installed => self.send(Command::Open(entry.id)), EntryStatus::Available | EntryStatus::UpdateAvailable => { - if entry.consent.is_some() { + if self.confirming && self.selected.as_ref().is_some_and(|e| e.id == entry.id) { + if let Some(consent) = entry.consent { + self.send(Command::Install(consent)); + self.installing = self.busy; + } + } else if entry.consent.is_some() { + let already_selected = self.selected.as_ref().is_some_and(|e| e.id == entry.id); self.selected = Some(entry); self.confirming = true; - self.reset_scroll(cx); + if !already_selected { + self.reset_scroll(cx); + } } else { self.notice = "Refresh the catalog before installing this app.".into(); } @@ -767,6 +779,12 @@ impl AppHubView { cx, if self.installing && self.selected.as_ref().is_some_and(|e| e.id == entry.id) { "Installing…" + } else if self.confirming + && self.selected.as_ref().is_some_and(|e| e.id == entry.id) + && entry.consent.is_some() + && matches!(entry.status, EntryStatus::Available | EntryStatus::UpdateAvailable) + { + "Install" } else { button_text(entry) }, @@ -860,18 +878,8 @@ impl AppHubView { ); item.label(cx, ids!(privacy)) .set_text(cx, &lines_or(&entry.privacy, "")); - item.button(cx, ids!(confirm)) - .set_enabled(cx, !self.busy && entry.consent.is_some()); item.button(cx, ids!(cancel)) .set_enabled(cx, !self.installing); - item.button(cx, ids!(confirm)).set_text( - cx, - if self.installing { - "Installing…" - } else { - "Install" - }, - ); } Row::Categories => { for (id, category) in [ @@ -985,14 +993,7 @@ impl AppHubView { } } } - Row::Consent(entry) => { - if item.button(cx, ids!(confirm)).clicked(actions) && !self.busy { - if let Some(consent) = entry.consent { - self.installing = true; - self.send(Command::Install(consent)); - self.render(cx); - } - } + Row::Consent(_) => { if item.button(cx, ids!(cancel)).clicked(actions) && !self.installing { self.confirming = false; self.render(cx);