adds tips and parallax background to onboarding
update client id for MSA + fix account already logged in issues
This commit is contained in:
parent
2bbecb6e86
commit
c27688c8d4
11 changed files with 299 additions and 103 deletions
|
|
@ -19,6 +19,7 @@ pub use queries::{
|
|||
ClusterAction, invalidate_cluster_queries, invalidate_profile_queries, use_cluster_mutation,
|
||||
AddOfflineAccountKeys, RefreshAccountKeys, RemoveAccountKeys, SetDefaultAccountKeys,
|
||||
UseRefreshAccount, UseRemoveAccount, UseSetDefaultAccount, accounts_have_microsoft,
|
||||
login_code_already_handled,
|
||||
mutation_error, mutation_is_pending, pick_version_metadata, try_account, try_accounts,
|
||||
try_default_account, try_game_profile, use_account, use_accounts, use_add_microsoft_account,
|
||||
use_add_offline_account, use_begin_microsoft_login, use_bundle_updates,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use freya::query::{
|
||||
Mutation, MutationCapability, MutationStateData, QueriesStorage, Query, QueryCapability,
|
||||
QueryStateData, UseMutation, UseQuery, use_mutation, use_query,
|
||||
|
|
@ -7,6 +9,17 @@ use oneclient_core::LauncherError;
|
|||
use oneclient_core::auth::{self, AccountKind, MinecraftAccount, MinecraftLogin};
|
||||
use uuid::Uuid;
|
||||
|
||||
static HANDLED_LOGIN_CODE: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
||||
pub fn login_code_already_handled(user_code: &str) -> bool {
|
||||
let mut handled = HANDLED_LOGIN_CODE.lock().unwrap();
|
||||
if handled.as_deref() == Some(user_code) {
|
||||
return true;
|
||||
}
|
||||
*handled = Some(user_code.to_string());
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ListAccountsKeys;
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ pub use versions::{use_versions, use_loader_versions, loader_versions};
|
|||
pub use auth::{
|
||||
AddOfflineAccountKeys, RefreshAccountKeys, RemoveAccountKeys, SetDefaultAccountKeys,
|
||||
UseRefreshAccount, UseRemoveAccount, UseSetDefaultAccount, accounts_have_microsoft,
|
||||
login_code_already_handled,
|
||||
mutation_error, mutation_is_pending, try_account, try_accounts, try_default_account,
|
||||
use_account, use_accounts, use_add_microsoft_account, use_add_offline_account,
|
||||
use_begin_microsoft_login, use_current_account, use_default_account,
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@ half4 main(float2 fragCoord) {
|
|||
}
|
||||
"#;
|
||||
|
||||
fn gradient_overlay_radial() -> impl IntoElement {
|
||||
pub(crate) fn gradient_overlay_radial() -> impl IntoElement {
|
||||
let effect = use_hook(|| {
|
||||
freya::engine::prelude::RuntimeEffect::make_for_shader(VIGNETTE_SPOTLIGHT_SHADER, None)
|
||||
.expect("Failed to compile vignette shader")
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ mod settings_shell;
|
|||
|
||||
pub use animated_outlet::AnimatedAppOutlet;
|
||||
pub use app_shell::{AppShell, HOME_BACKGROUND_ASSET};
|
||||
pub(crate) use app_shell::gradient_overlay_radial;
|
||||
pub use cluster_shell::{ClusterShell, cluster_content};
|
||||
pub use onboarding_shell::OnboardingShell;
|
||||
pub use root_layout::RootLayout;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
use std::sync::Mutex;
|
||||
|
||||
use chrono::Utc;
|
||||
use freya::prelude::*;
|
||||
use freya::query::{MutationCapability, MutationStateData, UseMutation};
|
||||
|
|
@ -11,15 +9,14 @@ use crate::components::{
|
|||
};
|
||||
use crate::hooks::{
|
||||
AddOfflineAccountKeys, RefreshAccountKeys, RemoveAccountKeys, SetDefaultAccountKeys,
|
||||
accounts_have_microsoft, try_accounts, try_default_account, use_accounts,
|
||||
use_add_offline_account, use_begin_microsoft_login, use_current_account,
|
||||
accounts_have_microsoft, login_code_already_handled, try_accounts, try_default_account,
|
||||
use_accounts, use_add_offline_account, use_begin_microsoft_login, use_current_account,
|
||||
use_finish_microsoft_login, use_refresh_account, use_remove_account, use_set_default_account,
|
||||
};
|
||||
use crate::platform;
|
||||
use crate::theme::colors;
|
||||
use crate::ui::border_all_color;
|
||||
|
||||
static HANDLED_LOGIN_CODE: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct Accounts;
|
||||
|
|
@ -48,12 +45,8 @@ impl Component for Accounts {
|
|||
_ => None,
|
||||
};
|
||||
let Some(login) = login else { return };
|
||||
{
|
||||
let mut handled = HANDLED_LOGIN_CODE.lock().unwrap();
|
||||
if handled.as_deref() == Some(login.user_code.as_str()) {
|
||||
return;
|
||||
}
|
||||
*handled = Some(login.user_code.clone());
|
||||
if login_code_already_handled(&login.user_code) {
|
||||
return;
|
||||
}
|
||||
handled_code.set(Some(login.user_code.clone()));
|
||||
finish.mutate(login.clone());
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
use std::sync::Mutex;
|
||||
|
||||
use freya::prelude::*;
|
||||
use freya::query::{MutationCapability, MutationStateData, UseMutation};
|
||||
use freya::text_edit::Clipboard;
|
||||
|
|
@ -7,8 +5,8 @@ use oneclient_core::auth::{MinecraftAccount, MinecraftLogin};
|
|||
|
||||
use crate::components::{Avatar, Button, Icon, IconType, OverlayPopup};
|
||||
use crate::hooks::{
|
||||
try_default_account, use_begin_microsoft_login, use_current_account,
|
||||
use_finish_microsoft_login,
|
||||
login_code_already_handled, try_default_account, use_begin_microsoft_login,
|
||||
use_current_account, use_finish_microsoft_login,
|
||||
};
|
||||
use crate::platform;
|
||||
use crate::routes::Route;
|
||||
|
|
@ -18,8 +16,6 @@ use crate::view::onboarding::{
|
|||
onboarding_illustration, onboarding_nav, onboarding_page, step_heading,
|
||||
};
|
||||
|
||||
static HANDLED_LOGIN_CODE: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct OnboardingAccount;
|
||||
|
||||
|
|
@ -38,12 +34,8 @@ impl Component for OnboardingAccount {
|
|||
_ => None,
|
||||
};
|
||||
let Some(login) = login else { return };
|
||||
{
|
||||
let mut handled = HANDLED_LOGIN_CODE.lock().unwrap();
|
||||
if handled.as_deref() == Some(login.user_code.as_str()) {
|
||||
return;
|
||||
}
|
||||
*handled = Some(login.user_code.clone());
|
||||
if login_code_already_handled(&login.user_code) {
|
||||
return;
|
||||
}
|
||||
finish.mutate(login.clone());
|
||||
pending_login.set(Some(login));
|
||||
|
|
|
|||
|
|
@ -7,10 +7,18 @@ use std::time::Duration;
|
|||
use oneclient_core::clusters::Cluster;
|
||||
|
||||
use crate::components::DynamicArt;
|
||||
use crate::hooks::use_settings_snapshot;
|
||||
use crate::layout::gradient_overlay_radial;
|
||||
use crate::theme::colors;
|
||||
|
||||
const BACKDROP_INTERVAL_SECS: u64 = 12;
|
||||
|
||||
const PARALLAX_SCALE: f32 = 1.10;
|
||||
const PARALLAX_STRENGTH: f32 = 0.008;
|
||||
const PARALLAX_STEP: f32 = 0.12;
|
||||
const PARALLAX_SETTLE: f32 = 0.002;
|
||||
const PARALLAX_FRAME: Duration = Duration::from_millis(33);
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct LoadingBackdrop {
|
||||
pub clusters: Vec<Cluster>,
|
||||
|
|
@ -23,7 +31,7 @@ impl Component for LoadingBackdrop {
|
|||
let mut index = use_state(|| 0usize);
|
||||
|
||||
let stop = use_hook(|| Arc::new(AtomicBool::new(false)));
|
||||
|
||||
|
||||
use_drop({
|
||||
let stop = stop.clone();
|
||||
move || stop.store(true, Ordering::Relaxed)
|
||||
|
|
@ -53,6 +61,52 @@ impl Component for LoadingBackdrop {
|
|||
|
||||
let current = *index.read() % count;
|
||||
|
||||
let parallax_enabled = use_settings_snapshot().settings.dynamic_background_enabled;
|
||||
|
||||
let mut size = use_state(|| (0f32, 0f32));
|
||||
let mut target = use_state(|| (0f32, 0f32));
|
||||
let mut pan = use_state(|| (0f32, 0f32));
|
||||
|
||||
let mut anim_task = use_state(|| None::<OwnedTaskHandle>);
|
||||
use_side_effect_with_deps(¶llax_enabled, move |enabled| {
|
||||
if *enabled {
|
||||
let handle = spawn(async move {
|
||||
loop {
|
||||
let (tx, ty) = *target.peek();
|
||||
let (cx, cy) = *pan.peek();
|
||||
|
||||
if (tx - cx).abs() > PARALLAX_SETTLE || (ty - cy).abs() > PARALLAX_SETTLE {
|
||||
pan.set((
|
||||
cx + (tx - cx) * PARALLAX_STEP,
|
||||
cy + (ty - cy) * PARALLAX_STEP,
|
||||
));
|
||||
}
|
||||
|
||||
tokio::time::sleep(PARALLAX_FRAME).await;
|
||||
}
|
||||
});
|
||||
anim_task.set(Some(handle.owned()));
|
||||
} else {
|
||||
anim_task.set(None);
|
||||
target.set((0.0, 0.0));
|
||||
pan.set((0.0, 0.0));
|
||||
}
|
||||
});
|
||||
|
||||
let (cx, cy) = if parallax_enabled {
|
||||
*pan.read()
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
let (sw, sh) = *size.read();
|
||||
let pan_x = cx * sw * PARALLAX_STRENGTH;
|
||||
let pan_y = cy * sh * PARALLAX_STRENGTH;
|
||||
|
||||
let art = match clusters.get(current) {
|
||||
Some(cluster) => DynamicArt::for_cluster(cluster).max_edge(1280),
|
||||
None => DynamicArt::fallback().max_edge(1280),
|
||||
};
|
||||
|
||||
let fade = use_animation_with_dependencies(¤t, |conf, _| {
|
||||
conf.on_creation(OnCreation::Run);
|
||||
conf.on_change(OnChange::Rerun);
|
||||
|
|
@ -62,26 +116,6 @@ impl Component for LoadingBackdrop {
|
|||
.function(Function::Cubic)
|
||||
});
|
||||
|
||||
let art: Element = match clusters.get(current) {
|
||||
Some(cluster) => rect()
|
||||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.opacity(fade.get().value())
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.child(DynamicArt::for_cluster(cluster).max_edge(1280)),
|
||||
)
|
||||
.into_element(),
|
||||
|
||||
None => rect()
|
||||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.background(colors::page())
|
||||
.into_element(),
|
||||
};
|
||||
|
||||
rect()
|
||||
.position(Position::new_absolute().top(0.).left(0.))
|
||||
.width(Size::fill())
|
||||
|
|
@ -89,41 +123,44 @@ impl Component for LoadingBackdrop {
|
|||
.overflow(Overflow::Clip)
|
||||
.interactive(false)
|
||||
.background(colors::page())
|
||||
.on_sized(move |e: Event<SizedEventData>| {
|
||||
let (nw, nh) = (e.area.width(), e.area.height());
|
||||
let (pw, ph) = *size.peek();
|
||||
if (pw - nw).abs() > 0.5 || (ph - nh).abs() > 0.5 {
|
||||
size.set((nw, nh));
|
||||
}
|
||||
})
|
||||
.maybe(parallax_enabled, |el| {
|
||||
el.on_capture_global_pointer_move(move |e: Event<PointerEventData>| {
|
||||
let (sw, sh) = *size.peek();
|
||||
if sw > 0.0 && sh > 0.0 {
|
||||
let loc = e.global_location();
|
||||
target.set((
|
||||
(loc.x as f32 / sw) * 2.0 - 1.0,
|
||||
(loc.y as f32 / sh) * 2.0 - 1.0,
|
||||
));
|
||||
}
|
||||
})
|
||||
})
|
||||
.child(
|
||||
rect()
|
||||
.position(Position::new_absolute().top(0.).left(0.))
|
||||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.position(Position::new_absolute().top(0.).left(0.))
|
||||
.layer(Layer::Relative(0))
|
||||
.scale(PARALLAX_SCALE)
|
||||
.offset_x(pan_x)
|
||||
.offset_y(pan_y)
|
||||
.opacity(fade.get().value())
|
||||
.child(art),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.position(Position::new_absolute().top(0.).left(0.))
|
||||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.position(Position::new_absolute().top(0.).left(0.))
|
||||
.layer(Layer::Relative(8))
|
||||
.background(
|
||||
LinearGradient::new()
|
||||
.angle(270.)
|
||||
.stop((Color::BLACK.with_a(235), 0.))
|
||||
.stop((Color::BLACK.with_a(235), 32.))
|
||||
.stop((Color::BLACK.with_a(120), 48.))
|
||||
.stop((Color::BLACK.with_a(10), 100.)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.position(Position::new_absolute().top(0.).left(0.))
|
||||
.layer(Layer::Relative(8))
|
||||
.background(
|
||||
LinearGradient::new()
|
||||
.angle(0.)
|
||||
.stop((Color::BLACK.with_a(0), 55.))
|
||||
.stop((Color::BLACK.with_a(160), 100.)),
|
||||
),
|
||||
.layer(Layer::Relative(1))
|
||||
.child(gradient_overlay_radial()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,9 +21,11 @@ use crate::theme::colors;
|
|||
mod backdrop;
|
||||
mod progress;
|
||||
mod summary;
|
||||
mod tips;
|
||||
pub use backdrop::LoadingBackdrop;
|
||||
use progress::{ProgressView, failure_panel, progress_panel};
|
||||
use summary::summary_view;
|
||||
use tips::{tip_panel, use_onboarding_tip};
|
||||
|
||||
const FADE_DURATION_MS: u64 = 700;
|
||||
const MAX_TASK_ROWS: usize = 6;
|
||||
|
|
@ -154,6 +156,14 @@ struct DownloadStage {
|
|||
label: String,
|
||||
}
|
||||
|
||||
enum InstallUiEvent {
|
||||
Progress(usize, usize),
|
||||
Stage(DownloadStage),
|
||||
Activity(String),
|
||||
TotalEstimate(u64),
|
||||
Finished(Vec<InstallFailure>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct InstallHandles {
|
||||
progress: State<(usize, usize)>,
|
||||
|
|
@ -257,7 +267,7 @@ impl Component for OnboardingDownloading {
|
|||
let heading = if has_failures && is_complete {
|
||||
"Download issues found"
|
||||
} else if predownload {
|
||||
"Downloading your content"
|
||||
"Downloading..."
|
||||
} else {
|
||||
"Finishing up..."
|
||||
};
|
||||
|
|
@ -277,6 +287,8 @@ impl Component for OnboardingDownloading {
|
|||
1.0
|
||||
};
|
||||
|
||||
let tip = use_onboarding_tip();
|
||||
|
||||
if !*confirmed.read() {
|
||||
let items = onboarding_bundles_items(&bundles_query).unwrap_or_default();
|
||||
let selected = selected_state.read().clone();
|
||||
|
|
@ -365,7 +377,8 @@ impl Component for OnboardingDownloading {
|
|||
predownload,
|
||||
running: is_running,
|
||||
}))
|
||||
.child(rect().width(Size::flex(1.0))),
|
||||
.child(rect().width(Size::flex(1.0)))
|
||||
.child(tip_panel(tip)),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
|
|
@ -455,6 +468,11 @@ fn run_install_batch(plans: Vec<ClusterPlan>, predownload: bool, handles: Instal
|
|||
let total = plans.len();
|
||||
progress.set((0, total));
|
||||
|
||||
let (notif_tx, mut notif_rx) = mpsc::unbounded_channel::<Notification>();
|
||||
let notifier = NotificationService::new(notif_tx);
|
||||
|
||||
let (ui_tx, mut ui_rx) = mpsc::unbounded_channel::<InstallUiEvent>();
|
||||
|
||||
spawn(async move {
|
||||
let mut prev = 0u64;
|
||||
let mut speed = 0f64;
|
||||
|
|
@ -482,23 +500,52 @@ fn run_install_batch(plans: Vec<ClusterPlan>, predownload: bool, handles: Instal
|
|||
});
|
||||
|
||||
spawn(async move {
|
||||
let mut failed = Vec::new();
|
||||
|
||||
let (tx, mut rx) = mpsc::unbounded_channel::<Notification>();
|
||||
let notifier = NotificationService::new(tx);
|
||||
|
||||
spawn(async move {
|
||||
while let Some(notification) = rx.recv().await {
|
||||
let mut local = GroupedAgg::default();
|
||||
loop {
|
||||
let Some(first) = notif_rx.recv().await else {
|
||||
break;
|
||||
};
|
||||
if let Notification::GroupedProgress(event) = first {
|
||||
apply_grouped(&mut local, event);
|
||||
}
|
||||
// Drain everything already queued in one pass.
|
||||
while let Ok(notification) = notif_rx.try_recv() {
|
||||
if let Notification::GroupedProgress(event) = notification {
|
||||
let mut next = agg.peek().clone();
|
||||
apply_grouped(&mut next, event);
|
||||
agg.set(next);
|
||||
apply_grouped(&mut local, event);
|
||||
}
|
||||
}
|
||||
});
|
||||
agg.set(local.clone());
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
});
|
||||
|
||||
spawn(async move {
|
||||
while let Some(event) = ui_rx.recv().await {
|
||||
match event {
|
||||
InstallUiEvent::Progress(done, total) => progress.set((done, total)),
|
||||
InstallUiEvent::Stage(next) => stage.set(next),
|
||||
InstallUiEvent::Activity(text) => activity.set(text),
|
||||
InstallUiEvent::TotalEstimate(bytes) => total_estimate.set(bytes),
|
||||
InstallUiEvent::Finished(failed) => {
|
||||
agg.set(GroupedAgg::default());
|
||||
activity.set("Wrapping up...".to_string());
|
||||
invalidate_cluster_queries().await;
|
||||
failures.set(failed);
|
||||
running.set(false);
|
||||
complete.set(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut failed = Vec::new();
|
||||
|
||||
if predownload {
|
||||
activity.set("Calculating download size...".to_string());
|
||||
let _ = ui_tx.send(InstallUiEvent::Activity(
|
||||
"Calculating download size...".to_string(),
|
||||
));
|
||||
if let Ok(state) = oneclient_core::LauncherState::get() {
|
||||
let mut sum = 0u64;
|
||||
for plan in &plans {
|
||||
|
|
@ -515,19 +562,19 @@ fn run_install_batch(plans: Vec<ClusterPlan>, predownload: bool, handles: Instal
|
|||
"download size estimate failed: {err}"
|
||||
),
|
||||
}
|
||||
total_estimate.set(sum);
|
||||
let _ = ui_tx.send(InstallUiEvent::TotalEstimate(sum));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (index, plan) in plans.into_iter().enumerate() {
|
||||
stage.set(DownloadStage {
|
||||
let _ = ui_tx.send(InstallUiEvent::Stage(DownloadStage {
|
||||
index,
|
||||
total,
|
||||
label: plan.mc_version.clone(),
|
||||
});
|
||||
}));
|
||||
|
||||
if let Err(reason) = install_one(&plan, predownload, ¬ifier, activity).await {
|
||||
if let Err(reason) = install_one(&plan, predownload, ¬ifier, &ui_tx).await {
|
||||
tracing::error!(
|
||||
cluster_id = plan.cluster_id,
|
||||
mc_version = %plan.mc_version,
|
||||
|
|
@ -535,17 +582,11 @@ fn run_install_batch(plans: Vec<ClusterPlan>, predownload: bool, handles: Instal
|
|||
);
|
||||
failed.push(InstallFailure { plan, reason });
|
||||
}
|
||||
progress.set((index + 1, total));
|
||||
let _ = ui_tx.send(InstallUiEvent::Progress(index + 1, total));
|
||||
}
|
||||
|
||||
drop(notifier);
|
||||
agg.set(GroupedAgg::default());
|
||||
activity.set("Wrapping up...".to_string());
|
||||
|
||||
invalidate_cluster_queries().await;
|
||||
failures.set(failed);
|
||||
running.set(false);
|
||||
complete.set(true);
|
||||
let _ = ui_tx.send(InstallUiEvent::Finished(failed));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -553,12 +594,14 @@ async fn install_one(
|
|||
plan: &ClusterPlan,
|
||||
predownload: bool,
|
||||
notifier: &NotificationService,
|
||||
mut activity: State<String>,
|
||||
ui_tx: &mpsc::UnboundedSender<InstallUiEvent>,
|
||||
) -> Result<(), String> {
|
||||
let state = oneclient_core::LauncherState::get().map_err(|err| err.to_string())?;
|
||||
|
||||
if !plan.overrides.is_empty() {
|
||||
activity.set("Saving your package choices...".to_string());
|
||||
let _ = ui_tx.send(InstallUiEvent::Activity(
|
||||
"Saving your package choices...".to_string(),
|
||||
));
|
||||
}
|
||||
for (bundle_name, package_id) in &plan.overrides {
|
||||
oneclient_core::set_bundle_package_enabled(
|
||||
|
|
@ -579,7 +622,10 @@ async fn install_one(
|
|||
let session =
|
||||
GroupedProgressSession::start(notifier, format!("Downloading {}", plan.mc_version));
|
||||
|
||||
activity.set(format!("Downloading Minecraft {}...", plan.mc_version));
|
||||
let _ = ui_tx.send(InstallUiEvent::Activity(format!(
|
||||
"Downloading Minecraft {}...",
|
||||
plan.mc_version
|
||||
)));
|
||||
|
||||
let prepared = oneclient_core::ClusterManager::prepare(
|
||||
&state,
|
||||
|
|
@ -592,10 +638,10 @@ async fn install_one(
|
|||
.await;
|
||||
|
||||
let bundles_result = if prepared.is_ok() {
|
||||
activity.set(format!(
|
||||
let _ = ui_tx.send(InstallUiEvent::Activity(format!(
|
||||
"Installing mods & content for {}...",
|
||||
plan.mc_version
|
||||
));
|
||||
)));
|
||||
oneclient_core::install_cluster_bundles(
|
||||
plan.cluster_id,
|
||||
state.bundles.as_ref(),
|
||||
|
|
|
|||
113
packages/oneclient_app/src/view/onboarding/downloading/tips.rs
Normal file
113
packages/oneclient_app/src/view/onboarding/downloading/tips.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
use super::*;
|
||||
|
||||
use crate::theme::colors;
|
||||
|
||||
const ONBOARDING_TIPS_BACKUP: &[&str] = &[
|
||||
"418 I'm a teapot",
|
||||
"Floors 8 through 10 of the Catacombs were originally planned for release, but portions of it were later merged into Floor 7 and its Master Mode counterpart.",
|
||||
"At one point, Feather Client was willing to pay $300 USD to add Skytils, a popular SkyBlock mod.",
|
||||
"Grass is a plant with narrow leaves growing from the base. [...]\nGrasses are monocotyledon, herbaceous plants.",
|
||||
"In Canada, Santa's postal code is H0H 0H0.",
|
||||
"Guvf vf abg na vzcbegnag fragrapr.",
|
||||
"Vs lbh qrpvcure guvf, lbh fubhyq yvfgra gb Mrgihr'f zhfvp.",
|
||||
"Try to do something creative! It could be music, art, coding...\nYou won't know how much you like it until you try it, so give it a shot :)",
|
||||
"At one point in Minecraft, there was a rare chance of Snow Golems naturally spawning.",
|
||||
"W[h]yvest",
|
||||
"RUMOUR. Open your ears; 9r\"5j5&?OWTY Z0d",
|
||||
"Make sure to drink water! You never know if you might be dehydrated.",
|
||||
"People asking you to \"test their mod\" are likely trying to hack you.\nThe only ones you can trust are ones widely trusted by the community.",
|
||||
"Have a great day!",
|
||||
"You can sleep through a thunderstorm, even during the day",
|
||||
];
|
||||
|
||||
const FUNFACTS_URL: &str =
|
||||
"https://raw.githubusercontent.com/Polyfrost/DataStorage/main/oneclient/funfacts.txt";
|
||||
|
||||
fn normalize_line_breaks(value: &str) -> String {
|
||||
value.replace("/n", "\n").replace("\\n", "\n")
|
||||
}
|
||||
|
||||
fn parse_funfacts(text: &str) -> Vec<String> {
|
||||
text.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.map(normalize_line_breaks)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Loads onboarding tips (remote with backup fallback) and returns a single
|
||||
/// stable random tip for the lifetime of the component.
|
||||
pub(super) fn use_onboarding_tip() -> String {
|
||||
let mut tips = use_state(|| {
|
||||
ONBOARDING_TIPS_BACKUP
|
||||
.iter()
|
||||
.map(|tip| normalize_line_breaks(tip))
|
||||
.collect::<Vec<String>>()
|
||||
});
|
||||
|
||||
use_hook(move || {
|
||||
spawn(async move {
|
||||
let Ok(state) = oneclient_core::LauncherState::get() else {
|
||||
return;
|
||||
};
|
||||
match state.services.requester.http().get(FUNFACTS_URL).send().await {
|
||||
Ok(res) if res.status().is_success() => match res.text().await {
|
||||
Ok(text) => {
|
||||
let parsed = parse_funfacts(&text);
|
||||
if !parsed.is_empty() {
|
||||
tips.set(parsed);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("onboarding tips: reading body failed: {err}");
|
||||
}
|
||||
},
|
||||
Ok(res) => {
|
||||
tracing::warn!("onboarding tips: unexpected status {}", res.status());
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("onboarding tips: request failed, using backup facts: {err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let seed = use_hook(|| {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.subsec_nanos() as usize)
|
||||
.unwrap_or(0)
|
||||
});
|
||||
|
||||
let list = tips.read();
|
||||
if list.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
list[seed % list.len()].clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn tip_panel(tip: String) -> impl IntoElement {
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::px(320.))
|
||||
.spacing(4.)
|
||||
.cross_align(Alignment::End)
|
||||
.child(
|
||||
label()
|
||||
.text("TIP")
|
||||
.width(Size::fill())
|
||||
.font_size(11.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.text_align(TextAlign::Right)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(tip)
|
||||
.width(Size::fill())
|
||||
.font_size(13.)
|
||||
.text_align(TextAlign::Right)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
|
||||
pub const DUMMY_REPLACE_NEWLINE: &str = "\n";
|
||||
|
||||
// pub const MICROSOFT_CLIENT_ID: &str = "9eac3a4e-8cdd-43ef-863e-49cd601b1f03";
|
||||
pub const MICROSOFT_CLIENT_ID: &str = "c36a9fb6-4f2a-41ff-90bd-ae7cc92031eb";
|
||||
pub const MICROSOFT_CLIENT_ID: &str = "9419b7ee-1448-4d1b-b52a-550d8f36ab56";
|
||||
pub const MINECRAFT_SCOPES: &str = "XboxLive.SignIn XboxLive.offline_access";
|
||||
pub const CURSEFORGE_API_KEY: &str = "$2a$10$6utA1UNSmFPrE/Lh7b7ndeeGmiOkjKNY8kpFB0fsmE/d42ZAfFgCe";
|
||||
pub const DISCORD_CLIENT_ID: &str = "1426999264633946334";
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue