diff --git a/Cargo.lock b/Cargo.lock
index 50ff0fce..d6c4b51d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4706,6 +4706,7 @@ dependencies = [
"async-trait",
"async_zip",
"futures-lite",
+ "futures-util",
"oneclient_common",
"oneclient_events",
"oneclient_net",
@@ -5073,6 +5074,7 @@ dependencies = [
"tokio",
"tokio-util",
"tracing",
+ "windows-sys 0.61.2",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 129d8d21..ab35b5b3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -129,6 +129,7 @@ trash = {version = "=5.2.6"}
percent-encoding = {version = "=2.3.2"}
url = {version = "=2.5.8", features = ["serde"]}
uuid = {version = "=1.23.1", features = ["serde", "v4"]}
+windows-sys = {version = "=0.61.2", features = ["Win32_Foundation", "Win32_Storage_FileSystem"]}
# util (codegen/macro)
anyhow = {version = "=1.0.102"}
diff --git a/packages/oneclient_app/src/components/generic_prompt.rs b/packages/oneclient_app/src/components/generic_prompt.rs
index e1468bf0..96b22e2b 100644
--- a/packages/oneclient_app/src/components/generic_prompt.rs
+++ b/packages/oneclient_app/src/components/generic_prompt.rs
@@ -7,6 +7,7 @@ use oneclient_java::JAVA_CHOICE_DOWNLOAD;
use crate::components::{Button, OverlayPopup};
use crate::hooks::{use_dispatch, use_notifications_snapshot};
+use crate::microsoft_java::MICROSOFT_JAVA_CHOICE_INSTALL;
use crate::notifications::PendingPromptView;
use crate::theme::colors;
use crate::ui::border_all_color;
@@ -15,7 +16,9 @@ use crate::updater::UPDATE_CHOICE_INSTALL;
const CARD_BG: Color = Color::from_rgb(26, 34, 41);
fn claimed_elsewhere(prompt: &PendingPromptView) -> bool {
- prompt.has_choice(JAVA_CHOICE_DOWNLOAD) || prompt.has_choice(UPDATE_CHOICE_INSTALL)
+ prompt.has_choice(JAVA_CHOICE_DOWNLOAD)
+ || prompt.has_choice(UPDATE_CHOICE_INSTALL)
+ || prompt.has_choice(MICROSOFT_JAVA_CHOICE_INSTALL)
}
#[derive(PartialEq)]
@@ -149,15 +152,17 @@ mod tests {
}
#[test]
- fn the_two_rich_overlays_keep_their_own_prompts() {
- assert!(claimed_elsewhere(&prompt(vec![Choice::primary(
+ fn the_rich_overlays_keep_their_own_prompts() {
+ for id in [
JAVA_CHOICE_DOWNLOAD,
- "Download",
- )])));
- assert!(claimed_elsewhere(&prompt(vec![Choice::primary(
UPDATE_CHOICE_INSTALL,
- "Download",
- )])));
+ MICROSOFT_JAVA_CHOICE_INSTALL,
+ ] {
+ assert!(
+ claimed_elsewhere(&prompt(vec![Choice::primary(id, "Download")])),
+ "{id} has its own overlay"
+ );
+ }
}
#[test]
diff --git a/packages/oneclient_app/src/components/java_install_manager.rs b/packages/oneclient_app/src/components/java_install_manager.rs
index 16e5987e..0f28d586 100644
--- a/packages/oneclient_app/src/components/java_install_manager.rs
+++ b/packages/oneclient_app/src/components/java_install_manager.rs
@@ -8,6 +8,7 @@ use crate::ui::border_all_color;
fn providers() -> Vec<(JavaVendor, &'static str)> {
vec![
+ (JavaVendor::Microsoft, "Microsoft Build of OpenJDK"),
(JavaVendor::Zulu, "Azul Zulu"),
(JavaVendor::Adoptium, "Eclipse Temurin"),
(JavaVendor::Corretto, "Amazon Corretto"),
diff --git a/packages/oneclient_app/src/components/microsoft_java_prompt.rs b/packages/oneclient_app/src/components/microsoft_java_prompt.rs
new file mode 100644
index 00000000..10e6513d
--- /dev/null
+++ b/packages/oneclient_app/src/components/microsoft_java_prompt.rs
@@ -0,0 +1,189 @@
+use std::time::Duration;
+
+use freya::prelude::*;
+use oneclient_events::Answer;
+
+use crate::components::{Button, Icon, IconType, OverlayPopup};
+use crate::hooks::{use_dispatch, use_notifications_snapshot};
+use crate::microsoft_java::{MICROSOFT_JAVA_CHOICE_INSTALL, MICROSOFT_JAVA_CHOICE_NEVER};
+use crate::theme::colors;
+use crate::ui::border_all_color;
+
+const CARD_BG: Color = Color::from_rgb(26, 34, 41);
+
+const HOLD_SECONDS: u8 = 3;
+
+#[derive(PartialEq)]
+pub struct MicrosoftJavaPromptOverlay;
+
+impl Component for MicrosoftJavaPromptOverlay {
+ fn render(&self) -> impl IntoElement {
+ let snapshot = use_notifications_snapshot();
+ let dispatch = use_dispatch();
+ let remaining = use_state(|| HOLD_SECONDS);
+
+ let showing = snapshot
+ .pending_prompt
+ .as_ref()
+ .is_some_and(|prompt| prompt.has_choice(MICROSOFT_JAVA_CHOICE_INSTALL));
+
+ use_side_effect_with_deps(&showing, move |&showing| {
+ let mut remaining = remaining;
+
+ if !showing {
+ remaining.set(HOLD_SECONDS);
+ return;
+ }
+
+ spawn(async move {
+ let mut remaining = remaining;
+ for step in (0..HOLD_SECONDS).rev() {
+ tokio::time::sleep(Duration::from_secs(1)).await;
+
+ // Re-armed while this was sleeping so a newer prompt owns the
+ // hold now and this countdown is stale
+ if { *remaining.peek() } != step + 1 {
+ return;
+ }
+
+ remaining.set(step);
+ }
+ });
+ });
+
+ let Some(prompt) = snapshot.pending_prompt.clone() else {
+ return rect().into_element();
+ };
+
+ if !prompt.has_choice(MICROSOFT_JAVA_CHOICE_INSTALL) {
+ return rect().into_element();
+ }
+
+ let held = *remaining.read();
+ let locked = held > 0;
+
+ let dismiss_label = prompt
+ .dismiss
+ .clone()
+ .unwrap_or_else(|| "Cancel".to_string());
+ let never_label = prompt
+ .choice(MICROSOFT_JAVA_CHOICE_NEVER)
+ .map(|choice| choice.label.clone())
+ .unwrap_or_else(|| "Don't ask again".to_string());
+ let install_label = prompt
+ .choice(MICROSOFT_JAVA_CHOICE_INSTALL)
+ .map(|choice| choice.label.clone())
+ .unwrap_or_else(|| "Proceed".to_string());
+
+ let close = dispatch.clone();
+ let cancel = dispatch.clone();
+ let never = dispatch.clone();
+ let accept = dispatch.clone();
+
+ OverlayPopup::new()
+ // Closing from the backdrop is a dismissal too so it waits as well
+ .on_close(move |_| {
+ if !locked {
+ close.dismiss_prompt();
+ }
+ })
+ .child(
+ rect()
+ .width(Size::window_percent(100.))
+ .height(Size::window_percent(100.))
+ .center()
+ .child(
+ rect()
+ .vertical()
+ .width(Size::px(440.))
+ .max_width(Size::window_percent(90.))
+ .spacing(14.)
+ .padding(Gaps::new_all(20.))
+ .corner_radius(CornerRadius::new_all(14.))
+ .background(CARD_BG)
+ .border(border_all_color(1., colors::component_border()))
+ .child(
+ rect()
+ .horizontal()
+ .cross_align(Alignment::Center)
+ .spacing(10.)
+ .child(
+ Icon::new(IconType::DownloadCloud02)
+ .size(20.)
+ .color(colors::brand()),
+ )
+ .child(
+ label()
+ .text(prompt.title.clone())
+ .font_size(16.)
+ .font_weight(FontWeight::SEMI_BOLD)
+ .color(colors::fg_primary()),
+ ),
+ )
+ .child(
+ label()
+ .text(prompt.question.clone())
+ .font_size(12.)
+ .max_lines(6)
+ .width(Size::fill())
+ .color(colors::fg_secondary()),
+ )
+ .child(
+ rect()
+ .horizontal()
+ .width(Size::fill())
+ .cross_align(Alignment::Center)
+ .main_align(Alignment::SpaceBetween)
+ .child(
+ label()
+ .text(if locked {
+ held.to_string()
+ } else {
+ String::new()
+ })
+ .font_size(12.)
+ .color(colors::fg_secondary()),
+ )
+ .child(
+ rect()
+ .horizontal()
+ .spacing(8.)
+ .child(
+ Button::new()
+ .secondary()
+ .disabled(locked)
+ .on_press(move |_| cancel.dismiss_prompt())
+ .text(dismiss_label),
+ )
+ .child(
+ Button::new()
+ .secondary()
+ .disabled(locked)
+ .on_press(move |_| {
+ never.answer_prompt(Answer::new(
+ MICROSOFT_JAVA_CHOICE_NEVER,
+ ))
+ })
+ .text(never_label),
+ )
+ .child(
+ Button::new()
+ .primary()
+ .on_press(move |_| {
+ accept.answer_prompt(Answer::new(
+ MICROSOFT_JAVA_CHOICE_INSTALL,
+ ))
+ })
+ .child(
+ Icon::new(IconType::DownloadCloud02)
+ .size(14.),
+ )
+ .text(install_label),
+ ),
+ ),
+ ),
+ ),
+ )
+ .into_element()
+ }
+}
diff --git a/packages/oneclient_app/src/components/mod.rs b/packages/oneclient_app/src/components/mod.rs
index 75dd453b..e3bf43ad 100644
--- a/packages/oneclient_app/src/components/mod.rs
+++ b/packages/oneclient_app/src/components/mod.rs
@@ -16,6 +16,7 @@ mod icons;
mod java_install_manager;
mod generic_prompt;
mod java_prompt;
+mod microsoft_java_prompt;
mod link_confirm;
mod local_image;
mod log_viewer;
@@ -62,6 +63,7 @@ pub use icons::{Icon, IconTint, IconType};
pub use java_install_manager::JavaInstallManager;
pub use generic_prompt::GenericPromptOverlay;
pub use java_prompt::JavaPromptOverlay;
+pub use microsoft_java_prompt::MicrosoftJavaPromptOverlay;
pub use link_confirm::ConfirmLinkOverlay;
pub use local_image::LocalImage;
pub use markdown::{Markdown, MarkdownStyle};
diff --git a/packages/oneclient_app/src/components/overlay_popup.rs b/packages/oneclient_app/src/components/overlay_popup.rs
index 4f22759e..f4f5d5ba 100644
--- a/packages/oneclient_app/src/components/overlay_popup.rs
+++ b/packages/oneclient_app/src/components/overlay_popup.rs
@@ -8,6 +8,7 @@ use freya::{
use crate::hooks::use_overlay_claim;
const SCRIM_ALPHA: f32 = 90.;
+const SCRIM_MIN_ALPHA: u8 = 1;
/// Overlay level of a top level popup. Its scrim sits two levels below it.
pub const OVERLAY_BASE_LEVEL: u8 = 12;
@@ -116,7 +117,7 @@ impl Component for OverlayPopup {
});
let scrim_alpha = if self.backdrop { SCRIM_ALPHA } else { 0. };
- let alpha = (fade.read().value() * scrim_alpha) as u8;
+ let alpha = ((fade.read().value() * scrim_alpha) as u8).max(SCRIM_MIN_ALPHA);
rect()
.layer(Layer::Overlay)
@@ -130,7 +131,7 @@ impl Component for OverlayPopup {
.height(Size::window_percent(100.))
.layer(Layer::OverlayLevel(self.overlay_level.saturating_sub(2)))
.background(Color::from_argb(alpha, 0, 0, 0))
- .on_press(move |_| {
+ .on_all_press(move |_| {
if let Some(on_close) = scrim_close.as_ref() {
on_close.call(());
}
diff --git a/packages/oneclient_app/src/events.rs b/packages/oneclient_app/src/events.rs
index 43dbddde..56bd0328 100644
--- a/packages/oneclient_app/src/events.rs
+++ b/packages/oneclient_app/src/events.rs
@@ -11,7 +11,7 @@ use tokio::sync::mpsc;
use crate::hooks::PumpSignal;
use crate::notifications::{MESSAGE_TOAST_TTL, PendingPromptView};
-use crate::state::{AppChannel, AppState, LoginProgress};
+use crate::state::{AppChannel, AppState, LoginProgress, StorageScanProgress};
/// Quiet period before log lines are written without it every line wakes the log view
const GAME_LOG_FLUSH: Duration = Duration::from_millis(120);
@@ -127,6 +127,7 @@ impl EventPump {
let mut logs: Vec<(i64, String)> = Vec::new();
let mut failed: Option<(i64, String)> = None;
let mut login: Option