feat: added a don't show again option to the update prompt (#723)

This commit is contained in:
Maciek 2026-09-04 17:49:47 +02:00 • committed by LynithDev
parent 4aaf974f04
commit 2c43aa16ba
3 changed files with 209 additions and 33 deletions

View file

@ -0,0 +1,133 @@
use freya::{
animation::{AnimColor, AnimNum, AnimatedValue, Ease, Function, use_animation_transition},
prelude::*,
};
use crate::{
components::{Icon, IconType},
theme::colors,
ui,
};
const BOX_SIZE: f32 = 18.;
const MARK_SIZE: f32 = 12.;
const TIME: u64 = 180;
pub fn checkbox_labeled(value: State<bool>, text: impl Into<Box<str>>) -> impl IntoElement {
let mut v = value;
Check {
value,
label: Some(text.into()),
on_press: (move |()| v.toggle()).into(),
disabled: false,
}
}
#[derive(PartialEq)]
struct Check {
value: State<bool>,
label: Option<Box<str>>,
on_press: EventHandler<()>,
disabled: bool,
}
impl Component for Check {
fn render(&self) -> impl IntoElement {
let value = self.value;
let on_press = self.on_press.clone();
let checked = *value.read();
let disabled = self.disabled;
let a11y_id = use_a11y();
let focus = use_focus(a11y_id);
let box_border = if focus().is_focused() && !disabled {
colors::fg_primary()
} else if checked && !disabled {
colors::brand()
} else {
colors::component_border()
};
let background = use_animation_transition(value, |_, checked| {
if checked {
AnimColor::new(colors::component_bg(), colors::brand())
} else {
AnimColor::new(colors::brand(), colors::component_bg())
}
.time(TIME)
.function(Function::Expo)
.ease(Ease::Out)
});
let mark = use_animation_transition(value, |_, checked| {
let scale = AnimNum::new(0.5, 1.)
.time(TIME)
.function(Function::Expo)
.ease(Ease::Out);
let opacity = AnimNum::new(0., 1.)
.time(TIME)
.function(Function::Expo)
.ease(Ease::Out);
if checked {
(scale, opacity)
} else {
(scale.into_reversed(), opacity.into_reversed())
}
});
let (mark_scale, mark_opacity) = {
let mark = mark.read();
(mark.0.value(), mark.1.value())
};
rect()
.horizontal()
.cross_align(Alignment::Center)
.spacing(8.)
.a11y_id(a11y_id)
.a11y_focusable(!disabled)
.a11y_role(AccessibilityRole::CheckBox)
.maybe(!disabled, |el| {
el.cursor(CursorIcon::Pointer).on_press(move |_| {
on_press.call(());
})
})
.child(
rect()
.width(Size::px(BOX_SIZE))
.height(Size::px(BOX_SIZE))
.corner_radius(CornerRadius::new_all(5.))
.main_align(Alignment::Center)
.cross_align(Alignment::Center)
.background(if disabled {
colors::component_bg_disabled()
} else {
background.read().value()
})
.border(ui::border_all_color(1., box_border))
.maybe_child((checked || mark_opacity > 0.).then(|| {
rect().opacity(mark_opacity).scale(mark_scale).child(
Icon::new(IconType::Check)
.size(MARK_SIZE)
.color(if disabled {
colors::fg_secondary()
} else {
Color::WHITE
}),
)
})),
)
.maybe_child(self.label.as_ref().map(|text| {
label()
.text(text.to_string())
.font_size(12.)
.color(if disabled {
colors::fg_secondary()
} else {
colors::fg_primary()
})
}))
}
}

View file

@ -3,6 +3,7 @@ mod active_cluster_panel;
mod avatar;
mod button;
mod charts;
mod checkbox;
mod cluster_landscape_art;
mod cluster_update_popup;
mod context_menu;
@ -46,6 +47,7 @@ pub use active_cluster_panel::ActiveClusterPanel;
pub use avatar::Avatar;
pub use button::{Button, ButtonSize, ButtonVariant, link_button, open_folder_button};
pub use charts::{BarChart, PieChart, ValueUnit, slice_color};
pub use checkbox::checkbox_labeled;
pub use cluster_landscape_art::ClusterLandscapeArt;
pub use cluster_update_popup::ClusterUpdatePopup;
pub use context_menu::ContextMenu;

View file

@ -3,11 +3,12 @@
use std::collections::HashMap;
use freya::prelude::*;
use oneclient_common::{PackageUpdateMode, Patch};
use oneclient_content::packages::{CachedPackageMeta, ProviderId};
use oneclient_core::BrowserPackageUpdate;
use oneclient_core::{BrowserPackageUpdate, ProfileUpdate};
use oneclient_db::models::ClusterId;
use crate::components::{Button, Dropdown, Icon, IconType, OverlayPopup, ScrollArea};
use crate::components::{Button, Dropdown, Icon, IconType, OverlayPopup, ScrollArea, checkbox_labeled};
use crate::hooks::{
package_meta_batch, use_dispatch, use_notifications_snapshot, use_package_meta_batch,
};
@ -63,6 +64,7 @@ impl Component for PackageUpdatePopup {
// Absent entries default to Update so groups arriving mid-modal need no reconciliation
let choices = use_state(HashMap::<RowKey, RowChoice>::new);
let mut dont_show_again = use_state(|| false);
let groups = snapshot.package_updates.clone();
@ -83,6 +85,8 @@ impl Component for PackageUpdatePopup {
}
let Some(groups) = groups.filter(|groups| !groups.is_empty()) else {
// The popup stays mounted, so the tick must not survive into the next prompt
dont_show_again.set_if_modified(false);
return rect().into_element();
};
@ -95,7 +99,7 @@ impl Component for PackageUpdatePopup {
.width(Size::window_percent(100.))
.height(Size::window_percent(100.))
.center()
.child(dialog(&groups, &meta, dispatch, choices)),
.child(dialog(&groups, &meta, dispatch, choices, dont_show_again)),
)
.into_element()
}
@ -113,6 +117,7 @@ fn dialog(
meta: &MetaMap,
dispatch: crate::Actions,
choices: State<HashMap<RowKey, RowChoice>>,
dont_show_again: State<bool>,
) -> impl IntoElement {
rect()
.vertical()
@ -130,7 +135,7 @@ fn dialog(
0.,
Color::from_argb(150, 0, 0, 0),
)))
.child(content(groups, meta, dispatch, choices))
.child(content(groups, meta, dispatch, choices, dont_show_again))
}
fn content(
@ -138,10 +143,13 @@ fn content(
meta: &MetaMap,
dispatch: crate::Actions,
choices: State<HashMap<RowKey, RowChoice>>,
dont_show_again: State<bool>,
) -> impl IntoElement {
let dismiss = dispatch.clone();
let proceed_dispatch = dispatch.clone();
let dsa_dispatch = dispatch.clone();
let total: usize = groups.iter().map(|group| group.packages.len()).sum();
let clusters: Vec<ClusterId> = groups.iter().map(|group| group.cluster_id).collect();
let subtitle = match groups {
[only] => format!(
@ -194,40 +202,73 @@ fn content(
.horizontal()
.width(Size::fill())
.cross_align(Alignment::Center)
.main_align(Alignment::End)
.main_align(Alignment::SpaceBetween)
.spacing(8.)
.child(
Button::new()
.ghost()
.on_press(move |_| dismiss.close_package_updates())
.text("Cancel"),
rect()
.direction(Direction::Horizontal)
.main_align(Alignment::Start)
.cross_align(Alignment::Center)
.spacing(6.)
.child(
checkbox_labeled(dont_show_again, "Don't show again?")
)
)
.child(
Button::new()
.primary()
.on_press(move |_| {
let answers = choices.read();
for update in &all {
match answers
.get(&row_key(update))
.copied()
.unwrap_or_default()
{
RowChoice::Update => {
proceed_dispatch.apply_package_update(update.clone());
rect()
.width(Size::fill())
.direction(Direction::Horizontal)
.cross_align(Alignment::Center)
.main_align(Alignment::End)
.child(
Button::new()
.ghost()
.on_press(move |_| dismiss.close_package_updates())
.text("Cancel"),
)
.child(
Button::new()
.primary()
.on_press(move |_| {
if *dont_show_again.peek() {
// A cluster override outranks the global one, so the prompt
// is only silenced by writing the cluster that raised it
for cluster_id in &clusters {
dsa_dispatch.update_cluster_profile(
*cluster_id,
ProfileUpdate {
browser_update_mode: Patch::Set(
PackageUpdateMode::Skip,
),
..Default::default()
},
);
}
}
RowChoice::Skip => {}
RowChoice::SkipVersion => proceed_dispatch
.skip_package_update(
update.cluster_id,
update.hash.clone(),
),
}
}
proceed_dispatch.close_package_updates();
})
.child(Icon::new(IconType::DownloadCloud02).size(15.))
.text("Proceed"),
let answers = choices.read();
for update in &all {
match answers
.get(&row_key(update))
.copied()
.unwrap_or_default()
{
RowChoice::Update => {
proceed_dispatch.apply_package_update(update.clone());
}
RowChoice::Skip => {}
RowChoice::SkipVersion => proceed_dispatch
.skip_package_update(
update.cluster_id,
update.hash.clone(),
),
}
}
proceed_dispatch.close_package_updates();
})
.child(Icon::new(IconType::DownloadCloud02).size(15.))
.text("Proceed"),
)
),
)
}