chore: changed the UX for optional mods modal (#878)
* chore: changed the UX for optional mods modal * refactor: fold cancel_optional_mods into close_optional_mods * fix: forget optional mod picks once the modal closes * chore: rustfmt the optional mods import * chore: say what the optional mods buttons actually do --------- Co-authored-by: LynithDev <61880709+LynithDev@users.noreply.github.com>
This commit is contained in:
parent
8e66212596
commit
35dcd584d0
3 changed files with 57 additions and 44 deletions
|
|
@ -12,7 +12,9 @@ use crate::hooks::{
|
|||
package_meta_batch, use_cached_image, use_dispatch, use_notifications_snapshot,
|
||||
use_package_meta_batch,
|
||||
};
|
||||
use crate::notifications::{ClusterUpdateItem, OptionalModRef, OptionalModsGroup};
|
||||
use crate::notifications::{
|
||||
ClusterUpdateItem, OptionalModRef, OptionalModsGroup, OptionalModsOutcome,
|
||||
};
|
||||
use crate::theme::colors;
|
||||
use crate::ui::border_all_color;
|
||||
|
||||
|
|
@ -21,6 +23,8 @@ const DIALOG_W: f32 = 720.;
|
|||
const DIALOG_PAD: f32 = 22.;
|
||||
const LIST_MAX_H: f32 = 306.;
|
||||
const ICON_SIZE: f32 = 40.;
|
||||
const SKIP_NOTE: &str =
|
||||
"Anything you don't pick won't be offered again, but you can add it later from the Mods tab.";
|
||||
|
||||
type MetaMap = HashMap<(ProviderId, String), CachedPackageMeta>;
|
||||
|
||||
|
|
@ -33,7 +37,7 @@ impl Component for OptionalModsPopup {
|
|||
fn render(&self) -> impl IntoElement {
|
||||
let snapshot = use_notifications_snapshot();
|
||||
let dispatch = use_dispatch();
|
||||
let picked = use_state(Picked::new);
|
||||
let mut picked = use_state(Picked::new);
|
||||
|
||||
let groups = snapshot.optional_mods.clone();
|
||||
|
||||
|
|
@ -57,11 +61,12 @@ impl Component for OptionalModsPopup {
|
|||
};
|
||||
|
||||
let close = dispatch.clone();
|
||||
let dismissed: Vec<(i64, OptionalModRef)> =
|
||||
groups.iter().flat_map(|group| group.offers()).collect();
|
||||
|
||||
OverlayPopup::new()
|
||||
.on_close(move |_| close.decline_optional_mods(dismissed.clone()))
|
||||
.on_close(move |_| {
|
||||
picked.set(Picked::new());
|
||||
close.close_optional_mods(OptionalModsOutcome::Cancel);
|
||||
})
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::window_percent(100.))
|
||||
|
|
@ -150,7 +155,7 @@ fn dialog(
|
|||
fn content(
|
||||
groups: &[OptionalModsGroup],
|
||||
meta: &MetaMap,
|
||||
picked: State<Picked>,
|
||||
mut picked: State<Picked>,
|
||||
dispatch: crate::Actions,
|
||||
) -> impl IntoElement {
|
||||
let total: usize = groups.iter().map(|group| group.mods.len()).sum();
|
||||
|
|
@ -168,24 +173,23 @@ fn content(
|
|||
let plural = if total == 1 { "" } else { "s" };
|
||||
let subtitle = match groups {
|
||||
[only] => format!(
|
||||
"{} includes {total} optional mod{plural} that {} off by default. Pick the {} to turn on.",
|
||||
"{} includes {total} optional mod{plural} that {} off by default. Pick the {} to turn on. {SKIP_NOTE}",
|
||||
only.cluster_name,
|
||||
if total == 1 { "is" } else { "are" },
|
||||
if total == 1 { "one" } else { "ones" }
|
||||
),
|
||||
_ => format!(
|
||||
"Your bundles include {total} optional mod{plural} across {} clusters that are off by default. Pick the ones to turn on.",
|
||||
"Your bundles include {total} optional mod{plural} across {} clusters that are off by default. Pick the ones to turn on. {SKIP_NOTE}",
|
||||
groups.len()
|
||||
),
|
||||
};
|
||||
|
||||
let cancel = dispatch.clone();
|
||||
let declined = offers.clone();
|
||||
|
||||
let enable_text = if enable.is_empty() {
|
||||
"Enable".to_string()
|
||||
let primary_text = if enable.is_empty() {
|
||||
"Launch".to_string()
|
||||
} else {
|
||||
format!("Enable {}", enable.len())
|
||||
format!("Install {} & launch", enable.len())
|
||||
};
|
||||
|
||||
rect()
|
||||
|
|
@ -210,7 +214,7 @@ fn content(
|
|||
label()
|
||||
.text(subtitle)
|
||||
.font_size(12.5)
|
||||
.max_lines(3)
|
||||
.max_lines(4)
|
||||
.color(colors::fg_secondary()),
|
||||
),
|
||||
)
|
||||
|
|
@ -225,22 +229,25 @@ fn content(
|
|||
.child(
|
||||
Button::new()
|
||||
.ghost()
|
||||
.on_press(move |_| cancel.decline_optional_mods(declined.clone()))
|
||||
.on_press(move |_| {
|
||||
picked.set(Picked::new());
|
||||
cancel.close_optional_mods(OptionalModsOutcome::Cancel);
|
||||
})
|
||||
.text("Cancel"),
|
||||
)
|
||||
.maybe_child((!offers.is_empty()).then(|| {
|
||||
.child({
|
||||
let apply = dispatch.clone();
|
||||
let install = !enable.is_empty();
|
||||
Button::new()
|
||||
.primary()
|
||||
.disabled(enable.is_empty())
|
||||
.on_press(move |_| {
|
||||
picked.set(Picked::new());
|
||||
apply.record_skipped_optional_mods(skip.clone());
|
||||
apply.enable_optional_mods(enable.clone());
|
||||
})
|
||||
.child(Icon::new(IconType::Check).size(15.))
|
||||
.text(enable_text.clone())
|
||||
.into_element()
|
||||
})),
|
||||
.maybe_child(install.then(|| Icon::new(IconType::Check).size(15.)))
|
||||
.text(primary_text)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ use tokio::sync::mpsc;
|
|||
use crate::components::IconType;
|
||||
use crate::notifications::{
|
||||
ClusterUpdateSummary, NotificationAction, NotificationSpec, OptionalModRef, OptionalModsGroup,
|
||||
PackageUpdateGroup, PendingPrompt,
|
||||
OptionalModsOutcome, PackageUpdateGroup, PendingPrompt,
|
||||
};
|
||||
use crate::state::{AppChannel, AppState, AsyncStatus, RelocationState};
|
||||
use crate::{invalidate_java_queries, launcher};
|
||||
|
|
@ -727,18 +727,13 @@ impl Actions {
|
|||
pub fn open_optional_mods(
|
||||
&self,
|
||||
groups: Vec<OptionalModsGroup>,
|
||||
done: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
done: Option<tokio::sync::oneshot::Sender<OptionalModsOutcome>>,
|
||||
) {
|
||||
self.with_engine(move |state| state.notifications.open_optional_mods(groups, done));
|
||||
}
|
||||
|
||||
pub fn close_optional_mods(&self) {
|
||||
self.with_engine(|state| state.notifications.finish_optional_mods());
|
||||
}
|
||||
|
||||
pub fn decline_optional_mods(&self, mods: Vec<(ClusterId, OptionalModRef)>) {
|
||||
self.close_optional_mods();
|
||||
self.record_skipped_optional_mods(mods);
|
||||
pub fn close_optional_mods(&self, outcome: OptionalModsOutcome) {
|
||||
self.with_engine(move |state| state.notifications.finish_optional_mods(outcome));
|
||||
}
|
||||
|
||||
pub fn record_skipped_optional_mods(&self, mods: Vec<(ClusterId, OptionalModRef)>) {
|
||||
|
|
@ -768,7 +763,7 @@ impl Actions {
|
|||
/// `mods/` once at startup, so finishing after the process starts is useless
|
||||
pub fn enable_optional_mods(&self, mods: Vec<(ClusterId, OptionalModRef)>) {
|
||||
if mods.is_empty() {
|
||||
self.close_optional_mods();
|
||||
self.close_optional_mods(OptionalModsOutcome::Launch);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -780,7 +775,7 @@ impl Actions {
|
|||
|
||||
spawn_forever(async move {
|
||||
let Ok(state) = launcher::state() else {
|
||||
actions.close_optional_mods();
|
||||
actions.close_optional_mods(OptionalModsOutcome::Launch);
|
||||
return;
|
||||
};
|
||||
let content = state.services.content();
|
||||
|
|
@ -886,7 +881,7 @@ impl Actions {
|
|||
.send();
|
||||
}
|
||||
|
||||
actions.close_optional_mods();
|
||||
actions.close_optional_mods(OptionalModsOutcome::Launch);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1641,7 +1636,7 @@ impl Actions {
|
|||
&self,
|
||||
state: &Arc<oneclient_core::LauncherState>,
|
||||
cluster_id: ClusterId,
|
||||
) {
|
||||
) -> OptionalModsOutcome {
|
||||
let pending = match oneclient_core::pending_optional_mods(
|
||||
cluster_id,
|
||||
state.bundles.as_ref(),
|
||||
|
|
@ -1656,7 +1651,7 @@ impl Actions {
|
|||
error = %err,
|
||||
"could not read queued optional mods, launching anyway"
|
||||
);
|
||||
return;
|
||||
return OptionalModsOutcome::Launch;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1665,7 +1660,7 @@ impl Actions {
|
|||
let Some(group) =
|
||||
crate::install::pending_optional_group(cluster_id, &pending, &state.services).await
|
||||
else {
|
||||
return;
|
||||
return OptionalModsOutcome::Launch;
|
||||
};
|
||||
|
||||
let (done, wait) = tokio::sync::oneshot::channel();
|
||||
|
|
@ -1676,7 +1671,7 @@ impl Actions {
|
|||
state.center_open = false;
|
||||
});
|
||||
|
||||
let _ = wait.await;
|
||||
wait.await.unwrap_or(OptionalModsOutcome::Launch)
|
||||
}
|
||||
|
||||
async fn prompt_package_updates(
|
||||
|
|
@ -1833,9 +1828,14 @@ async fn launch(actions: &Actions, cluster_id: ClusterId) {
|
|||
.await;
|
||||
|
||||
// After the updates so the player never faces two modals at once
|
||||
actions
|
||||
if actions
|
||||
.resolve_optional_mods_before_launch(&state, cluster_id)
|
||||
.await;
|
||||
.await
|
||||
== OptionalModsOutcome::Cancel
|
||||
{
|
||||
events.game_stage(cluster_id, oneclient_events::LaunchStage::Exited);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = oneclient_core::launch_cluster(&state, cluster_id, &account, true).await {
|
||||
// A missing file is the one failure the launcher can fix itself and a
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ pub struct PackageUpdateGroup {
|
|||
|
||||
pub type OptionalModRef = (String, String);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum OptionalModsOutcome {
|
||||
Launch,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ClusterUpdateItem {
|
||||
pub provider: ProviderId,
|
||||
|
|
@ -233,7 +239,7 @@ pub struct NotificationState {
|
|||
/// Held here not by the launch task
|
||||
/// because every way the modal can end goes through this state
|
||||
package_updates_done: Option<oneshot::Sender<Vec<BrowserPackageUpdate>>>,
|
||||
optional_mods_done: Option<oneshot::Sender<()>>,
|
||||
optional_mods_done: Option<oneshot::Sender<OptionalModsOutcome>>,
|
||||
}
|
||||
|
||||
const CATEGORY_ORDER: [TaskCategory; 7] = [
|
||||
|
|
@ -365,7 +371,7 @@ impl NotificationState {
|
|||
pub fn open_optional_mods(
|
||||
&mut self,
|
||||
groups: Vec<OptionalModsGroup>,
|
||||
done: Option<oneshot::Sender<()>>,
|
||||
done: Option<oneshot::Sender<OptionalModsOutcome>>,
|
||||
) {
|
||||
let groups: Vec<OptionalModsGroup> = groups
|
||||
.into_iter()
|
||||
|
|
@ -374,12 +380,12 @@ impl NotificationState {
|
|||
|
||||
if groups.is_empty() {
|
||||
if let Some(done) = done {
|
||||
let _ = done.send(());
|
||||
let _ = done.send(OptionalModsOutcome::Launch);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
self.finish_optional_mods();
|
||||
self.finish_optional_mods(OptionalModsOutcome::Launch);
|
||||
self.optional_mods = Some(groups);
|
||||
self.optional_mods_done = done;
|
||||
}
|
||||
|
|
@ -388,10 +394,10 @@ impl NotificationState {
|
|||
self.optional_mods = None;
|
||||
}
|
||||
|
||||
pub fn finish_optional_mods(&mut self) {
|
||||
pub fn finish_optional_mods(&mut self, outcome: OptionalModsOutcome) {
|
||||
self.optional_mods = None;
|
||||
if let Some(done) = self.optional_mods_done.take() {
|
||||
let _ = done.send(());
|
||||
let _ = done.send(outcome);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue