OneLauncher-Cracked/uncommitted.patch
Julian Chang 29d1f1c9d2
Bugs
Fixed #492
Fixed #510
Fixed #506
Fixed #568
Fixed #582
Fixed #578
Fixed #567
Fixed #588
Fixed #585
Fixed #571
Fixed #587
Fixed #584
2026-07-15 13:41:43 -04:00

5975 lines
220 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

diff --git a/Cargo.lock b/Cargo.lock
index 4bbfb520aaf2633fa51513fc3bd28d10a3c85a6b..1b2edf4f6723b081b4a862fdb9b66c6f3f3403fd 100644
GIT binary patch
delta 6903
zcmai3dyHL2ebyX%{aD+v7u&mw9XlIiJ0W2=^Eh*6CJJ5$wN+?K8z*T?5N4UDeUp7S
zyLW>fK}eJr6cS@j#Ha$bq_il1^g*&Jw<u{IMQsB`EfG?Ah2WNos;CVKeNt%B@7%j<
z-*qX~%JRAQo_l8I_xK*a`}vnPJ@vxo&um#UFvcdhsO`~<>#2dkCAP+`$*;|mvF@wR
zy2XJ(-)@~8_j4iod?pY(DU)*4a%Qyl%q6XqPo<J3CK-aYo|c|@lcJ{D8&>k@ozl)4
z6^yc6*X-uu{K?~FXZ_-n+wxUkBOCKoXNzrX7Fx6Q?|t=PK6GQTD}UipGB#?vD<5yS
zPwX4KJ<cz*=4Qv<Jho5d6Q3YAO_n<HWUYv*WI6_=srOcvnwucF(%hDmYD<;kf~HE9
zQh94>B}yivF}}WS|L`@v6}o5J_ol(IeZAN7?Y<WaasKv}kA3efwp-H+``(lvTOuR%
zx1Joj!kuczh4y@F_E`6>5a(OB$ElgQ`QEqUEEd3rQ}YY;)BikN?|*g2bvV-)o!}FU
zcfZn{(ataQ?r&ZM+wQ$+wdZX2mT5bCY|$Qz<86Bkt7Ee>t(lnFEr{0CbH%3okx!Gs
z`mR?;@-q*Sb@}N>$rS_L<J8Z7ez^X_c*9{<Che8<l4-*e)4}+%Vv>4ky>iiMWDU91
zUOO!;0u@S=N^b?1lBKBX$9`q_n$<`3>DYKHojXGRk>=yEcyfHYwR{eQG32`mxopEq
zjO%;vbB7I=+9+0PENi8)s^V3&%qUSN8mY3C)k@-ZjMzGJP6m;TiB)Ng_LZoa*jc~p
zt3&mvKfh#i@5xQV;(R@QnVqziq*WPhX_5VC6IF>PX9abouZ(o56i#ub42?z#8hGVh
zl?-JjB_(N7B3G<zw|mI*EBu6#$d{fi_SUcdVB_#iYp#3y!koV~wmHHgCm7AY^kMS)
z`sMG9>{<Po{@UgfG+%!?IWlQtStU_9L_ykAmM(bwbD~Plg_1LCaeig2aXu<uMixVK
zLK|Gs;8lKh4H<3%lka@^Tp^G<w-ZsH`QeR|T3Z%;NQsqI6@}-e){%K9y{N2WnsXfA
zhEix@j8I9-z*8Aa(oRts^0Q-Pc+>Qpx6|YAv9}`HuN)YyU;E0)Nbh9tiEdTLmi5rt
zn<hh|Ch}moFp7#0s*=h|NaGEsUL}FdR8q+3D`qJLo=}`SQq*LMl)IW;J#x!=Eb7Zg
z7a{uEcW$vA=leeYU;Vu5)BmHvGkcH>%L`h&uipQkF3<bV7Q0algKwERF+F|`3w}yF
zPtWz>qiO5z=I3U8obqvc8qwXg@mxRU!w-=I`Sc59+rAk)J$-xh7t~rcQFh;H<3aWF
zPi(JG{mbr+HiUUT-I|U0j~^y(k~NJ)mEhC~rdg<hGO~<SC0tNmsnTiz7~q^D_R<Ki
zD`ekM0D{pD`5YJ?IM$k(ZY>RMb_e=6<l9%RuODXrv3b0`ID2fN9cKXm`Pv7H?fKqc
zAisP_TknzffQLs`3KX*76`o3~M7i}gX$}(c9&l56V0mdP!!?VvlsdY6?U7=ne)+$L
z^1-vk>+A0ySYMy``3?2IeR*_XY#-v6U%9S0uzq>rnmxLw{`OS^OCRFLHxKw*+w-=z
zzqXyIeCZR#^^<@SoLmZJ7`RO0)D;IZ1fWC+Q45)bW)@+JSlToj@&KK{y^DsskpK2F
zw7|Kj?t4@H*oz}?oN3Lr?wFc8*$T@kbhkv`W0U#Cg|;6LaRxX(9rI~Mu3UQMP_?tZ
z>xQ*!8}g{XpxvadWQBv93P4qYFagS?vLzsy@|1$B&@DixNIiEvDrV4_*pjm}qLqAo
z=E>oYZ6#}K`lC(tGtZ1H9lYtc*R2D?mlJI1xp(|<``|3Fu|4JOLad*7w?8BVP8Z79
zFavs<m$D4#QCX%?S*%degg)dha-IYl6WkuWwrI=bqidGSN3Se4)I(1nTzY!;u8{$^
zc&ruj%ik>Qq=O)kctnx_m$F2fb*da_IcQ@&kjG<DSC!T_@%B`*7%6Qao;%NUeP+jS
zzUsTh;Ks$pRye0pmfn2-J!G_xVpqXuzJ612<Dmemh(J)oWQD-FvP=d~WYT$ON@sL)
z3SSCSMejjxoOvZoDLEvIWBK{dkddu@{XO2AIZ;3I`Q3XjcyD?Y$noCV$^euVN3C!c
zbj5`=Ql-iZ2VP-GIEhaX80DN(R)XuCO126Vq0u2f_Ba{Y<mb+%DL>}OSiWu_*}m=6
zY`3i!XIqV689!Oy#x7oZ`hknqz0S|ghw=HAKR$k9J}y9S#Qg5d3wy|b+hst_SjUuS
zrh-(9!pfwR^$<bWTLj%EU8)ql^+Mu8Sdmg`d0Eq|NAe}l7d%frM7Gu+6FWC_GeIYG
zex)E6FMaXBuU)dSH5(R=Pq*Ac{oJgZEU7aQkRE7Kk!o5&la&rvrqTjJutLwHs5lim
zBo0*phN~1~jyL370po}BUH260ho|iwr=}L#w%zK%8&<$YzVq$m4NH&wzU6Bh@Touh
z9d|fZ+&KK{m_RDp;+fu}Lut}RU<1*!3QQIZRazTl*?GjI(v22i!mIksmXRfL_OZd0
zK6(6m<>qw@?O6->9Qvf4teCRUnMj$%5}HwHqsjzojX@Y8F;O*!xk@`gq|uC96mqcC
z17E2}lk6C-ulnq!`so)p=7$+Mxb*k`{uC+bL_YFBapPn~!MhIfL@EVAE73GA>ZqW>
z5_Bv0w@h4u?#oaq5oKu&`V%6q3XHN;-@PzWkN;?B)9mft>Sze2wi|aXo%zo{A#0n?
z?@IgQ|Mk5cJs9ZT)iCC`n+vB_h`T=hqdy(!>dt)TDY8Ak^Y6&@hXbm$1WR*kg0umm
zNmn>lDl3s7>zTpcqKFZv$5tHECF+|(gG=v9bU`<=*Z=qjWF@k1dG+dZ*YADpgDcm0
zfDpS5DhbThNrYUwZNmy_<_Gt|aR_NO5D8J>GTA6Slz|mUDQcE`OPNYQZ{T1+CK{~b
z*qV?GA`w-f^4)(;MmzTpkxMqNtn$0N$XmB=>QmI&E@C0nNhwWLItSlUqAiSatYUy-
zbPW|2ABU)*R}Cu7tCB0!l&>Na;1L09Q;c^0>5b&JZR>i<C|`R3!F(|k*B>r}R-$1U
zv>-JO%m<;SDNsqo<h&78Su%tZh}CqaRFnpRra_{mf<A77*?Gs+1d{F(M~e*|_KW0B
za-MiQUw9iiv+i}RldCX^PKbQz)x`~yK5@v~1fWeQ;Yx5{@+vVr8_uZ|6<5&2WkO05
zAcF%Sh2{!c7+F_dWO6&XV#A91$zxBEUHRx>G19sDCh~#d`VZ#zcV4@NylXJiHW|q$
z7Re27hHogLt;@(uoKA`4DUuhOLq&vFL1GIk@glIK@ey`V1;<PT-|~F-Eo8Jl`qqo;
z@Bih7d~}Y;&e28k8$(#>U><&e+}BvtgZWeUk)7SuCK23F2^|G^1cU<t19|McmlPxb
zI)zPyYA!3d2CE>Ut@5A(fPI87aJ*6ajVC>jKYa%d@bUY}ru_18az}pYezGBld&y9}
z^-BZ!mKNEXKXErwI&&}CP)~koIN$RC8O>X#$-&MO4-i>2p~wiuo_zKL<kH^WOh|@t
zY{#XTl<>i_3atWYOi<PW6v&lONP`wa;2vY0Vvyv}Bvr)_`~Agme=Np#G@7EH_K;@P
zvkmYq6$dO(0rEkfkw%LH8wy!%%Or)xE-aG?Y>d>RenSZ|1XAiGMCz`UpZQ`j);aL&
zWU4q{U->sbOzvH+>P8<VM|ZRqeggT-JxHD%nvb`=2ex<|W}xST@|6#fJv(OG^QXEP
z1fIg>IAod6r>Be2eAf$zZhVM5w|dpV&XLpP?E}LLQ?1!Fx9Z$tg4N`T8zv#oco%mZ
zO3Eh|tm4o|uuqan>5X%ca}1~%9r8fS2B|?3x`fDr5{8-`$+HF7154A>;Iplh`H?%w
z?xys5$sS&8jo;d81Ej=6&$Bm<HfC};VVy@GCVz2o8CvQy;y`}ahsmbSZ#_XCFIE8O
z$W!EJM%Ta&=eJ!`xJfXw79~_i;H8R{N+ocuD?kA2Du9+GP)$N~!;XVr0361HhE!lW
z8I_Ox5s>Ee-;i&tU}&fHHL_*RGSFtc@SU%JlVq~>!X!NZufRD47Y8mjP$gC6wDe`f
zHHF&NO;gwg)xE>QzEtR98eM`@kKi7`ZR{H3`XdwTHua1A#xFJFoDGcyzGyl7h!+zR
zFr9+s7oj2s`&_}cZ;;D7C%#4gY~!vKS-dK{`050&AK}pIs&ukS$!doL!naUkrO?2B
z=PmjLV;vqG0M3>imeMsw#bFc$)d{H)>qm}^=9j-i)^@}{kdd8ho4U(qmyiZv2K2R3
zfOw3D@E<T=R#8d}ukc*pKTs+`e^=5$MuPl=MSU?OEPw?YAN>0Iz2DvP{#7%Z)o1S(
zd!zoFp%KP5%XQy%%KdLR+w~~Rqo(D%pak3TWP8;&(QrL?v3w7Vzgol`W(L!tRX3in
zkc$0FG`+SmrE1UJ+M27s^~4)C_iv~hmctA%4Br!lYDObyn232n2|43|TNunh(m~*A
z1Xs%n!X2|BBQ0{`6a0fzn92-~oFCqux8}ftuCG2<C+m8P=SzRJxMbV%M$QGIm!lHx
z3hTg<3s82!o9L)knBoLa8HF_9QUF+urNKl@L@ZcLC(kSE8w1lw%Dv|v*-J(<n(B*7
zclE8ytFJ`7yZ9l9*=PWj6jLIK`9#3jL`LWgh_nQ$4|7T}_B0hm%h(uYU>1`#4Nt|K
z=sJm?J3iMFlid}2Iu>Oxw~S{^aJt@bYI(%jw=qmzms7ns`_XImtOV<kFBLoT_m?0Z
zqfZp=iuo4o4uC>nsMWPxw$#w0M#>7q*knzGgrQ3sZcG)&_y_u&m5qPfId5kJ+SDvh
z4VDu!{&liH-*aQJXZvzoP-W+ete2RCxj{p;9{vXIF$VZ3fkBdq(7?c8txAf%f!_xn
zBO+1(%piW?HDO_a+~dPL(>eRlckFIHR?4zl#Jyxt2S+RdOq+wP;aoB=AuAwO0P;|0
zu#Ld4hVg-y6=MKa$~}?<$DU|yUmMwdVToP1>P5>pEO$!ppm4~%g1`fz04M>!%_vbb
zU=MJ*nb0LnFDfUj)fiR4d_WRM9Uyx+1iR}_%NljDW9+9a-_3Qt>z&2kyx}z2QYSL9
z5`y-r6LF#0ia&(e88s$=GgPJFAt~l1C^*d6L*p+oK@9;k!6<|)4iD{CVQ^vLvXCeA
z!p*}lv0abeJ^AtFJ~&?k`Pzrb-h6h1Y`=JHUo%1K-nTeE-TS}aiFdC1F=_20vi>Bw
zVG^QSD2xnZ5RwM)T3|i^N8oHyf(Z<$%rZ(Xp}YcgbmZ{HFba`3iVgEhOcd)A4{ogI
zK6Oz(`;TOA=i|f0U#;(x0<Y~Z209<uTRgt@!Z&YzL$SYD-<=Rlu+IAq6fgA)<NGcv
z-n(aohRM$s#YKHAc`)B`q}bWPZw~L@+|Ny)yAtex9cs)8H1=&Vx^yByF`)inWAQ6P
zh0%~TQ2xBZ$K)X6BYunU94x`5Da%GwE)Qe2T#SLf9bfq+pci}Ct)3;z@4K}aMYzvr
zp?kx^amyP1gf9Yr?pnLpPUCrKbFsVg<W%uRvIhP%pMDu^j=2rIZlcYoK9&&C4PtX>
zAm}})NNJRT1J+g57*l|NLCeU>K#Rg(1iupd_4NxQBb|F~@k7|8?#QTvjcqH6ah+d!
zH?fDQK`?Me4x)w4g6P6uguCZ}Lu4MP3&AR#g8k#D#D<m8AS#4FF;_PE6$jPwp_$^N
zB!BLO;-<rJ;vSAUXx0=%<9`rrcp`<K;*~;6B|NIq&1e9Qz4-|V?p|SLjLs`v!!tYc
s-mgHS{Z7D>x6Bn++}vcEPw<X<u6RGjg3XUx`Rp!oap%iti-qC;0XmRen*aa+
delta 535
zcmX9)T}V@L9Oe9`H+S9E*4(_^vD*|(aMJqF2o_;Mgb#`#tnwim1r38lF)%4tFIi%c
zUERO(r4S{C;O4*h4Xj?Qz&>B>EokedH>qBRW!Rm2JDhVkoO51%bu6z{XPh>3-`;?X
z*e==3frM`6RaL%Lu2f2?ooJ9aqCwthOE{#_+2WIjE<91>x88NHTuF}9$^#E>LVlih
zDza|o%Pv`5KDS@`iW!yP11%0Ny6^%#>%lxk!G?es*?}rC>_tJuYp}^7KYpPEz1*as
z5qxSV?vx3!uA`GX0;qFyq#1^tyIaw|o#V&R=Vmj8RgbyxTji?>48j_qv#S>q3eWXp
z)Gy*=ILv*w(WY|F#6wSsFwAkKN?e~s(!7667okxo9DRybDv1YK%;PMHiL})+ya>Ap
zEnrw=7SJt%87zs#4~U3p7N52(Y+2%!{4<BQy9Htt5SjyCF<w9u*W@uq_WEWuR6jTn
zKW|=c4D!40Ff>m8#cFxoRGPYkyN7(d@efm01M7n4Y5D<nRnQwdKXQ@J$4B?jMV+VH
zDYs2zJILUnlO!w3uq#Fl#Me&K8o0lQQbBeO5xKUKtaL9&Z_%WK!;`cE{+OboUA#0&
g6VFnl@t?<(2a6fH2d<tY{S1dUDXDR`)^^wbA82X75dZ)H
diff --git a/Cargo.toml b/Cargo.toml
index fd4a499..3a93c21 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,12 +28,24 @@ oneclient_macro = { path = "./packages/oneclient_macro" }
# core (extra)
gamemode_rs = {path = "./packages/gamemode_rs"}
interfrost = {version = "=1.3.2"}
+discord-rich-presence = {version = "=1.1.0"}
# logging
tracing = {version = "=0.1.44"}
tracing-futures = {version = "=0.2.5"}
tracing-subscriber = {version = "=0.3.23", features = ["env-filter", "fmt", "ansi", "json", "parking_lot", "registry"]}
+sentry = {version = "=0.48.5", default-features = false, features = [
+ "backtrace",
+ "contexts",
+ "debug-images",
+ "panic",
+ "release-health",
+ "reqwest",
+ "rustls",
+]}
+sentry-tracing = {version = "=0.48.5"}
+
# database
sqlx = {version = "=0.9.0", default-features = false, features = [
"runtime-tokio",
@@ -65,6 +77,7 @@ reqwest = {version = "=0.13.3", default-features = false, features = [
"form",
"hickory-dns",
]}
+reqwest-websocket = {version = "=0.6.0"}
tokio = {version = "=1.52.3", features = ["full"]}
tokio-stream = {version = "=0.1.18", features = ["full"]}
tokio-util = {version = "=0.7.18", features = ["full"]}
diff --git a/packages/oneclient_app/examples/min_freya.rs b/packages/oneclient_app/examples/min_freya.rs
index dafed10..34bcb9b 100644
--- a/packages/oneclient_app/examples/min_freya.rs
+++ b/packages/oneclient_app/examples/min_freya.rs
@@ -1,4 +1,3 @@
-
use freya::prelude::*;
struct MinApp;
diff --git a/packages/oneclient_app/src/assets.rs b/packages/oneclient_app/src/assets.rs
index 89639f7..23c9712 100644
--- a/packages/oneclient_app/src/assets.rs
+++ b/packages/oneclient_app/src/assets.rs
@@ -4,17 +4,12 @@ pub struct AppAssets;
impl AppAssets {
pub fn get_bytes(path: &str) -> Option<bytes::Bytes> {
- AppAssets::get(path).map(|file| {
- match file.data {
- std::borrow::Cow::Borrowed(slice) => {
- bytes::Bytes::copy_from_slice(slice)
- },
- std::borrow::Cow::Owned(vec) => {
- tracing::warn!("Cloning asset data for '{}'", path);
- bytes::Bytes::from(vec)
- },
+ AppAssets::get(path).map(|file| match file.data {
+ std::borrow::Cow::Borrowed(slice) => bytes::Bytes::copy_from_slice(slice),
+ std::borrow::Cow::Owned(vec) => {
+ tracing::warn!("Cloning asset data for '{}'", path);
+ bytes::Bytes::from(vec)
}
})
}
}
-
diff --git a/packages/oneclient_app/src/bridge/commands.rs b/packages/oneclient_app/src/bridge/commands.rs
index 5e64e9f..63637f4 100644
--- a/packages/oneclient_app/src/bridge/commands.rs
+++ b/packages/oneclient_app/src/bridge/commands.rs
@@ -1,4 +1,3 @@
-
use std::path::PathBuf;
use oneclient_core::notification::UserChoice;
@@ -12,25 +11,43 @@ use crate::notifications::{ClusterUpdateSummary, NotificationSpec};
pub enum BridgeCommand {
ReloadSettings,
SaveSettings,
- SetSettings { settings: LauncherSettings },
- RecordSeenVersion { version: String },
+ SetSettings {
+ settings: LauncherSettings,
+ },
+ RecordSeenVersion {
+ version: String,
+ },
MarkOnboardingSeen,
+ AcceptTos {
+ terms_version: u32,
+ privacy_version: u32,
+ },
- SaveGlobalProfile { profile: GameSettingsProfile },
- UpdateGlobalProfile { update: ProfileUpdate },
+ SaveGlobalProfile {
+ profile: GameSettingsProfile,
+ },
+ UpdateGlobalProfile {
+ update: ProfileUpdate,
+ },
- CreateSettingsProfile { name: String },
+ CreateSettingsProfile {
+ name: String,
+ },
CreateProfileFromGlobal {
name: String,
mem_max: Option<u32>,
force_fullscreen: Option<bool>,
},
- UpsertNamedProfile { profile: GameSettingsProfile },
+ UpsertNamedProfile {
+ profile: GameSettingsProfile,
+ },
UpdateNamedProfile {
name: String,
update: ProfileUpdate,
},
- DeleteNamedProfile { name: String },
+ DeleteNamedProfile {
+ name: String,
+ },
UpdateClusterProfile {
cluster_id: ClusterId,
@@ -49,8 +66,12 @@ pub enum BridgeCommand {
vendor: oneclient_core::java::JavaVendor,
major: u32,
},
- AddCustomJavaRuntime { path: PathBuf },
- RemoveJavaRuntime { path: String },
+ AddCustomJavaRuntime {
+ path: PathBuf,
+ },
+ RemoveJavaRuntime {
+ path: String,
+ },
ToggleNotificationCenter,
CloseNotificationCenter,
@@ -64,14 +85,20 @@ pub enum BridgeCommand {
AnswerPrompt(UserChoice),
OpenClusterUpdate(ClusterUpdateSummary),
CloseClusterUpdate,
- SendNotification { spec: NotificationSpec },
+ SendNotification {
+ spec: NotificationSpec,
+ },
SendTestProgress {
current: u64,
total: u64,
},
- LaunchCluster { cluster_id: ClusterId },
- KillCluster { cluster_id: ClusterId },
+ LaunchCluster {
+ cluster_id: ClusterId,
+ },
+ KillCluster {
+ cluster_id: ClusterId,
+ },
DismissGameError,
ImportLocalFile {
@@ -91,7 +118,9 @@ pub enum BridgeCommand {
bundle_name: String,
skip_compatibility: bool,
},
- ApplyBundleUpdates { cluster_id: ClusterId },
+ ApplyBundleUpdates {
+ cluster_id: ClusterId,
+ },
SyncBundles,
ImportLauncher {
diff --git a/packages/oneclient_app/src/bridge/mod.rs b/packages/oneclient_app/src/bridge/mod.rs
index 54cfff5..a6b5cce 100644
--- a/packages/oneclient_app/src/bridge/mod.rs
+++ b/packages/oneclient_app/src/bridge/mod.rs
@@ -1,4 +1,3 @@
-
mod commands;
mod runtime;
mod snapshot;
diff --git a/packages/oneclient_app/src/bridge/runtime.rs b/packages/oneclient_app/src/bridge/runtime.rs
index 7f3f70b..919ee01 100644
--- a/packages/oneclient_app/src/bridge/runtime.rs
+++ b/packages/oneclient_app/src/bridge/runtime.rs
@@ -9,8 +9,8 @@ use tokio::sync::{mpsc, watch};
use crate::components::IconType;
use crate::notifications::{
- ClusterUpdateSummary, InboxEntry, MESSAGE_TOAST_TTL, NotificationAction, NotificationActionKind,
- NotificationSpec, NotificationState, PendingPrompt, PendingPromptView,
+ ClusterUpdateSummary, InboxEntry, MESSAGE_TOAST_TTL, NotificationAction,
+ NotificationActionKind, NotificationSpec, NotificationState, PendingPrompt, PendingPromptView,
};
use super::commands::BridgeCommand;
@@ -243,6 +243,7 @@ impl CoreBridgeRuntime {
LauncherState::initialize(notifier).await?;
oneclient_core::status::start();
+ oneclient_core::plus::start();
let data_dir = oneclient_core::paths::launcher_dir()
.map(|p| p.display().to_string())
@@ -277,10 +278,12 @@ impl CoreBridgeRuntime {
snapshots.settings.error = None;
let state = LauncherState::get()?;
let loaded = store::load_settings(Some(&state.services.notifier)).await;
+ let discord_enabled = loaded.discord_enabled;
{
let mut lock = state.settings.write();
*lock = loaded;
}
+ state.discord.set_enabled(discord_enabled);
refresh_settings(snapshots)?;
}
BridgeCommand::SaveSettings => {
@@ -298,6 +301,7 @@ impl CoreBridgeRuntime {
mutate_settings(snapshots, |s| *s = settings)?;
let state = LauncherState::get()?;
let snapshot = state.settings.read().clone();
+ state.discord.set_enabled(snapshot.discord_enabled);
if let Err(err) = save_settings(&snapshot).await {
snapshots.settings.error = Some(err.to_string());
}
@@ -319,6 +323,20 @@ impl CoreBridgeRuntime {
snapshots.settings.error = Some(err.to_string());
}
}
+ BridgeCommand::AcceptTos {
+ terms_version,
+ privacy_version,
+ } => {
+ mutate_settings(snapshots, |settings| {
+ settings.accepted_tos_version = terms_version;
+ settings.accepted_privacy_version = privacy_version;
+ })?;
+ let state = LauncherState::get()?;
+ let snapshot = state.settings.read().clone();
+ if let Err(err) = save_settings(&snapshot).await {
+ snapshots.settings.error = Some(err.to_string());
+ }
+ }
BridgeCommand::SaveGlobalProfile { profile } => {
let state = LauncherState::get()?;
@@ -609,20 +627,18 @@ impl CoreBridgeRuntime {
version_id,
} => {
let state = LauncherState::get()?;
- match install_package(&state, provider, &project_id, &version_id, cluster_id).await
- {
- Ok(()) => {
- state
- .services
- .notifier
- .send_info("Installed", "Package added to cluster");
- bump_clusters_generation(snapshots);
+ tokio::spawn(async move {
+ let notifier = state.services.notifier.clone();
+ match install_package(&state, provider, &project_id, &version_id, cluster_id)
+ .await
+ {
+ Ok(name) => {
+ notifier.send_info("Installed", &format!("Added {name}"));
+ notifier.invalidate_clusters();
+ }
+ Err(err) => notifier.send_error("Install failed", &err.to_string()),
}
- Err(err) => state
- .services
- .notifier
- .send_error("Install failed", &err.to_string()),
- }
+ });
}
BridgeCommand::InstallBundle {
@@ -631,15 +647,24 @@ impl CoreBridgeRuntime {
skip_compatibility,
} => {
let state = LauncherState::get()?;
- oneclient_core::install_bundle(
- cluster_id,
- &bundle_name,
- skip_compatibility,
- state.bundles.as_ref(),
- &state.services,
- )
- .await?;
- bump_clusters_generation(snapshots);
+ tokio::spawn(async move {
+ let notifier = state.services.notifier.clone();
+ match oneclient_core::install_bundle(
+ cluster_id,
+ &bundle_name,
+ skip_compatibility,
+ state.bundles.as_ref(),
+ &state.services,
+ )
+ .await
+ {
+ Ok(_) => {
+ notifier.send_info("Installed", &format!("Added {bundle_name}"));
+ notifier.invalidate_clusters();
+ }
+ Err(err) => notifier.send_error("Install failed", &err.to_string()),
+ }
+ });
}
BridgeCommand::ApplyBundleUpdates { cluster_id } => {
let state = LauncherState::get()?;
@@ -792,7 +817,7 @@ async fn install_package(
project_id: &str,
version_id: &str,
cluster_id: i64,
-) -> anyhow::Result<()> {
+) -> anyhow::Result<String> {
let provider_impl = state.services.packages.get(provider)?;
let project = provider_impl
.get_project(project_id, &state.services)
@@ -823,7 +848,7 @@ async fn install_package(
child.finish();
session.finish();
result?;
- Ok(())
+ Ok(project.name)
}
fn apply_game_stage(snapshots: &mut BridgeSnapshot, cluster_id: i64, stage: LaunchStage) {
diff --git a/packages/oneclient_app/src/bridge/snapshot.rs b/packages/oneclient_app/src/bridge/snapshot.rs
index baa7908..b878408 100644
--- a/packages/oneclient_app/src/bridge/snapshot.rs
+++ b/packages/oneclient_app/src/bridge/snapshot.rs
@@ -32,10 +32,7 @@ impl GameSnapshot {
}
pub fn logs_for(&self, cluster_id: i64) -> Arc<Vec<Arc<str>>> {
- self.logs
- .get(&cluster_id)
- .cloned()
- .unwrap_or_default()
+ self.logs.get(&cluster_id).cloned().unwrap_or_default()
}
}
diff --git a/packages/oneclient_app/src/components/active_cluster_panel.rs b/packages/oneclient_app/src/components/active_cluster_panel.rs
index 773865f..9e703b5 100644
--- a/packages/oneclient_app/src/components/active_cluster_panel.rs
+++ b/packages/oneclient_app/src/components/active_cluster_panel.rs
@@ -60,7 +60,9 @@ impl Component for ActiveClusterPanel {
let p = intro.get().value();
let slide_x = (p - 1.0) * 48.0;
- let parsed = active.as_ref().and_then(|c| parse_mc_version(&c.mc_version));
+ let parsed = active
+ .as_ref()
+ .and_then(|c| parse_mc_version(&c.mc_version));
let metadata = use_version_metadata(
parsed.as_ref().map(|p| p.major),
parsed.and_then(|p| p.minor),
@@ -114,7 +116,11 @@ impl Component for ActiveClusterPanel {
.spacing(8.)
.cross_align(Alignment::Center)
.margin(Gaps::new(8., 0., 0., 0.))
- .child(launch_button(cluster_id, dispatch, launch_button_state(&game, cluster_id)))
+ .child(launch_button(
+ cluster_id,
+ dispatch,
+ launch_button_state(&game, cluster_id),
+ ))
.child(cluster_settings_button(cluster_id)),
)
}
diff --git a/packages/oneclient_app/src/components/button.rs b/packages/oneclient_app/src/components/button.rs
index cf9ac79..61a3d4e 100644
--- a/packages/oneclient_app/src/components/button.rs
+++ b/packages/oneclient_app/src/components/button.rs
@@ -3,7 +3,10 @@ use std::borrow::Cow;
use freya::prelude::*;
-use crate::{components::{IconType, Icon}, theme::colors};
+use crate::{
+ components::{Icon, IconType},
+ theme::colors,
+};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ButtonVariant {
@@ -285,8 +288,7 @@ impl Component for Button {
rect.get_style().clone_from(&self.style);
- rect = rect.background(background)
- .color(foreground);
+ rect = rect.background(background).color(foreground);
if let Some(border) = border_color {
rect = rect.border(
@@ -420,7 +422,11 @@ fn size_layout(size: ButtonSize) -> (Gaps, CornerRadius, f32, Size, Size) {
pub fn link_button() -> Button {
Button::new()
.primary()
- .child(Icon::new(IconType::LinkExternal01).size(14.).color(colors::fg_primary()))
+ .child(
+ Icon::new(IconType::LinkExternal01)
+ .size(14.)
+ .color(colors::fg_primary()),
+ )
.child(label().text("Open"))
}
@@ -433,4 +439,4 @@ pub fn open_folder_button(folder: std::path::PathBuf) -> Button {
crate::platform::open_url(&folder.to_string_lossy());
})
.child(Icon::new(IconType::Folder).size(16.))
-}
\ No newline at end of file
+}
diff --git a/packages/oneclient_app/src/components/cluster_update_popup.rs b/packages/oneclient_app/src/components/cluster_update_popup.rs
index 7880627..3c5f35a 100644
--- a/packages/oneclient_app/src/components/cluster_update_popup.rs
+++ b/packages/oneclient_app/src/components/cluster_update_popup.rs
@@ -49,7 +49,13 @@ fn dialog(summary: &ClusterUpdateSummary, dispatch: crate::BridgeDispatch) -> im
.corner_radius(CornerRadius::new_all(16.))
.background(CARD_BG)
.border(border_all_color(1., colors::component_border()))
- .shadow(Shadow::from((0., 18., 52., 0., Color::from_argb(150, 0, 0, 0))))
+ .shadow(Shadow::from((
+ 0.,
+ 18.,
+ 52.,
+ 0.,
+ Color::from_argb(150, 0, 0, 0),
+ )))
.child(banner(summary))
.child(content(summary, dispatch))
}
@@ -93,7 +99,11 @@ fn banner(summary: &ClusterUpdateSummary) -> impl IntoElement {
.horizontal()
.cross_align(Alignment::Center)
.spacing(5.)
- .child(Icon::new(IconType::RefreshCw01).size(12.).color(Color::WHITE))
+ .child(
+ Icon::new(IconType::RefreshCw01)
+ .size(12.)
+ .color(Color::WHITE),
+ )
.child(
label()
.text("Updated")
diff --git a/packages/oneclient_app/src/components/dropdown.rs b/packages/oneclient_app/src/components/dropdown.rs
index f40a0eb..5ebecd2 100644
--- a/packages/oneclient_app/src/components/dropdown.rs
+++ b/packages/oneclient_app/src/components/dropdown.rs
@@ -81,8 +81,8 @@ impl Component for Dropdown {
};
let visible = options.len().min(MAX_VISIBLE_OPTIONS);
- let list_h = visible as f32 * OPTION_HEIGHT
- + visible.saturating_sub(1) as f32 * OPTION_SPACING;
+ let list_h =
+ visible as f32 * OPTION_HEIGHT + visible.saturating_sub(1) as f32 * OPTION_SPACING;
let offset_y = match (button_area(), list_size()) {
(Some(button), Some(list)) => {
@@ -141,10 +141,10 @@ impl Component for Dropdown {
.font_size(12.)
.color(colors::fg_primary()),
)
- .child(Icon::new(
- IconType::ChevronDown)
- .size(14.)
- .color(colors::fg_secondary()),
+ .child(
+ Icon::new(IconType::ChevronDown)
+ .size(14.)
+ .color(colors::fg_secondary()),
),
)
.maybe_child(is_open.then(|| {
diff --git a/packages/oneclient_app/src/components/dynamic_art.rs b/packages/oneclient_app/src/components/dynamic_art.rs
index 4c25af2..636734c 100644
--- a/packages/oneclient_app/src/components/dynamic_art.rs
+++ b/packages/oneclient_app/src/components/dynamic_art.rs
@@ -83,9 +83,12 @@ pub fn use_art_bytes(
let state = reader.state();
match (&art_url, &*state) {
(Some(url), QueryStateData::Settled { res: Ok(bytes), .. })
- | (Some(url), QueryStateData::Loading { res: Some(Ok(bytes)) }) => {
- (format!("{max_edge}|{url}"), bytes.clone())
- }
+ | (
+ Some(url),
+ QueryStateData::Loading {
+ res: Some(Ok(bytes)),
+ },
+ ) => (format!("{max_edge}|{url}"), bytes.clone()),
_ => (
format!("{max_edge}|{HOME_BACKGROUND_ASSET}"),
fallback.read().clone(),
diff --git a/packages/oneclient_app/src/components/log_viewer.rs b/packages/oneclient_app/src/components/log_viewer.rs
index 80f531f..306991f 100644
--- a/packages/oneclient_app/src/components/log_viewer.rs
+++ b/packages/oneclient_app/src/components/log_viewer.rs
@@ -1,4 +1,3 @@
-
use std::sync::{Arc, LazyLock};
use freya::prelude::*;
@@ -289,12 +288,11 @@ fn auto_scroll_toggle(mut auto_scroll: State<bool>) -> impl IntoElement {
} else {
colors::fg_secondary()
}))
- .child(
- label()
- .text("Auto-scroll")
- .font_size(11.)
- .color(if on { colors::brand() } else { colors::fg_secondary() }),
- )
+ .child(label().text("Auto-scroll").font_size(11.).color(if on {
+ colors::brand()
+ } else {
+ colors::fg_secondary()
+ }))
.into_element()
}
@@ -308,11 +306,15 @@ struct PosGeom {
corrected_y: f32,
}
-fn pos_at_screen(lines: &[Arc<str>], geom: PosGeom, screen_x: f32, screen_y: f32) -> (usize, usize) {
+fn pos_at_screen(
+ lines: &[Arc<str>],
+ geom: PosGeom,
+ screen_x: f32,
+ screen_y: f32,
+) -> (usize, usize) {
let region_y = screen_y - geom.viewport_top_px;
let band_y = region_y - geom.corrected_y - BODY_PAD_TOP;
- let line =
- ((band_y / LINE_H).floor() as i64).clamp(0, (geom.count as i64 - 1).max(0)) as usize;
+ let line = ((band_y / LINE_H).floor() as i64).clamp(0, (geom.count as i64 - 1).max(0)) as usize;
let text_origin_x = ROW_PAD_X + geom.num_col_w + NUM_GAP;
let local_x = screen_x - geom.viewport_left_px - geom.corrected_x - text_origin_x;
diff --git a/packages/oneclient_app/src/components/major_version_card.rs b/packages/oneclient_app/src/components/major_version_card.rs
index b8c1600..641b6ff 100644
--- a/packages/oneclient_app/src/components/major_version_card.rs
+++ b/packages/oneclient_app/src/components/major_version_card.rs
@@ -55,11 +55,7 @@ impl Component for MajorVersionCard {
let on_press = self.on_press.clone();
let opacity = if selected || hovered || focused {
- if selected {
- 1.0
- } else {
- 0.85
- }
+ if selected { 1.0 } else { 0.85 }
} else {
0.6
};
@@ -108,7 +104,10 @@ impl Component for MajorVersionCard {
.main_align(Alignment::SpaceBetween)
.corner_radius(CornerRadius::new_all(12.))
.cross_align(Alignment::Start)
- .border(border_all_color(1., border_color).alignment(BorderAlignment::Inner))
+ .border(
+ border_all_color(1., border_color)
+ .alignment(BorderAlignment::Inner),
+ )
.layer(Layer::Relative(3))
.background(
LinearGradient::new()
diff --git a/packages/oneclient_app/src/components/microsoft_login.rs b/packages/oneclient_app/src/components/microsoft_login.rs
index b84e4df..27309f4 100644
--- a/packages/oneclient_app/src/components/microsoft_login.rs
+++ b/packages/oneclient_app/src/components/microsoft_login.rs
@@ -84,7 +84,7 @@ pub fn use_microsoft_login() -> MicrosoftLogin {
return;
}
- // open the browser immediately
+ // open the browser immediately
platform::open_url(session.auth_url());
finish.mutate(session.clone());
pending_login.set(Some(session));
@@ -100,7 +100,7 @@ pub fn use_microsoft_login() -> MicrosoftLogin {
}
});
- // is true once the flow has some status updates (just to ensure the user has seen the popup) and the flow is still in progress
+ // is true once the flow has some status updates (just to ensure the user has seen the popup) and the flow is still in progress
let locked = status.as_ref().is_some_and(|s| s.current > 0);
let in_flight = begin.read().state().is_loading() || finish.read().state().is_loading();
let pending = in_flight && !*cancelled.read();
diff --git a/packages/oneclient_app/src/components/navbar/app_navbar.rs b/packages/oneclient_app/src/components/navbar/app_navbar.rs
index 898dfb7..d902086 100644
--- a/packages/oneclient_app/src/components/navbar/app_navbar.rs
+++ b/packages/oneclient_app/src/components/navbar/app_navbar.rs
@@ -37,7 +37,7 @@ impl Component for Navbar {
.window_drag()
.width(Size::window_percent(100.))
.height(Size::px(theme::NAVBAR_HEIGHT_PX))
- .position(Position::new_absolute().top(0.).left(0.).right(0.))
+ .position(Position::new_absolute().top(0.).left(0.).right(0.)),
)
}
}
@@ -95,8 +95,8 @@ struct NavLink {
impl Component for NavLink {
fn render(&self) -> impl IntoElement {
let mut hovering = use_state(|| false);
- let a11y_id = use_a11y();
- let focused = use_focus(a11y_id);
+ let a11y_id = use_a11y();
+ let focused = use_focus(a11y_id);
let active = self.active;
let target = self.target.clone();
@@ -125,12 +125,12 @@ impl Component for NavLink {
.cross_align(Alignment::Center)
.spacing(2.)
.width(Size::px(nav_label.len() as f32 * 10. + 10.))
- // TODO: Hacky workaround for weird freya measurement bug, where if a rectangle has a transparent background (so in dec = 0), it will be measured weirdly.
- // This basically fools freya into thinking it indeed has a box and properly handles pointer events
- .background(Color::RED.with_a(0))
- .a11y_id(a11y_id)
- .a11y_focusable(true)
- .a11y_role(AccessibilityRole::Button)
+ // TODO: Hacky workaround for weird freya measurement bug, where if a rectangle has a transparent background (so in dec = 0), it will be measured weirdly.
+ // This basically fools freya into thinking it indeed has a box and properly handles pointer events
+ .background(Color::RED.with_a(0))
+ .a11y_id(a11y_id)
+ .a11y_focusable(true)
+ .a11y_role(AccessibilityRole::Button)
.on_all_press(move |e: Event<PressEventData>| {
e.prevent_default();
let _ = RouterContext::get().push(target.clone());
diff --git a/packages/oneclient_app/src/components/package_row.rs b/packages/oneclient_app/src/components/package_row.rs
index 4742b31..7e6d132 100644
--- a/packages/oneclient_app/src/components/package_row.rs
+++ b/packages/oneclient_app/src/components/package_row.rs
@@ -391,9 +391,12 @@ fn remote_icon(
let reader = icon_query.read();
let loaded = match (icon_url, &*reader.state()) {
(Some(url), QueryStateData::Settled { res: Ok(bytes), .. })
- | (Some(url), QueryStateData::Loading { res: Some(Ok(bytes)) }) => {
- Some((url.clone(), bytes.clone()))
- }
+ | (
+ Some(url),
+ QueryStateData::Loading {
+ res: Some(Ok(bytes)),
+ },
+ ) => Some((url.clone(), bytes.clone())),
_ => None,
};
@@ -420,7 +423,11 @@ fn icon_box(icon: IconType, size: f32) -> Element {
.height(Size::px(size))
.corner_radius(CornerRadius::new_all(8.))
.background(colors::component_bg())
- .child(Icon::new(icon).size(size * 0.4).color(colors::fg_secondary()))
+ .child(
+ Icon::new(icon)
+ .size(size * 0.4)
+ .color(colors::fg_secondary()),
+ )
.into_element()
}
@@ -436,7 +443,10 @@ fn meta_size(size: u64) -> impl IntoElement {
}
fn provider_badge(provider: ProviderId) -> Element {
- badge(Icon::new(provider).size(12.).into_element(), provider.to_string())
+ badge(
+ Icon::new(provider).size(12.).into_element(),
+ provider.to_string(),
+ )
}
fn local_badge() -> Element {
diff --git a/packages/oneclient_app/src/components/player_model.rs b/packages/oneclient_app/src/components/player_model.rs
index 1f5054a..b7151b9 100644
--- a/packages/oneclient_app/src/components/player_model.rs
+++ b/packages/oneclient_app/src/components/player_model.rs
@@ -128,7 +128,7 @@ impl Component for PlayerModel {
let (w, h) = (ctx.size.width, ctx.size.height);
- if w <= 0.0 || h <= 0.0 {
+ if w <= 0.0 || h <= 0.0 {
return;
}
diff --git a/packages/oneclient_app/src/components/recents_row.rs b/packages/oneclient_app/src/components/recents_row.rs
index 3a3c80b..93a9d85 100644
--- a/packages/oneclient_app/src/components/recents_row.rs
+++ b/packages/oneclient_app/src/components/recents_row.rs
@@ -49,7 +49,9 @@ impl Component for RecentsRow {
conf.on_creation(OnCreation::Run);
conf.on_change(OnChange::Rerun);
let total = CARD_MS + (count.saturating_sub(1) as u64) * STAGGER_MS;
- AnimNum::new(0., 1.).time(total.max(1)).function(Function::Linear)
+ AnimNum::new(0., 1.)
+ .time(total.max(1))
+ .function(Function::Linear)
});
let progress = intro.get().value();
@@ -112,11 +114,7 @@ impl Component for ClusterCard {
let eased = 1.0 - (1.0 - local).powi(3);
let rise = (1.0 - eased) * CARD_RISE_PX;
- let title = format!(
- "{} {}",
- self.cluster.mc_version,
- self.cluster.mc_loader
- );
+ let title = format!("{} {}", self.cluster.mc_version, self.cluster.mc_loader);
let cluster_id = self.cluster.id;
let on_press = move |_| {
diff --git a/packages/oneclient_app/src/components/screenshot_viewer.rs b/packages/oneclient_app/src/components/screenshot_viewer.rs
index 1eb5a31..cdb6539 100644
--- a/packages/oneclient_app/src/components/screenshot_viewer.rs
+++ b/packages/oneclient_app/src/components/screenshot_viewer.rs
@@ -102,11 +102,15 @@ impl Component for ScreenshotViewer {
.height(Size::fill())
.child(preview),
)
- .child(chevron_btn(IconType::ArrowRight, has_next, move |_| {
- if idx + 1 < len {
- index.set(idx + 1);
- }
- })),
+ .child(chevron_btn(
+ IconType::ArrowRight,
+ has_next,
+ move |_| {
+ if idx + 1 < len {
+ index.set(idx + 1);
+ }
+ },
+ )),
)
.child(
rect()
@@ -119,7 +123,9 @@ impl Component for ScreenshotViewer {
.secondary()
.on_press(move |_| {
if let Some(dir) = open_path.parent() {
- crate::platform::open_url(&dir.to_string_lossy());
+ crate::platform::open_url(
+ &dir.to_string_lossy(),
+ );
}
})
.child(Icon::new(IconType::Folder).size(14.))
@@ -198,8 +204,12 @@ fn header_row(
len: usize,
on_close: impl Into<EventHandler<Event<PressEventData>>>,
) -> impl IntoElement {
- let mut meta = format!("{} • {}", format_size(info.size_bytes), fmt_date(info.created));
-
+ let mut meta = format!(
+ "{} • {}",
+ format_size(info.size_bytes),
+ fmt_date(info.created)
+ );
+
if let Some(res) = info.resolution {
meta = format!("{} • {}", format_res(res), meta);
}
diff --git a/packages/oneclient_app/src/components/scrollview.rs b/packages/oneclient_app/src/components/scrollview.rs
index ba49b6c..471e973 100644
--- a/packages/oneclient_app/src/components/scrollview.rs
+++ b/packages/oneclient_app/src/components/scrollview.rs
@@ -314,7 +314,11 @@ impl ScrollArea {
let shift = *shift_held.read();
let h_delta = if horizontal {
- if shift && e.delta_x == 0.0 { e.delta_y } else { e.delta_x }
+ if shift && e.delta_x == 0.0 {
+ e.delta_y
+ } else {
+ e.delta_x
+ }
} else {
0.0
};
diff --git a/packages/oneclient_app/src/components/tab_bar.rs b/packages/oneclient_app/src/components/tab_bar.rs
index f79d340..1aa5d32 100644
--- a/packages/oneclient_app/src/components/tab_bar.rs
+++ b/packages/oneclient_app/src/components/tab_bar.rs
@@ -19,7 +19,7 @@ impl TabItem {
}
}
- #[allow(dead_code)]
+ #[allow(dead_code)]
pub fn count_text(mut self, text: impl Into<String>) -> Self {
self.count = Some(text.into());
self
diff --git a/packages/oneclient_app/src/components/toasts.rs b/packages/oneclient_app/src/components/toasts.rs
index 2ea7c95..0976248 100644
--- a/packages/oneclient_app/src/components/toasts.rs
+++ b/packages/oneclient_app/src/components/toasts.rs
@@ -32,7 +32,6 @@ impl Variant {
Self::Message
}
}
-
}
fn entry_icon(entry: &InboxEntry) -> (IconType, Color) {
diff --git a/packages/oneclient_app/src/components/update_prompt.rs b/packages/oneclient_app/src/components/update_prompt.rs
index 2c3071f..3bb1a5b 100644
--- a/packages/oneclient_app/src/components/update_prompt.rs
+++ b/packages/oneclient_app/src/components/update_prompt.rs
@@ -19,7 +19,7 @@ impl Component for UpdatePromptOverlay {
let Some(prompt) = snapshot.pending_prompt.clone() else {
return rect().into_element();
};
-
+
if prompt.kind != PromptKind::Update {
return rect().into_element();
}
diff --git a/packages/oneclient_app/src/constants.rs b/packages/oneclient_app/src/constants.rs
index 4b2b07e..23f6ae5 100644
--- a/packages/oneclient_app/src/constants.rs
+++ b/packages/oneclient_app/src/constants.rs
@@ -1,2 +1,2 @@
pub const APP_ID: &str = "org.polyfrost.OneClient";
-pub const WINDOW_TITLE: &str = "OneClient";
\ No newline at end of file
+pub const WINDOW_TITLE: &str = "OneClient";
diff --git a/packages/oneclient_app/src/hooks/dispatch.rs b/packages/oneclient_app/src/hooks/dispatch.rs
index 488dd8b..0b042bf 100644
--- a/packages/oneclient_app/src/hooks/dispatch.rs
+++ b/packages/oneclient_app/src/hooks/dispatch.rs
@@ -42,6 +42,13 @@ impl BridgeDispatch {
self.send(BridgeCommand::MarkOnboardingSeen);
}
+ pub fn accept_tos(&self, terms_version: u32, privacy_version: u32) {
+ self.send(BridgeCommand::AcceptTos {
+ terms_version,
+ privacy_version,
+ });
+ }
+
pub fn import_launcher(
&self,
source: oneclient_core::MigrationSource,
@@ -64,9 +71,7 @@ impl BridgeDispatch {
}
pub fn create_settings_profile(&self, name: impl Into<String>) {
- self.send(BridgeCommand::CreateSettingsProfile {
- name: name.into(),
- });
+ self.send(BridgeCommand::CreateSettingsProfile { name: name.into() });
}
pub fn create_profile_from_global(
@@ -94,20 +99,11 @@ impl BridgeDispatch {
}
pub fn delete_named_profile(&self, name: impl Into<String>) {
- self.send(BridgeCommand::DeleteNamedProfile {
- name: name.into(),
- });
+ self.send(BridgeCommand::DeleteNamedProfile { name: name.into() });
}
- pub fn update_cluster_profile(
- &self,
- cluster_id: ClusterId,
- update: ProfileUpdate,
- ) {
- self.send(BridgeCommand::UpdateClusterProfile {
- cluster_id,
- update,
- });
+ pub fn update_cluster_profile(&self, cluster_id: ClusterId, update: ProfileUpdate) {
+ self.send(BridgeCommand::UpdateClusterProfile { cluster_id, update });
}
pub fn create_and_assign_cluster_profile(
@@ -121,11 +117,7 @@ impl BridgeDispatch {
});
}
- pub fn set_cluster_loader_version(
- &self,
- cluster_id: ClusterId,
- version: impl Into<String>,
- ) {
+ pub fn set_cluster_loader_version(&self, cluster_id: ClusterId, version: impl Into<String>) {
self.send(BridgeCommand::SetClusterLoaderVersion {
cluster_id,
version: version.into(),
@@ -318,6 +310,7 @@ impl NotificationBuilder {
}
pub fn send(self) {
- self.dispatch.send(BridgeCommand::SendNotification { spec: self.spec });
+ self.dispatch
+ .send(BridgeCommand::SendNotification { spec: self.spec });
}
}
diff --git a/packages/oneclient_app/src/hooks/mod.rs b/packages/oneclient_app/src/hooks/mod.rs
index 9062362..87b90ee 100644
--- a/packages/oneclient_app/src/hooks/mod.rs
+++ b/packages/oneclient_app/src/hooks/mod.rs
@@ -19,30 +19,30 @@ pub use queries::{
CancelMicrosoftLoginKeys, CancelMicrosoftLoginMutation, ClusterAction, ClusterBundles,
ClusterLogsQuery, FinishMicrosoftLoginMutation, LogAction, LogContentQuery, MigrationQuery,
OnboardingBundlesQuery, RefreshAccountKeys, RemoveAccountKeys, ScreenshotAction,
- SetDefaultAccountKeys, UploadLogKeys, UploadLogMutation, UseLogAction, UseRefreshAccount,
- UseRemoveAccount, UseScreenshotAction, UseSetDefaultAccount, UseUploadLog, VERSIONS_PAGE_SIZE,
- accounts_have_microsoft, bundle_overrides_map, bundles_with_status_items, category_list,
- changelog_error, changelog_groups, changelog_is_loading, cluster_content_items,
+ SetDefaultAccountKeys, TermsQuery, UploadLogKeys, UploadLogMutation, UseLogAction,
+ UseRefreshAccount, UseRemoveAccount, UseScreenshotAction, UseSetDefaultAccount, UseUploadLog,
+ VERSIONS_PAGE_SIZE, accounts_have_microsoft, bundle_overrides_map, bundles_with_status_items,
+ category_list, changelog_error, changelog_groups, changelog_is_loading, cluster_content_items,
content_type_for_slug, has_migration_data, invalidate_cluster_queries, invalidate_java_queries,
invalidate_logs_queries, invalidate_profile_queries, invalidate_screenshots_queries,
java_runtimes, loader_versions, login_code_already_handled, migration_detection,
mutation_error, mutation_is_pending, onboarding_bundles_items, package_meta_batch,
pick_version_metadata, project_detail, provider_versions, reset_login_code_dedup, search_items,
- search_pending, search_total, try_account, try_accounts, try_cluster_analytics,
- try_cluster_logs, try_cluster_screenshots, try_default_account, try_game_profile,
- try_global_analytics, try_log_content, use_account, use_accounts, use_add_microsoft_account,
- use_add_offline_account, use_begin_microsoft_login, use_bundle_overrides, use_bundle_updates,
- use_bundles_with_status, use_cached_image, use_cancel_microsoft_login, use_changelog,
- use_cluster_analytics, use_cluster_content, use_cluster_logs, use_cluster_mutation,
- use_cluster_profile, use_cluster_screenshots, use_cluster_settings, use_clusters,
- use_current_account, use_default_account, use_finish_microsoft_login, use_game_profile,
- use_global_analytics, use_java_runtimes, use_loader_versions, use_local_image, use_log_action,
- use_log_content, use_migration, use_named_profiles, use_onboarding_bundles,
- use_package_categories, use_package_meta_batch, use_package_project, use_package_search,
- use_package_versions, use_player_profile, use_player_skin, use_provider_versions,
- use_refresh_account, use_refresh_all_accounts, use_remove_account, use_screenshot_action,
- use_set_default_account, use_upload_log, use_version_metadata, use_versions, version_list,
- versions_total,
+ search_pending, search_total, terms_document, terms_error, terms_is_loading, try_account,
+ try_accounts, try_cluster_analytics, try_cluster_logs, try_cluster_screenshots,
+ try_default_account, try_game_profile, try_global_analytics, try_log_content, use_account,
+ use_accounts, use_add_microsoft_account, use_add_offline_account, use_begin_microsoft_login,
+ use_bundle_overrides, use_bundle_updates, use_bundles_with_status, use_cached_image,
+ use_cancel_microsoft_login, use_changelog, use_cluster_analytics, use_cluster_content,
+ use_cluster_logs, use_cluster_mutation, use_cluster_profile, use_cluster_screenshots,
+ use_cluster_settings, use_clusters, use_current_account, use_default_account,
+ use_finish_microsoft_login, use_game_profile, use_global_analytics, use_java_runtimes,
+ use_loader_versions, use_local_image, use_log_action, use_log_content, use_migration,
+ use_named_profiles, use_onboarding_bundles, use_package_categories, use_package_meta_batch,
+ use_package_project, use_package_search, use_package_versions, use_player_profile,
+ use_player_skin, use_provider_versions, use_refresh_account, use_refresh_all_accounts,
+ use_remove_account, use_screenshot_action, use_set_default_account, use_terms, use_upload_log,
+ use_version_metadata, use_versions, version_list, versions_total,
};
use crate::{
diff --git a/packages/oneclient_app/src/hooks/queries/analytics.rs b/packages/oneclient_app/src/hooks/queries/analytics.rs
index 1d433fa..9181aa5 100644
--- a/packages/oneclient_app/src/hooks/queries/analytics.rs
+++ b/packages/oneclient_app/src/hooks/queries/analytics.rs
@@ -2,7 +2,6 @@ use freya::query::{Query, QueryCapability, QueryStateData, UseQuery, use_query};
use oneclient_core::LauncherError;
use oneclient_core::game::{Analytics, cluster_analytics, global_analytics};
-
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct GlobalAnalyticsKeys;
@@ -33,7 +32,6 @@ pub fn try_global_analytics(query: &UseQuery<GlobalAnalyticsQuery>) -> Option<An
}
}
-
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ClusterAnalyticsKeys {
pub cluster_id: i64,
diff --git a/packages/oneclient_app/src/hooks/queries/auth.rs b/packages/oneclient_app/src/hooks/queries/auth.rs
index d777507..a00bfa5 100644
--- a/packages/oneclient_app/src/hooks/queries/auth.rs
+++ b/packages/oneclient_app/src/hooks/queries/auth.rs
@@ -1,4 +1,3 @@
-
use std::sync::Mutex;
use freya::query::{
diff --git a/packages/oneclient_app/src/hooks/queries/bundles.rs b/packages/oneclient_app/src/hooks/queries/bundles.rs
index cc3f880..b870974 100644
--- a/packages/oneclient_app/src/hooks/queries/bundles.rs
+++ b/packages/oneclient_app/src/hooks/queries/bundles.rs
@@ -52,7 +52,9 @@ pub fn onboarding_bundles_items(
let reader = query.read();
match &*reader.state() {
QueryStateData::Settled { res: Ok(list), .. } => Some(list.clone()),
- QueryStateData::Loading { res: Some(Ok(list)) } => Some(list.clone()),
+ QueryStateData::Loading {
+ res: Some(Ok(list)),
+ } => Some(list.clone()),
_ => None,
}
}
@@ -75,12 +77,8 @@ impl QueryCapability for BundlesWithStatusQuery {
async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
let _ = keys;
let state = LauncherState::get()?;
- get_bundles_with_update_status(
- self.cluster_id,
- state.bundles.as_ref(),
- &state.services,
- )
- .await
+ get_bundles_with_update_status(self.cluster_id, state.bundles.as_ref(), &state.services)
+ .await
}
}
@@ -90,7 +88,9 @@ pub fn bundles_with_status_items(
let reader = query.read();
match &*reader.state() {
QueryStateData::Settled { res: Ok(list), .. } => list.clone(),
- QueryStateData::Loading { res: Some(Ok(list)) } => list.clone(),
+ QueryStateData::Loading {
+ res: Some(Ok(list)),
+ } => list.clone(),
_ => Vec::new(),
}
}
diff --git a/packages/oneclient_app/src/hooks/queries/changelog.rs b/packages/oneclient_app/src/hooks/queries/changelog.rs
index 04b3f89..c9ad5da 100644
--- a/packages/oneclient_app/src/hooks/queries/changelog.rs
+++ b/packages/oneclient_app/src/hooks/queries/changelog.rs
@@ -1,5 +1,7 @@
use freya::query::{Query, QueryCapability, QueryStateData, UseQuery, use_query};
-use oneclient_core::{fetch_changelog, parse_changelog, ChangelogGroup, LauncherError, LauncherState};
+use oneclient_core::{
+ ChangelogGroup, LauncherError, LauncherState, fetch_changelog, parse_changelog,
+};
use crate::hooks::use_settings_snapshot;
@@ -32,15 +34,14 @@ pub fn use_changelog() -> UseQuery<ChangelogQuery> {
.trim()
.to_string();
- use_query(Query::new(
- ChangelogKeys { meta_url_base },
- ChangelogQuery,
- ))
+ use_query(Query::new(ChangelogKeys { meta_url_base }, ChangelogQuery))
}
pub fn changelog_groups(query: &UseQuery<ChangelogQuery>) -> Option<Vec<ChangelogGroup>> {
match &*query.read().state() {
- QueryStateData::Settled { res: Ok(groups), .. } => Some(groups.clone()),
+ QueryStateData::Settled {
+ res: Ok(groups), ..
+ } => Some(groups.clone()),
QueryStateData::Loading {
res: Some(Ok(groups)),
} => Some(groups.clone()),
diff --git a/packages/oneclient_app/src/hooks/queries/cluster_content.rs b/packages/oneclient_app/src/hooks/queries/cluster_content.rs
index 4e72732..2159fcb 100644
--- a/packages/oneclient_app/src/hooks/queries/cluster_content.rs
+++ b/packages/oneclient_app/src/hooks/queries/cluster_content.rs
@@ -1,4 +1,3 @@
-
use freya::query::{Query, QueryCapability, QueryStateData, UseQuery, use_query};
use oneclient_core::packages::{ContentType, PackageStore};
use oneclient_core::{LauncherError, LauncherState, LinkedArtifactInfo};
@@ -51,7 +50,9 @@ pub fn cluster_content_items(query: &UseQuery<ClusterContentQuery>) -> Vec<Linke
let reader = query.read();
match &*reader.state() {
QueryStateData::Settled { res: Ok(list), .. } => list.clone(),
- QueryStateData::Loading { res: Some(Ok(list)) } => list.clone(),
+ QueryStateData::Loading {
+ res: Some(Ok(list)),
+ } => list.clone(),
_ => Vec::new(),
}
}
diff --git a/packages/oneclient_app/src/hooks/queries/clusters.rs b/packages/oneclient_app/src/hooks/queries/clusters.rs
index 0f09711..61f487f 100644
--- a/packages/oneclient_app/src/hooks/queries/clusters.rs
+++ b/packages/oneclient_app/src/hooks/queries/clusters.rs
@@ -1,7 +1,7 @@
use freya::query::{Query, QueryCapability, UseQuery, use_query};
-use oneclient_core::clusters::{Cluster, ClusterManager};
use oneclient_core::LauncherError;
use oneclient_core::LauncherState;
+use oneclient_core::clusters::{Cluster, ClusterManager};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ListClustersQuery;
@@ -23,5 +23,3 @@ impl QueryCapability for ListClustersQuery {
pub fn use_clusters() -> UseQuery<ListClustersQuery> {
use_query(Query::new(ListClustersKeys, ListClustersQuery))
}
-
-
diff --git a/packages/oneclient_app/src/hooks/queries/java.rs b/packages/oneclient_app/src/hooks/queries/java.rs
index e2c7b7e..df34a5c 100644
--- a/packages/oneclient_app/src/hooks/queries/java.rs
+++ b/packages/oneclient_app/src/hooks/queries/java.rs
@@ -1,6 +1,4 @@
-use freya::query::{
- QueriesStorage, Query, QueryCapability, QueryStateData, UseQuery, use_query,
-};
+use freya::query::{QueriesStorage, Query, QueryCapability, QueryStateData, UseQuery, use_query};
use oneclient_core::LauncherState;
use oneclient_core::java::{AvailableJava, JavaManager, JavaRuntime, JavaVendor};
@@ -31,7 +29,9 @@ pub fn java_runtimes(query: &UseQuery<ListJavaRuntimesQuery>) -> Vec<JavaRuntime
let reader = query.read();
match &*reader.state() {
QueryStateData::Settled { res: Ok(list), .. } => list.clone(),
- QueryStateData::Loading { res: Some(Ok(list)) } => list.clone(),
+ QueryStateData::Loading {
+ res: Some(Ok(list)),
+ } => list.clone(),
_ => Vec::new(),
}
}
@@ -68,14 +68,14 @@ pub fn use_provider_versions(vendor: JavaVendor) -> UseQuery<ProviderVersionsQue
))
}
-pub fn provider_versions(
- query: &UseQuery<ProviderVersionsQuery>,
-) -> (Vec<AvailableJava>, bool) {
+pub fn provider_versions(query: &UseQuery<ProviderVersionsQuery>) -> (Vec<AvailableJava>, bool) {
let reader = query.read();
match &*reader.state() {
QueryStateData::Settled { res: Ok(list), .. } => (list.clone(), false),
QueryStateData::Settled { res: Err(_), .. } => (Vec::new(), false),
- QueryStateData::Loading { res: Some(Ok(list)) } => (list.clone(), true),
+ QueryStateData::Loading {
+ res: Some(Ok(list)),
+ } => (list.clone(), true),
_ => (Vec::new(), true),
}
}
diff --git a/packages/oneclient_app/src/hooks/queries/logs.rs b/packages/oneclient_app/src/hooks/queries/logs.rs
index eb1ba9f..4d51fdb 100644
--- a/packages/oneclient_app/src/hooks/queries/logs.rs
+++ b/packages/oneclient_app/src/hooks/queries/logs.rs
@@ -89,7 +89,9 @@ where
{
match &*query.read().state() {
QueryStateData::Settled { res: Ok(value), .. } => Some(value.clone()),
- QueryStateData::Loading { res: Some(Ok(value)) } => Some(value.clone()),
+ QueryStateData::Loading {
+ res: Some(Ok(value)),
+ } => Some(value.clone()),
_ => None,
}
}
diff --git a/packages/oneclient_app/src/hooks/queries/mod.rs b/packages/oneclient_app/src/hooks/queries/mod.rs
index ac8cd27..84da2cc 100644
--- a/packages/oneclient_app/src/hooks/queries/mod.rs
+++ b/packages/oneclient_app/src/hooks/queries/mod.rs
@@ -14,6 +14,7 @@ mod player_profile;
mod screenshots;
mod settings_profiles;
mod skin;
+mod tos;
mod version_metadata;
mod versions;
@@ -69,5 +70,6 @@ pub use settings_profiles::{
use_named_profiles,
};
pub use skin::use_player_skin;
+pub use tos::{TermsQuery, terms_document, terms_error, terms_is_loading, use_terms};
pub use version_metadata::{pick_version_metadata, use_version_metadata};
pub use versions::{loader_versions, use_loader_versions, use_versions};
diff --git a/packages/oneclient_app/src/hooks/queries/mutations.rs b/packages/oneclient_app/src/hooks/queries/mutations.rs
index b2f05da..54d59e0 100644
--- a/packages/oneclient_app/src/hooks/queries/mutations.rs
+++ b/packages/oneclient_app/src/hooks/queries/mutations.rs
@@ -1,4 +1,3 @@
-
use std::path::PathBuf;
use freya::query::{Mutation, MutationCapability, QueriesStorage, UseMutation, use_mutation};
@@ -37,9 +36,18 @@ pub struct ClusterMutation;
#[derive(Clone, PartialEq, Eq, Hash)]
pub enum ClusterAction {
- ToggleArtifact { cluster_id: ClusterId, hash: String },
- RemoveArtifact { cluster_id: ClusterId, hash: String },
- RemoveBundlePackageFromDisk { cluster_id: ClusterId, hash: String },
+ ToggleArtifact {
+ cluster_id: ClusterId,
+ hash: String,
+ },
+ RemoveArtifact {
+ cluster_id: ClusterId,
+ hash: String,
+ },
+ RemoveBundlePackageFromDisk {
+ cluster_id: ClusterId,
+ hash: String,
+ },
SetBundlePackageEnabled {
cluster_id: ClusterId,
bundle_name: String,
@@ -106,10 +114,14 @@ impl MutationCapability for ClusterMutation {
ClusterAction::SetDedicatedDir {
cluster_id,
dedicated,
- } => oneclient_core::clusters::ClusterManager::set_dedicated_dir(
- &state, *cluster_id, *dedicated,
- )
- .await,
+ } => {
+ oneclient_core::clusters::ClusterManager::set_dedicated_dir(
+ &state,
+ *cluster_id,
+ *dedicated,
+ )
+ .await
+ }
};
result.map_err(|e| e.to_string())
}
diff --git a/packages/oneclient_app/src/hooks/queries/packages.rs b/packages/oneclient_app/src/hooks/queries/packages.rs
index 85c427a..1ca3c6b 100644
--- a/packages/oneclient_app/src/hooks/queries/packages.rs
+++ b/packages/oneclient_app/src/hooks/queries/packages.rs
@@ -1,4 +1,3 @@
-
use freya::query::{Query, QueryCapability, QueryStateData, UseQuery, use_query};
use oneclient_core::packages::domain::GameLoader;
use oneclient_core::packages::types::{
@@ -48,7 +47,8 @@ impl QueryCapability for PackageSearchQuery {
&SearchFilters {
query: (!keys.query.trim().is_empty()).then(|| keys.query.trim().to_string()),
content_type: Some(keys.content_type),
- game_versions: (!keys.game_versions.is_empty()).then(|| keys.game_versions.clone()),
+ game_versions: (!keys.game_versions.is_empty())
+ .then(|| keys.game_versions.clone()),
loaders: (!keys.loaders.is_empty()).then(|| keys.loaders.clone()),
categories: (!keys.categories.is_empty()).then(|| keys.categories.clone()),
sort: Some(keys.sort),
@@ -91,7 +91,9 @@ pub fn search_items(query: &UseQuery<PackageSearchQuery>) -> Vec<ProjectSummary>
let reader = query.read();
match &*reader.state() {
QueryStateData::Settled { res: Ok(page), .. } => page.items.clone(),
- QueryStateData::Loading { res: Some(Ok(page)) } => page.items.clone(),
+ QueryStateData::Loading {
+ res: Some(Ok(page)),
+ } => page.items.clone(),
_ => Vec::new(),
}
}
@@ -100,13 +102,18 @@ pub fn search_total(query: &UseQuery<PackageSearchQuery>) -> usize {
let reader = query.read();
match &*reader.state() {
QueryStateData::Settled { res: Ok(page), .. } => page.total,
- QueryStateData::Loading { res: Some(Ok(page)) } => page.total,
+ QueryStateData::Loading {
+ res: Some(Ok(page)),
+ } => page.total,
_ => 0,
}
}
pub fn search_pending(query: &UseQuery<PackageSearchQuery>) -> bool {
- matches!(&*query.read().state(), QueryStateData::Pending | QueryStateData::Loading { .. })
+ matches!(
+ &*query.read().state(),
+ QueryStateData::Pending | QueryStateData::Loading { .. }
+ )
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
@@ -126,7 +133,9 @@ impl QueryCapability for PackageProjectQuery {
async fn run(&self, keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
let state = LauncherState::get()?;
let provider = state.services.packages.get(keys.provider)?;
- provider.get_project(&keys.project_id, &state.services).await
+ provider
+ .get_project(&keys.project_id, &state.services)
+ .await
}
}
@@ -146,8 +155,12 @@ pub fn use_package_project(
pub fn project_detail(query: &UseQuery<PackageProjectQuery>) -> Option<ProjectDetail> {
let reader = query.read();
match &*reader.state() {
- QueryStateData::Settled { res: Ok(detail), .. } => Some(detail.clone()),
- QueryStateData::Loading { res: Some(Ok(detail)) } => Some(detail.clone()),
+ QueryStateData::Settled {
+ res: Ok(detail), ..
+ } => Some(detail.clone()),
+ QueryStateData::Loading {
+ res: Some(Ok(detail)),
+ } => Some(detail.clone()),
_ => None,
}
}
@@ -260,7 +273,9 @@ pub fn version_list(query: &UseQuery<PackageVersionsQuery>) -> Vec<VersionSummar
let reader = query.read();
match &*reader.state() {
QueryStateData::Settled { res: Ok(page), .. } => page.items.clone(),
- QueryStateData::Loading { res: Some(Ok(page)) } => page.items.clone(),
+ QueryStateData::Loading {
+ res: Some(Ok(page)),
+ } => page.items.clone(),
_ => Vec::new(),
}
}
@@ -269,7 +284,9 @@ pub fn versions_total(query: &UseQuery<PackageVersionsQuery>) -> usize {
let reader = query.read();
match &*reader.state() {
QueryStateData::Settled { res: Ok(page), .. } => page.total,
- QueryStateData::Loading { res: Some(Ok(page)) } => page.total,
+ QueryStateData::Loading {
+ res: Some(Ok(page)),
+ } => page.total,
_ => 0,
}
}
@@ -315,7 +332,9 @@ pub fn category_list(query: &UseQuery<PackageCategoriesQuery>) -> Vec<String> {
let reader = query.read();
match &*reader.state() {
QueryStateData::Settled { res: Ok(list), .. } => list.clone(),
- QueryStateData::Loading { res: Some(Ok(list)) } => list.clone(),
+ QueryStateData::Loading {
+ res: Some(Ok(list)),
+ } => list.clone(),
_ => Vec::new(),
}
}
diff --git a/packages/oneclient_app/src/hooks/queries/player_profile.rs b/packages/oneclient_app/src/hooks/queries/player_profile.rs
index 5bb3d3c..df42b8b 100644
--- a/packages/oneclient_app/src/hooks/queries/player_profile.rs
+++ b/packages/oneclient_app/src/hooks/queries/player_profile.rs
@@ -39,7 +39,10 @@ impl QueryCapability for FetchPlayerProfileQuery {
// }
}
-pub fn use_player_profile(uuid: impl Into<String>, access_token: Option<impl Into<String>>) -> UseQuery<FetchPlayerProfileQuery> {
+pub fn use_player_profile(
+ uuid: impl Into<String>,
+ access_token: Option<impl Into<String>>,
+) -> UseQuery<FetchPlayerProfileQuery> {
use_query(
Query::new(
PlayerProfileQueryKeys {
diff --git a/packages/oneclient_app/src/hooks/queries/screenshots.rs b/packages/oneclient_app/src/hooks/queries/screenshots.rs
index 3b01dd9..214aa56 100644
--- a/packages/oneclient_app/src/hooks/queries/screenshots.rs
+++ b/packages/oneclient_app/src/hooks/queries/screenshots.rs
@@ -12,7 +12,6 @@ use tokio::sync::Semaphore;
static LOCAL_IMAGE_SEMAPHORE: OnceLock<Arc<Semaphore>> = OnceLock::new();
-
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ClusterScreenshotsKeys {
pub cluster_id: i64,
@@ -96,7 +95,6 @@ pub fn try_cluster_screenshots(
}
}
-
pub async fn invalidate_screenshots_queries() {
QueriesStorage::<ClusterScreenshotsQuery>::try_invalidate_all().await;
QueriesStorage::<LocalImageQuery>::try_invalidate_all().await;
diff --git a/packages/oneclient_app/src/hooks/queries/skin.rs b/packages/oneclient_app/src/hooks/queries/skin.rs
index 6d3b1a8..f0d5bdf 100644
--- a/packages/oneclient_app/src/hooks/queries/skin.rs
+++ b/packages/oneclient_app/src/hooks/queries/skin.rs
@@ -42,4 +42,4 @@ fn java_string_hash(s: &str) -> i32 {
h = h.wrapping_mul(31).wrapping_add(c as i32);
}
h
-}
\ No newline at end of file
+}
diff --git a/packages/oneclient_app/src/hooks/queries/tos.rs b/packages/oneclient_app/src/hooks/queries/tos.rs
new file mode 100644
index 0000000..bec73ab
--- /dev/null
+++ b/packages/oneclient_app/src/hooks/queries/tos.rs
@@ -0,0 +1,61 @@
+use freya::query::{Query, QueryCapability, QueryStateData, UseQuery, use_query};
+use oneclient_core::{LauncherError, LauncherState, TermsDocument, fetch_terms};
+
+use crate::hooks::use_settings_snapshot;
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub struct TermsKeys {
+ pub meta_url_base: String,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub struct TermsQuery;
+
+impl QueryCapability for TermsQuery {
+ type Ok = TermsDocument;
+ type Err = LauncherError;
+ type Keys = TermsKeys;
+
+ async fn run(&self, _keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
+ let state = LauncherState::get()?;
+ fetch_terms(&state.services).await
+ }
+}
+
+pub fn use_terms() -> UseQuery<TermsQuery> {
+ let settings = use_settings_snapshot().settings;
+ let meta_url_base = settings
+ .custom_meta_url_base
+ .clone()
+ .unwrap_or_default()
+ .trim()
+ .to_string();
+
+ use_query(Query::new(TermsKeys { meta_url_base }, TermsQuery))
+}
+
+pub fn terms_document(query: &UseQuery<TermsQuery>) -> Option<TermsDocument> {
+ match &*query.read().state() {
+ QueryStateData::Settled {
+ res: Ok(document), ..
+ } => Some(document.clone()),
+ QueryStateData::Loading {
+ res: Some(Ok(document)),
+ } => Some(document.clone()),
+ _ => None,
+ }
+}
+
+pub fn terms_error(query: &UseQuery<TermsQuery>) -> Option<String> {
+ match &*query.read().state() {
+ QueryStateData::Settled { res: Err(err), .. } => Some(err.to_string()),
+ _ => None,
+ }
+}
+
+pub fn terms_is_loading(query: &UseQuery<TermsQuery>) -> bool {
+ matches!(
+ &*query.read().state(),
+ QueryStateData::Pending | QueryStateData::Loading { res: None }
+ )
+}
diff --git a/packages/oneclient_app/src/hooks/queries/version_metadata.rs b/packages/oneclient_app/src/hooks/queries/version_metadata.rs
index c589718..f30f513 100644
--- a/packages/oneclient_app/src/hooks/queries/version_metadata.rs
+++ b/packages/oneclient_app/src/hooks/queries/version_metadata.rs
@@ -1,6 +1,6 @@
use freya::query::QueryStateData;
-use oneclient_core::packages::domain::GameLoader;
use oneclient_core::VersionMetadata;
+use oneclient_core::packages::domain::GameLoader;
use super::use_versions;
@@ -46,7 +46,9 @@ pub fn use_version_metadata(
let state = reader.state();
let list = match &*state {
QueryStateData::Settled { res: Ok(list), .. } => list,
- QueryStateData::Loading { res: Some(Ok(list)) } => list,
+ QueryStateData::Loading {
+ res: Some(Ok(list)),
+ } => list,
_ => return None,
};
diff --git a/packages/oneclient_app/src/hooks/queries/versions.rs b/packages/oneclient_app/src/hooks/queries/versions.rs
index 8f4d053..b48c48f 100644
--- a/packages/oneclient_app/src/hooks/queries/versions.rs
+++ b/packages/oneclient_app/src/hooks/queries/versions.rs
@@ -69,7 +69,9 @@ pub fn loader_versions(query: &UseQuery<LoaderVersionsQuery>) -> Vec<String> {
let reader = query.read();
match &*reader.state() {
QueryStateData::Settled { res: Ok(list), .. } => list.clone(),
- QueryStateData::Loading { res: Some(Ok(list)) } => list.clone(),
+ QueryStateData::Loading {
+ res: Some(Ok(list)),
+ } => list.clone(),
_ => Vec::new(),
}
}
diff --git a/packages/oneclient_app/src/hooks/view_state.rs b/packages/oneclient_app/src/hooks/view_state.rs
index 0d8ec34..64696a3 100644
--- a/packages/oneclient_app/src/hooks/view_state.rs
+++ b/packages/oneclient_app/src/hooks/view_state.rs
@@ -5,38 +5,38 @@ use super::{use_dispatch, use_settings_snapshot};
#[derive(Clone, Copy)]
pub struct PersistedView {
- pub layout: State<ViewLayout>,
- pub sort: State<Option<String>>,
+ pub layout: State<ViewLayout>,
+ pub sort: State<Option<String>>,
}
pub fn use_view_state(key: &str) -> PersistedView {
- let key = key.to_string();
- let settings = use_settings_snapshot().settings;
- let dispatch = use_dispatch();
+ let key = key.to_string();
+ let settings = use_settings_snapshot().settings;
+ let dispatch = use_dispatch();
- let initial = settings.view_state(&key);
- let init_layout = initial.layout;
- let init_sort = initial.sort;
- let layout = use_state(move || init_layout);
- let sort = use_state(move || init_sort.clone());
+ let initial = settings.view_state(&key);
+ let init_layout = initial.layout;
+ let init_sort = initial.sort;
+ let layout = use_state(move || init_layout);
+ let sort = use_state(move || init_sort.clone());
- let mut first = use_state(|| true);
- use_side_effect(move || {
- let next = ViewState {
- layout: *layout.read(),
- sort: sort.read().clone(),
- };
- if *first.peek() {
- first.set(false);
- return;
- }
- if settings.view_state(&key) == next {
- return;
- }
- let mut updated = settings.clone();
- updated.set_view_state(key.clone(), next);
- dispatch.set_settings(updated);
- });
+ let mut first = use_state(|| true);
+ use_side_effect(move || {
+ let next = ViewState {
+ layout: *layout.read(),
+ sort: sort.read().clone(),
+ };
+ if *first.peek() {
+ first.set(false);
+ return;
+ }
+ if settings.view_state(&key) == next {
+ return;
+ }
+ let mut updated = settings.clone();
+ updated.set_view_state(key.clone(), next);
+ dispatch.set_settings(updated);
+ });
- PersistedView { layout, sort }
+ PersistedView { layout, sort }
}
diff --git a/packages/oneclient_app/src/layout/app_shell.rs b/packages/oneclient_app/src/layout/app_shell.rs
index e727b02..4b898b0 100644
--- a/packages/oneclient_app/src/layout/app_shell.rs
+++ b/packages/oneclient_app/src/layout/app_shell.rs
@@ -9,8 +9,8 @@ use skia_safe::sampling_options::CubicResampler;
use crate::Route;
use crate::components::Button;
-use crate::layout::AnimatedAppOutlet;
use crate::components::{AppNavbar, DynamicArt, Icon, IconType, OverlayPopup, ScrollArea};
+use crate::layout::AnimatedAppOutlet;
use crate::theme;
use crate::use_settings_snapshot;
use oneclient_db::models::ClusterId;
diff --git a/packages/oneclient_app/src/layout/cluster_shell.rs b/packages/oneclient_app/src/layout/cluster_shell.rs
index 49b13df..2572ac0 100644
--- a/packages/oneclient_app/src/layout/cluster_shell.rs
+++ b/packages/oneclient_app/src/layout/cluster_shell.rs
@@ -33,7 +33,7 @@ impl ClusterViewShellTab {
match self {
Self::Overview => "Overview",
Self::Logs => "Logs",
- Self::GameLog => "Game Log",
+ Self::GameLog => "Live Log",
Self::Screenshots => "Screenshots",
Self::Mods => "Mods",
Self::Shaders => "Shaders",
diff --git a/packages/oneclient_app/src/layout/mod.rs b/packages/oneclient_app/src/layout/mod.rs
index bab99f5..9342e22 100644
--- a/packages/oneclient_app/src/layout/mod.rs
+++ b/packages/oneclient_app/src/layout/mod.rs
@@ -6,8 +6,8 @@ mod root_layout;
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 app_shell::{AppShell, HOME_BACKGROUND_ASSET};
pub use cluster_shell::{ClusterShell, cluster_content};
pub use onboarding_shell::OnboardingShell;
pub use root_layout::RootLayout;
diff --git a/packages/oneclient_app/src/layout/settings_shell.rs b/packages/oneclient_app/src/layout/settings_shell.rs
index 773be6c..8fe9d55 100644
--- a/packages/oneclient_app/src/layout/settings_shell.rs
+++ b/packages/oneclient_app/src/layout/settings_shell.rs
@@ -198,7 +198,10 @@ const SEARCH_INDEX: &[SearchItem] = &[
icon: IconType::Globe01,
title: "Language",
description: "Choose the launcher language.",
- keywords: &["language", "locale", "english", "spanish", "french", "german", "russian", "japanese", "chinese"],
+ keywords: &[
+ "language", "locale", "english", "spanish", "french", "german", "russian", "japanese",
+ "chinese",
+ ],
route: Route::SettingsLanguage {},
},
// Developer
@@ -332,10 +335,7 @@ const GROUPS: &[SettingsGroup] = &[
},
SettingsGroup {
label: "ABOUT",
- tabs: &[
- SettingsTab::DeveloperOptions,
- SettingsTab::Changelog,
- ],
+ tabs: &[SettingsTab::DeveloperOptions, SettingsTab::Changelog],
},
];
@@ -490,18 +490,20 @@ fn search_results(query: String, mut search: State<String>) -> Vec<Element> {
matches.truncate(SEARCH_RESULTS_MAX);
if matches.is_empty() {
- return vec![rect()
- .width(Size::fill())
- .padding(Gaps::new_symmetric(10., 14.))
- .corner_radius(CornerRadius::new_all(12.))
- .background(colors::page_elevated())
- .child(
- label()
- .text("No settings matched your search.")
- .font_size(12.)
- .color(colors::fg_secondary()),
- )
- .into_element()];
+ return vec![
+ rect()
+ .width(Size::fill())
+ .padding(Gaps::new_symmetric(10., 14.))
+ .corner_radius(CornerRadius::new_all(12.))
+ .background(colors::page_elevated())
+ .child(
+ label()
+ .text("No settings matched your search.")
+ .font_size(12.)
+ .color(colors::fg_secondary()),
+ )
+ .into_element(),
+ ];
}
let mut out: Vec<Element> = Vec::with_capacity(matches.len() + 1);
@@ -696,11 +698,11 @@ impl Component for SidebarInfo {
.spacing(4.)
.font_size(12.)
.color(colors::fg_secondary())
- .children(items.into_iter().map(|item| {
- label()
- .text(item)
- .into_element()
- }))
+ .children(
+ items
+ .into_iter()
+ .map(|item| label().text(item).into_element()),
+ )
.on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
.on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
.on_press(copy_to_clipboard)
diff --git a/packages/oneclient_app/src/lib.rs b/packages/oneclient_app/src/lib.rs
index 673e9f7..4f2b6b3 100644
--- a/packages/oneclient_app/src/lib.rs
+++ b/packages/oneclient_app/src/lib.rs
@@ -12,8 +12,8 @@ mod notifications;
pub mod platform;
mod routes;
pub mod theme;
-pub mod updater;
mod ui;
+pub mod updater;
pub(crate) mod utils;
mod view;
diff --git a/packages/oneclient_app/src/main.rs b/packages/oneclient_app/src/main.rs
index 76bad9d..d5e7a08 100644
--- a/packages/oneclient_app/src/main.rs
+++ b/packages/oneclient_app/src/main.rs
@@ -39,12 +39,15 @@ fn main() {
let _tokio_guard = rt.enter();
let settings = rt.block_on(oneclient_core::settings::store::load_settings(None));
-
+
if settings.log_debug {
oneclient_core::logger::init_debug()
} else {
oneclient_core::logger::init()
- }.expect("Failed to initialize logger");
+ }
+ .expect("Failed to initialize logger");
+
+ let _sentry_guard = oneclient_core::reporting::init(settings.crash_reporting);
let (bridge, handle): (OneClientBridge, CoreBridgeHandle) = OneClientBridge::new();
handle.spawn_runtime();
@@ -67,7 +70,7 @@ fn main() {
std::env::var("ONECLIENT_GPU_CACHE_MB")
.ok()
.and_then(|v| v.parse::<usize>().ok())
- .unwrap_or(32)
+ .unwrap_or(32),
)
.with_default_font(theme::DEFAULT_FONT);
diff --git a/packages/oneclient_app/src/routes.rs b/packages/oneclient_app/src/routes.rs
index ba20fc9..09a4db7 100644
--- a/packages/oneclient_app/src/routes.rs
+++ b/packages/oneclient_app/src/routes.rs
@@ -17,13 +17,13 @@ use crate::view::{
ClusterShaders, ClusterTextures, ProcessLogs,
},
settings::{
- SettingsApis, SettingsAppearance, SettingsChangelog, SettingsDeveloper,
- SettingsJava, SettingsLanguage, SettingsLauncher, SettingsMinecraft,
+ SettingsApis, SettingsAppearance, SettingsChangelog, SettingsDeveloper, SettingsJava,
+ SettingsLanguage, SettingsLauncher, SettingsMinecraft,
},
},
onboarding::{
OnboardingAccount, OnboardingBundles, OnboardingDownloading, OnboardingLanguage,
- OnboardingMigration, OnboardingPreferences, OnboardingWelcome,
+ OnboardingMigration, OnboardingPreferences, OnboardingTerms, OnboardingWelcome,
},
};
@@ -37,6 +37,8 @@ pub enum Route {
#[layout(OnboardingShell)]
#[route("/onboarding")]
OnboardingWelcome {},
+ #[route("/onboarding/terms")]
+ OnboardingTerms {},
#[route("/onboarding/migration")]
OnboardingMigration {},
#[route("/onboarding/language")]
@@ -120,6 +122,7 @@ impl Route {
match self {
Route::Startup { .. } => "Startup".to_string(),
Route::OnboardingWelcome { .. } => "Welcome".to_string(),
+ Route::OnboardingTerms { .. } => "Terms & Privacy".to_string(),
Route::OnboardingMigration { .. } => "Migration".to_string(),
Route::OnboardingLanguage { .. } => "Language".to_string(),
Route::OnboardingAccount { .. } => "Account".to_string(),
diff --git a/packages/oneclient_app/src/theme/colors.rs b/packages/oneclient_app/src/theme/colors.rs
index 9dddbfa..0db8798 100644
--- a/packages/oneclient_app/src/theme/colors.rs
+++ b/packages/oneclient_app/src/theme/colors.rs
@@ -149,4 +149,4 @@ pub fn selection_bg() -> Color {
pub fn toast_action() -> Color {
Color::from_rgb(155, 161, 166)
-}
\ No newline at end of file
+}
diff --git a/packages/oneclient_app/src/ui.rs b/packages/oneclient_app/src/ui.rs
index 5900812..33feece 100644
--- a/packages/oneclient_app/src/ui.rs
+++ b/packages/oneclient_app/src/ui.rs
@@ -17,14 +17,12 @@ pub fn border_all(width: f32) -> Border {
}
pub fn border_all_color(width: f32, color: Color) -> Border {
- Border::new()
- .fill(color)
- .width(BorderWidth {
- top: width,
- right: width,
- bottom: width,
- left: width,
- })
+ Border::new().fill(color).width(BorderWidth {
+ top: width,
+ right: width,
+ bottom: width,
+ left: width,
+ })
}
pub fn border_left(width: f32) -> Border {
@@ -79,4 +77,4 @@ pub fn entrance_motion_layer(
.child(child),
)
.into_element()
-}
\ No newline at end of file
+}
diff --git a/packages/oneclient_app/src/updater.rs b/packages/oneclient_app/src/updater.rs
index ffc60e6..69c07c5 100644
--- a/packages/oneclient_app/src/updater.rs
+++ b/packages/oneclient_app/src/updater.rs
@@ -55,10 +55,7 @@ fn check_for_update() -> anyhow::Result<Option<Update>> {
Ok(check_update(current, config)?)
}
-async fn download_and_install(
- update: Update,
- notifier: NotificationService,
-) -> anyhow::Result<()> {
+async fn download_and_install(update: Update, notifier: NotificationService) -> anyhow::Result<()> {
let progress_id = Uuid::new_v4();
let version = update.version.clone();
let label = format!("Downloading OneClient {version}");
diff --git a/packages/oneclient_app/src/utils.rs b/packages/oneclient_app/src/utils.rs
index daab86b..ab046e6 100644
--- a/packages/oneclient_app/src/utils.rs
+++ b/packages/oneclient_app/src/utils.rs
@@ -16,7 +16,10 @@ pub fn group_clusters_by_major(clusters: &[Cluster]) -> ClusterGroups {
let Some(parsed) = parse_mc_version(&cluster.mc_version) else {
continue;
};
- groups.entry(parsed.major).or_default().push(cluster.clone());
+ groups
+ .entry(parsed.major)
+ .or_default()
+ .push(cluster.clone());
}
for list in groups.values_mut() {
@@ -82,9 +85,7 @@ pub fn resolve_cluster(
.iter()
.find(|cluster| {
let version_ok = minor.is_none_or(|minor| {
- parse_mc_version(&cluster.mc_version)
- .and_then(|parsed| parsed.minor)
- == Some(minor)
+ parse_mc_version(&cluster.mc_version).and_then(|parsed| parsed.minor) == Some(minor)
});
let loader_ok = loader.is_none_or(|loader| cluster.mc_loader == loader);
version_ok && loader_ok
@@ -241,21 +242,22 @@ fn version_sort_key(cluster: &Cluster) -> (u32, u32) {
.unwrap_or((0, 0))
}
-
#[cfg(not(target_os = "linux"))]
pub fn is_wayland() -> bool {
- false
+ false
}
#[cfg(target_os = "linux")]
pub fn is_wayland() -> bool {
- static IS_WAYLAND: OnceLock<bool> = OnceLock::new();
-
- *IS_WAYLAND.get_or_init(|| {
- if cfg!(target_os = "linux") {
- std::env::var("XDG_SESSION_TYPE").map(|v| v == "wayland").unwrap_or(false)
- } else {
- false
- }
- })
-}
\ No newline at end of file
+ static IS_WAYLAND: OnceLock<bool> = OnceLock::new();
+
+ *IS_WAYLAND.get_or_init(|| {
+ if cfg!(target_os = "linux") {
+ std::env::var("XDG_SESSION_TYPE")
+ .map(|v| v == "wayland")
+ .unwrap_or(false)
+ } else {
+ false
+ }
+ })
+}
diff --git a/packages/oneclient_app/src/view/app/accounts.rs b/packages/oneclient_app/src/view/app/accounts.rs
index c94bc14..1148231 100644
--- a/packages/oneclient_app/src/view/app/accounts.rs
+++ b/packages/oneclient_app/src/view/app/accounts.rs
@@ -1,7 +1,9 @@
-use chrono::Utc;
+use chrono::{DateTime, Utc};
+use freya::animation::{AnimNum, Function, OnFinish, use_animation};
use freya::prelude::*;
use freya::query::{MutationCapability, MutationStateData, UseMutation};
use oneclient_core::auth::{AccountKind, MinecraftAccount};
+use uuid::Uuid;
use crate::components::{
Avatar, Button, Icon, IconType, OverlayPopup, PlayerModel, ScrollArea, TextInput,
@@ -74,13 +76,17 @@ impl Component for Accounts {
let mut rows: Vec<Element> = accounts
.iter()
.map(|account| {
- account_row(
- account,
- Some(account.id) == default_id,
+ AccountRow {
+ id: account.id,
+ username: account.username.clone(),
+ kind: account.kind,
+ expires: account.expires,
+ is_default: Some(account.id) == default_id,
set_default,
remove,
refresh,
- )
+ }
+ .into_element()
})
.collect();
if rows.is_empty() {
@@ -339,113 +345,182 @@ fn hint_line(icon: IconType, text: String, color: Color) -> impl IntoElement {
.into_element()
}
-fn account_row(
- account: &MinecraftAccount,
+const REFRESH_SPIN_TIME: u64 = 800;
+
+const REFRESHING_OPACITY: f32 = 0.7;
+
+struct AccountRow {
+ id: Uuid,
+ username: String,
+ kind: AccountKind,
+ expires: DateTime<Utc>,
is_default: bool,
set_default: crate::hooks::UseSetDefaultAccount,
remove: crate::hooks::UseRemoveAccount,
refresh: crate::hooks::UseRefreshAccount,
-) -> Element {
- let id = account.id;
- let is_microsoft = account.is_microsoft();
- let expired = is_microsoft && account.expires <= Utc::now();
-
- let border_color = if expired {
- colors::danger()
- } else if is_default {
- colors::brand()
- } else {
- colors::component_border()
- };
+}
- rect()
- .horizontal()
- .width(Size::fill())
- .cross_align(Alignment::Center)
- .content(Content::Flex)
- .spacing(16.)
- .padding(Gaps::new_all(12.))
- .corner_radius(CornerRadius::new_all(12.))
- .background(colors::page_elevated())
- .border(border_all_color(1., border_color))
- .a11y_role(AccessibilityRole::Button)
- .maybe(!is_default, |el| {
- el.on_press(move |_| set_default.mutate(SetDefaultAccountKeys { id: Some(id) }))
- })
- .child(
- Avatar::new(id.to_string())
- .width(Size::px(40.))
- .height(Size::px(40.)),
- )
- .child(
- rect()
- .vertical()
- .width(Size::flex(1.0))
- .spacing(4.)
- .child(
- rect()
- .horizontal()
- .cross_align(Alignment::Center)
- .spacing(8.)
- .child(
- label()
- .text(account.username.clone())
- .font_size(16.)
- .font_weight(FontWeight::MEDIUM)
- .color(colors::fg_primary()),
- )
- .child(kind_badge(account.kind))
- .maybe_child(is_default.then(default_badge))
- .maybe_child(expired.then(expired_badge)),
- )
- .child(
- label()
- .text(id.to_string())
- .font_size(11.)
- .color(colors::fg_secondary()),
- ),
- )
- .maybe_child(is_microsoft.then(|| {
- Button::new()
- .ghost()
- .icon()
- .on_press(move |e: Event<PressEventData>| {
- e.stop_propagation();
- refresh.mutate(RefreshAccountKeys { id });
- })
- .child(
- Icon::new(IconType::RefreshCw01)
- .size(18.)
- .color(if expired {
- colors::danger()
- } else {
- colors::fg_secondary()
- }),
- )
- .into_element()
- }))
- .child(
- Button::new().ghost().icon().enabled(false).child(
- Icon::new(IconType::Pencil01)
- .size(18.)
- .color(colors::fg_secondary()),
- ),
- )
- .child(
- Button::new()
- .ghost()
- .icon()
- .on_press(move |e: Event<PressEventData>| {
- e.stop_propagation();
- remove.mutate(RemoveAccountKeys { id });
- })
- .child(
- Icon::new(IconType::Trash01)
+impl PartialEq for AccountRow {
+ fn eq(&self, other: &Self) -> bool {
+ self.id == other.id
+ && self.username == other.username
+ && self.kind == other.kind
+ && self.expires == other.expires
+ && self.is_default == other.is_default
+ }
+}
+
+impl Component for AccountRow {
+ fn render_key(&self) -> DiffKey {
+ DiffKey::from(&self.id)
+ }
+
+ fn render(&self) -> impl IntoElement {
+ let id = self.id;
+ let is_default = self.is_default;
+ let set_default = self.set_default;
+ let remove = self.remove;
+ let refresh = self.refresh;
+
+ let is_microsoft = self.kind == AccountKind::Microsoft;
+ let expired = is_microsoft && self.expires <= Utc::now();
+
+ let mut refreshing = use_state(|| false);
+ let is_refreshing = *refreshing.read();
+
+ let spin = use_animation(|conf| {
+ conf.on_finish(OnFinish::restart());
+ AnimNum::new(0., 360.)
+ .time(REFRESH_SPIN_TIME)
+ .function(Function::Linear)
+ });
+
+ use_side_effect_with_deps(&is_refreshing, move |&is_refreshing| {
+ let mut spin = spin;
+ if is_refreshing {
+ spin.start();
+ } else {
+ spin.reset();
+ }
+ });
+
+ let rotation = if is_refreshing {
+ spin.get().value()
+ } else {
+ 0.
+ };
+
+ let border_color = if expired {
+ colors::danger()
+ } else if is_default {
+ colors::brand()
+ } else {
+ colors::component_border()
+ };
+
+ rect()
+ .horizontal()
+ .width(Size::fill())
+ .cross_align(Alignment::Center)
+ .content(Content::Flex)
+ .spacing(16.)
+ .padding(Gaps::new_all(12.))
+ .corner_radius(CornerRadius::new_all(12.))
+ .background(colors::page_elevated())
+ .border(border_all_color(1., border_color))
+ .opacity(if is_refreshing {
+ REFRESHING_OPACITY
+ } else {
+ 1.
+ })
+ .a11y_role(AccessibilityRole::Button)
+ .maybe(!is_default, |el| {
+ el.on_press(move |_| set_default.mutate(SetDefaultAccountKeys { id: Some(id) }))
+ })
+ .child(
+ Avatar::new(id.to_string())
+ .width(Size::px(40.))
+ .height(Size::px(40.)),
+ )
+ .child(
+ rect()
+ .vertical()
+ .width(Size::flex(1.0))
+ .spacing(4.)
+ .child(
+ rect()
+ .horizontal()
+ .cross_align(Alignment::Center)
+ .spacing(8.)
+ .child(
+ label()
+ .text(self.username.clone())
+ .font_size(16.)
+ .font_weight(FontWeight::MEDIUM)
+ .color(colors::fg_primary()),
+ )
+ .child(kind_badge(self.kind))
+ .maybe_child(is_default.then(default_badge))
+ .maybe_child(expired.then(expired_badge)),
+ )
+ .child(
+ label()
+ .text(id.to_string())
+ .font_size(11.)
+ .color(colors::fg_secondary()),
+ ),
+ )
+ .maybe_child(is_microsoft.then(|| {
+ Button::new()
+ .ghost()
+ .icon()
+ .on_press(move |e: Event<PressEventData>| {
+ e.stop_propagation();
+ if *refreshing.peek() {
+ return;
+ }
+ refreshing.set(true);
+ spawn(async move {
+ refresh.mutate_async(RefreshAccountKeys { id }).await;
+ refreshing.set(false);
+ });
+ })
+ .child(
+ rect().rotate(rotation).child(
+ Icon::new(IconType::RefreshCw01)
+ .size(18.)
+ .color(if expired {
+ colors::danger()
+ } else {
+ colors::fg_secondary()
+ }),
+ ),
+ )
+ .into_element()
+ }))
+ .child(
+ Button::new().ghost().icon().enabled(false).child(
+ Icon::new(IconType::Pencil01)
.size(18.)
.color(colors::fg_secondary()),
),
- )
- .into_element()
+ )
+ .child(
+ Button::new()
+ .ghost()
+ .icon()
+ .on_press(move |e: Event<PressEventData>| {
+ e.stop_propagation();
+ remove.mutate(RemoveAccountKeys { id });
+ })
+ .child(
+ Icon::new(IconType::Trash01)
+ .size(18.)
+ .color(colors::fg_secondary()),
+ ),
+ )
+ .into_element()
+ }
}
fn kind_badge(kind: AccountKind) -> impl IntoElement {
diff --git a/packages/oneclient_app/src/view/app/analytics/charts.rs b/packages/oneclient_app/src/view/app/analytics/charts.rs
index b43f531..7aeb775 100644
--- a/packages/oneclient_app/src/view/app/analytics/charts.rs
+++ b/packages/oneclient_app/src/view/app/analytics/charts.rs
@@ -62,7 +62,10 @@ impl Component for WhenChart {
)
}
WhenMode::Hour => {
- let hour = self.peak_hour.map(format_hour).unwrap_or_else(|| "—".to_string());
+ let hour = self
+ .peak_hour
+ .map(format_hour)
+ .unwrap_or_else(|| "—".to_string());
(
format!("Peak around {hour}"),
BarChart::new(self.per_hour.clone(), hour_labels())
@@ -148,7 +151,12 @@ impl Component for DailyChart {
let win_total: i64 = values.iter().sum();
let subtitle = match (slice.first(), slice.last()) {
(Some((a, _)), Some((b, _))) => {
- format!("{} · {} – {}", format_duration_hm(win_total), format_day(*a), format_day(*b))
+ format!(
+ "{} · {} – {}",
+ format_duration_hm(win_total),
+ format_day(*a),
+ format_day(*b)
+ )
}
_ => format_duration_hm(win_total),
};
@@ -159,24 +167,16 @@ impl Component for DailyChart {
.horizontal()
.cross_align(Alignment::Center)
.spacing(8.)
- .child(nav_button(
- IconType::ChevronsLeft,
- can_older,
- move |_| {
- if can_older {
- *offset.write() = off + 1;
- }
- },
- ))
- .child(nav_button(
- IconType::ChevronsRight,
- can_newer,
- move |_| {
- if can_newer {
- *offset.write() = off - 1;
- }
- },
- ))
+ .child(nav_button(IconType::ChevronsLeft, can_older, move |_| {
+ if can_older {
+ *offset.write() = off + 1;
+ }
+ }))
+ .child(nav_button(IconType::ChevronsRight, can_newer, move |_| {
+ if can_newer {
+ *offset.write() = off - 1;
+ }
+ }))
.child(
SegmentedControl::new(range)
.height(30.)
@@ -272,6 +272,12 @@ fn weekday_labels() -> Vec<String> {
fn hour_labels() -> Vec<String> {
(0..24)
- .map(|h| if h % 6 == 0 { format_hour(h) } else { String::new() })
+ .map(|h| {
+ if h % 6 == 0 {
+ format_hour(h)
+ } else {
+ String::new()
+ }
+ })
.collect()
}
diff --git a/packages/oneclient_app/src/view/app/analytics/mod.rs b/packages/oneclient_app/src/view/app/analytics/mod.rs
index f09a358..2235de7 100644
--- a/packages/oneclient_app/src/view/app/analytics/mod.rs
+++ b/packages/oneclient_app/src/view/app/analytics/mod.rs
@@ -45,11 +45,7 @@ fn charts_grid(cards: Vec<Element>) -> Element {
let mut grid = rect().vertical().width(Size::fill()).spacing(16.);
for pair in cards.chunks(2) {
if pair.len() == 1 {
- grid = grid.child(
- rect()
- .width(Size::fill())
- .child(pair[0].clone()),
- );
+ grid = grid.child(rect().width(Size::fill()).child(pair[0].clone()));
continue;
}
diff --git a/packages/oneclient_app/src/view/app/analytics/servers.rs b/packages/oneclient_app/src/view/app/analytics/servers.rs
index fe5743d..a8e3e7c 100644
--- a/packages/oneclient_app/src/view/app/analytics/servers.rs
+++ b/packages/oneclient_app/src/view/app/analytics/servers.rs
@@ -12,7 +12,12 @@ use super::{card, card_header};
pub(super) fn servers_section(servers: &[ServerStat]) -> Element {
let total_joins: i64 = servers.iter().map(|s| s.joins).sum();
- let max_secs = servers.iter().map(|s| s.total_secs).max().unwrap_or(0).max(1);
+ let max_secs = servers
+ .iter()
+ .map(|s| s.total_secs)
+ .max()
+ .unwrap_or(0)
+ .max(1);
let mut list = rect().vertical().width(Size::fill()).spacing(4.);
for server in servers.iter().take(10) {
@@ -155,9 +160,12 @@ impl Component for ServerIcon {
let reader = query.read();
let loaded = match (&self.url, &*reader.state()) {
(Some(url), QueryStateData::Settled { res: Ok(bytes), .. })
- | (Some(url), QueryStateData::Loading { res: Some(Ok(bytes)) }) => {
- Some((url.clone(), bytes.clone()))
- }
+ | (
+ Some(url),
+ QueryStateData::Loading {
+ res: Some(Ok(bytes)),
+ },
+ ) => Some((url.clone(), bytes.clone())),
_ => None,
};
@@ -185,7 +193,11 @@ fn server_icon_placeholder(is_ip: bool, size: f32) -> Element {
.height(Size::px(size))
.corner_radius(CornerRadius::new_all(6.))
.background(colors::component_bg())
- .child(Icon::new(icon).size(size * 0.5).color(colors::fg_secondary()))
+ .child(
+ Icon::new(icon)
+ .size(size * 0.5)
+ .color(colors::fg_secondary()),
+ )
.into_element()
}
diff --git a/packages/oneclient_app/src/view/app/analytics/tiles.rs b/packages/oneclient_app/src/view/app/analytics/tiles.rs
index aaf072c..10bb6bd 100644
--- a/packages/oneclient_app/src/view/app/analytics/tiles.rs
+++ b/packages/oneclient_app/src/view/app/analytics/tiles.rs
@@ -3,9 +3,9 @@ use freya::prelude::*;
use oneclient_core::game::{Analytics, Persona};
use crate::components::{Icon, IconType};
-use crate::utils::format_duration_hm;
use crate::theme::colors;
use crate::ui::border_all_color;
+use crate::utils::format_duration_hm;
use super::card;
@@ -19,10 +19,22 @@ pub(super) fn tiles_row(analytics: &Analytics) -> Element {
let total_joins: i64 = analytics.servers.iter().map(|s| s.joins).sum();
let mut tiles = vec![
- stat_tile(IconType::ClockRewind, "Total playtime", format_duration_hm(stats.total_secs)),
+ stat_tile(
+ IconType::ClockRewind,
+ "Total playtime",
+ format_duration_hm(stats.total_secs),
+ ),
stat_tile(IconType::Play, "Sessions", stats.session_count.to_string()),
- stat_tile(IconType::Calendar, "Avg / session", format_duration_hm(avg_session)),
- stat_tile(IconType::Rocket02, "Active days", stats.active_days.to_string()),
+ stat_tile(
+ IconType::Calendar,
+ "Avg / session",
+ format_duration_hm(avg_session),
+ ),
+ stat_tile(
+ IconType::Rocket02,
+ "Active days",
+ stats.active_days.to_string(),
+ ),
];
if !analytics.servers.is_empty() {
tiles.push(stat_tile(
@@ -97,7 +109,11 @@ fn persona_card(persona: Persona) -> Element {
.corner_radius(CornerRadius::new_all(22.))
.background(colors::brand().with_a(40))
.center()
- .child(Icon::new(persona_icon(persona)).size(22.).color(colors::brand())),
+ .child(
+ Icon::new(persona_icon(persona))
+ .size(22.)
+ .color(colors::brand()),
+ ),
)
.child(
rect()
diff --git a/packages/oneclient_app/src/view/app/browser/index/cards.rs b/packages/oneclient_app/src/view/app/browser/index/cards.rs
index bc9c10c..c919849 100644
--- a/packages/oneclient_app/src/view/app/browser/index/cards.rs
+++ b/packages/oneclient_app/src/view/app/browser/index/cards.rs
@@ -1,17 +1,19 @@
use super::*;
use freya::router::RouterContext;
-use oneclient_core::packages::types::ProjectSummary;
use oneclient_core::packages::ProviderId;
+use oneclient_core::packages::types::ProjectSummary;
-use crate::components::{
- Icon, IconType,
-};
+use crate::components::{Icon, IconType};
use crate::routes::Route;
use crate::theme::colors;
use crate::ui::border_all_color;
-pub(super) fn grid_row(row: Vec<ProjectSummary>, cluster_id: i64, package_type: &str) -> impl IntoElement {
+pub(super) fn grid_row(
+ row: Vec<ProjectSummary>,
+ cluster_id: i64,
+ package_type: &str,
+) -> impl IntoElement {
let package_type = package_type.to_string();
let fill = GRID_COLUMNS - row.len();
@@ -154,7 +156,11 @@ impl Component for PackageCard {
}
}
-pub(super) fn list_row(item: ProjectSummary, cluster_id: i64, package_type: &str) -> impl IntoElement {
+pub(super) fn list_row(
+ item: ProjectSummary,
+ cluster_id: i64,
+ package_type: &str,
+) -> impl IntoElement {
ListRow {
item,
cluster_id,
@@ -185,69 +191,69 @@ impl Component for ListRow {
Cursor::set(CursorIcon::default());
});
- rect()
- .horizontal()
- .width(Size::fill())
- .height(Size::px(LIST_ROW_H))
- .cross_align(Alignment::Center)
- .spacing(12.)
- .padding(Gaps::new_all(16.))
- .corner_radius(CornerRadius::new_all(10.))
- .background(CARD_BG)
- .border(border_all_color(
- 1.,
- if focused {
- colors::brand()
- } else {
- colors::component_border()
- },
- ))
- .overflow(Overflow::Clip)
- .content(Content::Flex)
- .a11y_id(a11y_id)
- .a11y_focusable(true)
- .a11y_role(AccessibilityRole::Button)
- .on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
- .on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
- .on_all_press(move |_| open_package(cluster_id, &package_type, provider, &id))
- .child(Thumbnail::new(item.icon_url.clone(), 48.).radius(8.))
- .child(
- rect()
- .vertical()
- .width(Size::flex(1.0))
- .spacing(3.)
- .child(
- rect()
- .horizontal()
- .cross_align(Alignment::Center)
- .spacing(6.)
- .child(
- label()
- .text(item.name.clone())
- .font_size(15.)
- .font_weight(FontWeight::MEDIUM)
- .max_lines(1)
- .color(CARD_NAME),
- )
- .child(
- label()
- .text(format!("by {}", item.author))
- .font_size(10.)
- .max_lines(1)
- .color(colors::fg_secondary()),
- )
- .child(Icon::new(item.provider).size(12.)),
- )
- .child(
- label()
- .text(item.summary.clone())
- .font_size(11.)
- .max_lines(2)
- .width(Size::fill())
- .color(colors::fg_secondary()),
- ),
- )
- .child(downloads_row(item.downloads))
+ rect()
+ .horizontal()
+ .width(Size::fill())
+ .height(Size::px(LIST_ROW_H))
+ .cross_align(Alignment::Center)
+ .spacing(12.)
+ .padding(Gaps::new_all(16.))
+ .corner_radius(CornerRadius::new_all(10.))
+ .background(CARD_BG)
+ .border(border_all_color(
+ 1.,
+ if focused {
+ colors::brand()
+ } else {
+ colors::component_border()
+ },
+ ))
+ .overflow(Overflow::Clip)
+ .content(Content::Flex)
+ .a11y_id(a11y_id)
+ .a11y_focusable(true)
+ .a11y_role(AccessibilityRole::Button)
+ .on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
+ .on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
+ .on_all_press(move |_| open_package(cluster_id, &package_type, provider, &id))
+ .child(Thumbnail::new(item.icon_url.clone(), 48.).radius(8.))
+ .child(
+ rect()
+ .vertical()
+ .width(Size::flex(1.0))
+ .spacing(3.)
+ .child(
+ rect()
+ .horizontal()
+ .cross_align(Alignment::Center)
+ .spacing(6.)
+ .child(
+ label()
+ .text(item.name.clone())
+ .font_size(15.)
+ .font_weight(FontWeight::MEDIUM)
+ .max_lines(1)
+ .color(CARD_NAME),
+ )
+ .child(
+ label()
+ .text(format!("by {}", item.author))
+ .font_size(10.)
+ .max_lines(1)
+ .color(colors::fg_secondary()),
+ )
+ .child(Icon::new(item.provider).size(12.)),
+ )
+ .child(
+ label()
+ .text(item.summary.clone())
+ .font_size(11.)
+ .max_lines(2)
+ .width(Size::fill())
+ .color(colors::fg_secondary()),
+ ),
+ )
+ .child(downloads_row(item.downloads))
}
}
diff --git a/packages/oneclient_app/src/view/app/browser/index/mod.rs b/packages/oneclient_app/src/view/app/browser/index/mod.rs
index cb4c799..77df9ea 100644
--- a/packages/oneclient_app/src/view/app/browser/index/mod.rs
+++ b/packages/oneclient_app/src/view/app/browser/index/mod.rs
@@ -1,6 +1,5 @@
use freya::animation::{
- AnimNum, Ease, Function, OnChange, OnCreation,
- use_animation_with_dependencies,
+ AnimNum, Ease, Function, OnChange, OnCreation, use_animation_with_dependencies,
};
use freya::prelude::*;
use oneclient_core::packages::types::ProjectSummary;
@@ -174,7 +173,9 @@ impl Component for Browser {
match mode {
ViewLayout::Grid => {
let rows = BROWSE_PAGE_SIZE.div_ceil(GRID_COLUMNS);
- sa.lazy(rows, CARD_H, GRID_SPACING, |_| skeleton_grid_row().into_element())
+ sa.lazy(rows, CARD_H, GRID_SPACING, |_| {
+ skeleton_grid_row().into_element()
+ })
}
ViewLayout::List => sa.lazy(BROWSE_PAGE_SIZE, LIST_ROW_H, LIST_SPACING, |_| {
SkeletonListRow.into_element()
@@ -289,4 +290,3 @@ fn controls(
),
)
}
-
diff --git a/packages/oneclient_app/src/view/app/browser/index/sidebar.rs b/packages/oneclient_app/src/view/app/browser/index/sidebar.rs
index 1e03d38..3808083 100644
--- a/packages/oneclient_app/src/view/app/browser/index/sidebar.rs
+++ b/packages/oneclient_app/src/view/app/browser/index/sidebar.rs
@@ -120,4 +120,3 @@ impl Component for CategoryRow {
)
}
}
-
diff --git a/packages/oneclient_app/src/view/app/browser/index/skeletons.rs b/packages/oneclient_app/src/view/app/browser/index/skeletons.rs
index d7e9aa4..30f6d2a 100644
--- a/packages/oneclient_app/src/view/app/browser/index/skeletons.rs
+++ b/packages/oneclient_app/src/view/app/browser/index/skeletons.rs
@@ -1,8 +1,6 @@
use super::*;
-use freya::animation::{
- AnimNum, Ease, Function, OnCreation, OnFinish, use_animation,
-};
+use freya::animation::{AnimNum, Ease, Function, OnCreation, OnFinish, use_animation};
use crate::theme::colors;
use crate::ui::border_all_color;
diff --git a/packages/oneclient_app/src/view/app/browser/package/mod.rs b/packages/oneclient_app/src/view/app/browser/package/mod.rs
index db7aadd..b3cdbce 100644
--- a/packages/oneclient_app/src/view/app/browser/package/mod.rs
+++ b/packages/oneclient_app/src/view/app/browser/package/mod.rs
@@ -3,8 +3,8 @@ use oneclient_core::packages::{ContentType, ProviderId};
use crate::components::ScrollArea;
use crate::hooks::{
- content_type_for_slug, project_detail, use_browser_compat, use_dispatch,
- use_link_confirm, use_package_project, use_package_versions, version_list, versions_total,
+ content_type_for_slug, project_detail, use_browser_compat, use_dispatch, use_link_confirm,
+ use_package_project, use_package_versions, version_list, versions_total,
};
use crate::theme::colors;
use crate::ui::border_all_color;
@@ -205,4 +205,3 @@ impl Component for BrowserPackage {
.into_element()
}
}
-
diff --git a/packages/oneclient_app/src/view/app/browser/package/panels.rs b/packages/oneclient_app/src/view/app/browser/package/panels.rs
index c0e854d..42f88fc 100644
--- a/packages/oneclient_app/src/view/app/browser/package/panels.rs
+++ b/packages/oneclient_app/src/view/app/browser/package/panels.rs
@@ -1,14 +1,14 @@
use super::*;
use freya::query::QueryStateData;
+use oneclient_core::packages::ProviderId;
use oneclient_core::packages::types::{
GalleryImage, PackageBody, ProjectDetail, ReleaseType, VersionSummary,
};
-use oneclient_core::packages::ProviderId;
use crate::BridgeDispatch;
use crate::components::{Button, Icon, IconType, Segment, SegmentedControl};
-use crate::hooks::{use_cached_image, VERSIONS_PAGE_SIZE};
+use crate::hooks::{VERSIONS_PAGE_SIZE, use_cached_image};
use crate::theme::colors;
use crate::ui::border_all_color;
use crate::utils::format_size;
@@ -16,7 +16,6 @@ use crate::utils::format_size;
const GALLERY_IMAGE_H: f32 = 360.;
const GALLERY_EDGE: u32 = 1024;
-
pub(super) fn loading_body() -> impl IntoElement {
rect()
.width(Size::fill())
@@ -356,4 +355,3 @@ fn release_badge(release_type: ReleaseType) -> impl IntoElement {
.color(color),
)
}
-
diff --git a/packages/oneclient_app/src/view/app/browser/package/sidebar.rs b/packages/oneclient_app/src/view/app/browser/package/sidebar.rs
index 3c4e40f..2b3b017 100644
--- a/packages/oneclient_app/src/view/app/browser/package/sidebar.rs
+++ b/packages/oneclient_app/src/view/app/browser/package/sidebar.rs
@@ -1,16 +1,13 @@
use super::*;
-use oneclient_core::packages::types::{
- ProjectDetail, ProjectMember,
-};
use oneclient_core::packages::ProviderId;
+use oneclient_core::packages::types::{ProjectDetail, ProjectMember};
use crate::BridgeDispatch;
use crate::components::{Button, Icon, IconType};
use crate::theme::colors;
use crate::ui::border_all_color;
-
pub(super) fn sidebar(
project: Option<ProjectDetail>,
latest_version: Option<String>,
diff --git a/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs b/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs
index 827836e..4d760ff 100644
--- a/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs
+++ b/packages/oneclient_app/src/view/app/cluster/cluster_settings.rs
@@ -1,4 +1,3 @@
-
use freya::prelude::*;
use oneclient_core::Patch;
use oneclient_core::java::JavaRuntime;
@@ -6,7 +5,8 @@ use oneclient_core::packages::domain::GameLoader;
use oneclient_core::settings::{GameSettingsProfile, ProfileUpdate, Resolution};
use crate::components::{
- Button, Dropdown, Icon, IconType, ScrollArea, TextInput, toggle, toggle_controlled, validate_number
+ Button, Dropdown, Icon, IconType, ScrollArea, TextInput, toggle, toggle_controlled,
+ validate_number,
};
use crate::hooks::{
ClusterAction, java_runtimes, loader_versions, try_game_profile, use_cluster_mutation,
@@ -37,7 +37,10 @@ impl Component for ClusterSettings {
.as_ref()
.map(|c| c.mc_version.clone())
.unwrap_or_default();
- let loader = cluster.as_ref().map(|c| c.mc_loader).unwrap_or(GameLoader::Fabric);
+ let loader = cluster
+ .as_ref()
+ .map(|c| c.mc_loader)
+ .unwrap_or(GameLoader::Fabric);
let versions_query = use_loader_versions(mc_version, loader);
let runtimes_query = use_java_runtimes();
@@ -149,13 +152,11 @@ fn reset_button(overridden: bool, on_reset: EventHandler<()>) -> impl IntoElemen
Button::new()
.small()
- .ghost()
- .icon()
+ .ghost()
+ .icon()
.corner_radius(CornerRadius::new_all(7.))
- .maybe(overridden, |el| {
- el.on_press(move |_| on_reset.call(()))
- })
- .enabled(overridden)
+ .maybe(overridden, |el| el.on_press(move |_| on_reset.call(())))
+ .enabled(overridden)
.child(Icon::new(IconType::RefreshCcw02).size(15.).color(color))
.into_element()
}
@@ -394,7 +395,11 @@ impl Component for ResolutionRow {
.text_align(TextAlign::Center)
.width(Size::px(70.)),
)
- .child(Icon::new(IconType::X).size(14.).color(colors::fg_secondary()))
+ .child(
+ Icon::new(IconType::X)
+ .size(14.)
+ .color(colors::fg_secondary()),
+ )
.child(
TextInput::new(height)
.placeholder("480")
@@ -527,7 +532,12 @@ impl Component for TextRow {
.placeholder(placeholder)
.width(Size::px(220.));
- settings_row(icon, title, description, override_cell(control, overridden, on_reset))
+ settings_row(
+ icon,
+ title,
+ description,
+ override_cell(control, overridden, on_reset),
+ )
}
}
@@ -564,8 +574,7 @@ impl Component for JavaRow {
.map(runtime_label)
.unwrap_or_else(|| "Automatic".into());
- let paths: Vec<String> =
- runtimes.iter().map(|r| r.absolute_path.clone()).collect();
+ let paths: Vec<String> = runtimes.iter().map(|r| r.absolute_path.clone()).collect();
let dispatch = dispatch.clone();
Dropdown::new(selected, options)
@@ -641,7 +650,10 @@ impl Component for LoaderRow {
settings_row(
IconType::Rocket02,
"Loader Version",
- format!("The {} loader version used to launch this cluster.", self.loader),
+ format!(
+ "The {} loader version used to launch this cluster.",
+ self.loader
+ ),
control,
)
}
diff --git a/packages/oneclient_app/src/view/app/cluster/logs.rs b/packages/oneclient_app/src/view/app/cluster/logs.rs
index fa61a34..e136905 100644
--- a/packages/oneclient_app/src/view/app/cluster/logs.rs
+++ b/packages/oneclient_app/src/view/app/cluster/logs.rs
@@ -65,7 +65,8 @@ enum Confirm {
fn kind_badge(kind: LogKind) -> &'static str {
match kind {
- LogKind::Game { .. } => "Launcher",
+ LogKind::Game { .. } => "Game output",
+ LogKind::Launcher => "Launcher logs",
LogKind::Minecraft => "Minecraft",
LogKind::CrashReport => "Crash report",
LogKind::Other => "Other",
@@ -151,7 +152,7 @@ impl Component for ClusterLogs {
let value = search.read().clone();
(!value.trim().is_empty()).then_some(value)
};
-
+
let content_query = use_log_content(
selected_path.clone(),
level.read().to_level(),
@@ -162,7 +163,7 @@ impl Component for ClusterLogs {
let Some(cluster) = load_cluster(cluster_id) else {
return cluster_not_found();
};
- let folder = cluster.game_dir().ok().map(|d| d.join("logs"));
+ let folder = cluster.dir().ok().map(|d| d.join("logs"));
let files = try_cluster_logs(&logs_query).unwrap_or_default();
let has_log = !files.is_empty();
diff --git a/packages/oneclient_app/src/view/app/cluster/mod.rs b/packages/oneclient_app/src/view/app/cluster/mod.rs
index bc95a97..1cf1f0e 100644
--- a/packages/oneclient_app/src/view/app/cluster/mod.rs
+++ b/packages/oneclient_app/src/view/app/cluster/mod.rs
@@ -1,23 +1,23 @@
-mod overview;
mod logs;
+mod overview;
mod package_manager;
mod cluster_settings;
mod mods;
+mod process_logs;
mod screenshots;
mod shaders;
mod textures;
-mod process_logs;
-pub use overview::ClusterOverview;
pub use logs::ClusterLogs;
+pub use overview::ClusterOverview;
pub use cluster_settings::ClusterSettings;
pub use mods::ClusterMods;
+pub use process_logs::ProcessLogs;
pub use screenshots::ClusterScreenshots;
pub use shaders::ClusterShaders;
pub use textures::ClusterTextures;
-pub use process_logs::ProcessLogs;
use freya::prelude::*;
use freya::query::QueryStateData;
diff --git a/packages/oneclient_app/src/view/app/cluster/mods.rs b/packages/oneclient_app/src/view/app/cluster/mods.rs
index 6387b54..9c8622e 100644
--- a/packages/oneclient_app/src/view/app/cluster/mods.rs
+++ b/packages/oneclient_app/src/view/app/cluster/mods.rs
@@ -1,14 +1,14 @@
use freya::prelude::*;
-use oneclient_core::packages::{ContentType, ProviderId};
+use oneclient_core::packages::ContentType;
use crate::hooks::{
- bundle_overrides_map, bundles_with_status_items, cluster_content_items, package_meta_batch,
- use_bundle_overrides, use_bundles_with_status, use_cluster_content, use_package_meta_batch,
+ bundle_overrides_map, bundles_with_status_items, cluster_content_items, use_bundle_overrides,
+ use_bundles_with_status, use_cluster_content,
};
use crate::layout::cluster_content;
use super::package_manager::{
- PackageManager, bundle_categories, bundle_packages, managed_project_ids,
+ PackageManager, bundle_categories, bundle_packages, use_content_meta,
};
use super::{cluster_not_found, load_cluster};
@@ -23,8 +23,8 @@ impl Component for ClusterMods {
let bundles = use_bundles_with_status(self.cluster_id);
let overrides = use_bundle_overrides(self.cluster_id);
let bundle_items = bundles_with_status_items(&bundles);
- let project_ids = managed_project_ids(&bundle_items, ContentType::Mod);
- let meta = use_package_meta_batch(ProviderId::Modrinth, project_ids);
+ let content_items = cluster_content_items(&content);
+ let meta = use_content_meta(&content_items, &bundle_items, ContentType::Mod);
let Some(_cluster) = load_cluster(self.cluster_id) else {
return cluster_not_found();
@@ -32,10 +32,10 @@ impl Component for ClusterMods {
let all_categories = bundle_categories(&bundle_items);
let items = bundle_packages(
- cluster_content_items(&content),
+ content_items,
&bundle_items,
&bundle_overrides_map(&overrides),
- &package_meta_batch(&meta),
+ &meta,
ContentType::Mod,
);
diff --git a/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs b/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs
index 20fdcbc..3665b0a 100644
--- a/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs
+++ b/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs
@@ -1,4 +1,3 @@
-
use std::collections::{HashMap, HashSet};
use freya::prelude::*;
@@ -6,7 +5,7 @@ use oneclient_core::packages::{CachedPackageMeta, ContentType, ProviderId};
use oneclient_core::{BundleFileKind, BundleWithUpdateStatus, LinkedArtifactInfo};
use crate::components::{CardLayout, PackageEntry};
-use crate::hooks::use_view_state;
+use crate::hooks::{package_meta_batch, use_package_meta_batch, use_view_state};
mod views;
use views::{ContentBox, ContentKind, EnabledFilter, SortMode, toolbar_bar};
@@ -18,9 +17,13 @@ const GRID_GAP: f32 = 10.;
const GRID_MIN_W: f32 = 260.;
const LAZY_OVERSCAN: i64 = 2;
-pub fn managed_project_ids(
+pub type PackageMetaMap = HashMap<(ProviderId, String), CachedPackageMeta>;
+
+fn provider_project_ids(
+ content: &[LinkedArtifactInfo],
bundles: &[BundleWithUpdateStatus],
content_type: ContentType,
+ provider: ProviderId,
) -> Vec<String> {
let mut ids = Vec::new();
for bundle in bundles {
@@ -28,19 +31,49 @@ pub fn managed_project_ids(
if file.content_type() != content_type {
continue;
}
- if let BundleFileKind::Managed { project_id, .. } = &file.kind {
+ if let BundleFileKind::Managed {
+ project_id,
+ provider: file_provider,
+ ..
+ } = &file.kind
+ && *file_provider == provider
+ {
ids.push(project_id.clone());
}
}
}
+ for info in content {
+ if info.content_type != content_type || info.provider != Some(provider) {
+ continue;
+ }
+ if let Some(project_id) = &info.project_id {
+ ids.push(project_id.clone());
+ }
+ }
ids
}
+pub fn use_content_meta(
+ content: &[LinkedArtifactInfo],
+ bundles: &[BundleWithUpdateStatus],
+ content_type: ContentType,
+) -> PackageMetaMap {
+ let mut out = PackageMetaMap::new();
+ for provider in ProviderId::REMOTE_PROVIDERS.iter().copied() {
+ let ids = provider_project_ids(content, bundles, content_type, provider);
+ let query = use_package_meta_batch(provider, ids);
+ for (project_id, meta) in package_meta_batch(&query) {
+ out.insert((provider, project_id), meta);
+ }
+ }
+ out
+}
+
pub fn bundle_packages(
content: Vec<LinkedArtifactInfo>,
bundles: &[BundleWithUpdateStatus],
overrides: &HashMap<(String, String), String>,
- meta: &HashMap<String, CachedPackageMeta>,
+ meta: &PackageMetaMap,
content_type: ContentType,
) -> Vec<PackageEntry> {
let mut by_project: HashMap<&str, &LinkedArtifactInfo> = HashMap::new();
@@ -104,10 +137,7 @@ pub fn bundle_packages(
}
for info in &content {
- let in_bundle = info
- .project_id
- .as_deref()
- .is_some_and(|p| seen.contains(p))
+ let in_bundle = info.project_id.as_deref().is_some_and(|p| seen.contains(p))
|| seen.contains(&info.hash);
if in_bundle {
continue;
@@ -141,10 +171,10 @@ fn make_row(
categories: Vec<String>,
enabled: bool,
installed_info: Option<&LinkedArtifactInfo>,
- meta: &HashMap<String, CachedPackageMeta>,
+ meta: &PackageMetaMap,
fallback_name: String,
) -> PackageEntry {
- let m = meta.get(&package_id);
+ let m = meta.get(&(provider, package_id.clone()));
let name = m
.map(|p| p.name.clone())
.filter(|s| !s.is_empty())
diff --git a/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs b/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs
index 529adc1..4b30c03 100644
--- a/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs
+++ b/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs
@@ -264,7 +264,13 @@ impl Component for FilterPopover {
.background(colors::page_elevated().with_a(230))
.blur(12.)
.border(crate::ui::border_all_color(1., colors::component_border()))
- .shadow(Shadow::from((0., 8., 32., 0., Color::from_argb(120, 0, 0, 0))))
+ .shadow(Shadow::from((
+ 0.,
+ 8.,
+ 32.,
+ 0.,
+ Color::from_argb(120, 0, 0, 0),
+ )))
.a11y_id(a11y_id)
.a11y_role(AccessibilityRole::Dialog)
.on_global_key_down(move |e: Event<KeyboardEventData>| {
@@ -527,12 +533,16 @@ impl Component for ContentBox {
let header = (count > 0)
.then(|| match kind {
- ContentKind::External => Some(
- action_header(browse_button(cluster_id, package_type)).into_element(),
- ),
+ ContentKind::External => {
+ Some(action_header(browse_button(cluster_id, package_type)).into_element())
+ }
ContentKind::Local => Some(
- action_header(add_from_file_button(cluster_id, content_type, dispatch.clone()))
- .into_element(),
+ action_header(add_from_file_button(
+ cluster_id,
+ content_type,
+ dispatch.clone(),
+ ))
+ .into_element(),
),
ContentKind::Other => None,
})
diff --git a/packages/oneclient_app/src/view/app/cluster/process_logs.rs b/packages/oneclient_app/src/view/app/cluster/process_logs.rs
index 8568ae6..df8b1f7 100644
--- a/packages/oneclient_app/src/view/app/cluster/process_logs.rs
+++ b/packages/oneclient_app/src/view/app/cluster/process_logs.rs
@@ -1,4 +1,3 @@
-
use freya::prelude::*;
use crate::components::LogViewer;
@@ -30,9 +29,7 @@ impl Component for ProcessLogs {
not_running()
};
- cluster_content()
- .child(body)
- .into_element()
+ cluster_content().child(body).into_element()
}
}
diff --git a/packages/oneclient_app/src/view/app/cluster/screenshots.rs b/packages/oneclient_app/src/view/app/cluster/screenshots.rs
index ed13be2..c9fd68e 100644
--- a/packages/oneclient_app/src/view/app/cluster/screenshots.rs
+++ b/packages/oneclient_app/src/view/app/cluster/screenshots.rs
@@ -8,8 +8,7 @@ use oneclient_core::settings::ViewLayout;
use crate::components::{
Button, ContextMenu, Icon, IconType, LocalImage, OverlayPopup, ScreenshotViewer, ScrollArea,
- open_folder_button,
- Segment, SegmentedControl,
+ Segment, SegmentedControl, open_folder_button,
};
use crate::hooks::{
ScreenshotAction, try_cluster_screenshots, use_cluster_screenshots, use_dispatch,
@@ -57,7 +56,14 @@ impl Component for ClusterScreenshots {
let grid_width = use_state(|| 0f32);
let mut menu = use_state(|| None::<(f32, f32, PathBuf)>);
- let toolbar = toolbar_row(&shots, folder, view_mode, edit_mode, selected, confirm_delete);
+ let toolbar = toolbar_row(
+ &shots,
+ folder,
+ view_mode,
+ edit_mode,
+ selected,
+ confirm_delete,
+ );
let content: Element = if shots.is_empty() {
empty_state(matches!(
@@ -210,7 +216,12 @@ fn toolbar_row(
.horizontal()
.cross_align(Alignment::Center)
.spacing(10.)
- .maybe_child((!editing).then_some(folder).flatten().map(open_folder_button));
+ .maybe_child(
+ (!editing)
+ .then_some(folder)
+ .flatten()
+ .map(open_folder_button),
+ );
if editing {
let select_all_paths = all_paths.clone();
@@ -422,8 +433,6 @@ fn confirm_panel(
.into_element()
}
-
-
fn res_badge(res: Option<(u32, u32)>) -> Option<Element> {
res.map(format_res).map(|text| {
rect()
diff --git a/packages/oneclient_app/src/view/app/cluster/shaders.rs b/packages/oneclient_app/src/view/app/cluster/shaders.rs
index 61fde57..a69000b 100644
--- a/packages/oneclient_app/src/view/app/cluster/shaders.rs
+++ b/packages/oneclient_app/src/view/app/cluster/shaders.rs
@@ -1,14 +1,14 @@
use freya::prelude::*;
-use oneclient_core::packages::{ContentType, ProviderId};
+use oneclient_core::packages::ContentType;
use crate::hooks::{
- bundle_overrides_map, bundles_with_status_items, cluster_content_items, package_meta_batch,
- use_bundle_overrides, use_bundles_with_status, use_cluster_content, use_package_meta_batch,
+ bundle_overrides_map, bundles_with_status_items, cluster_content_items, use_bundle_overrides,
+ use_bundles_with_status, use_cluster_content,
};
use crate::layout::cluster_content;
use super::package_manager::{
- PackageManager, bundle_categories, bundle_packages, managed_project_ids,
+ PackageManager, bundle_categories, bundle_packages, use_content_meta,
};
use super::{cluster_not_found, load_cluster};
@@ -23,8 +23,8 @@ impl Component for ClusterShaders {
let bundles = use_bundles_with_status(self.cluster_id);
let overrides = use_bundle_overrides(self.cluster_id);
let bundle_items = bundles_with_status_items(&bundles);
- let project_ids = managed_project_ids(&bundle_items, ContentType::Shader);
- let meta = use_package_meta_batch(ProviderId::Modrinth, project_ids);
+ let content_items = cluster_content_items(&content);
+ let meta = use_content_meta(&content_items, &bundle_items, ContentType::Shader);
let Some(_cluster) = load_cluster(self.cluster_id) else {
return cluster_not_found();
@@ -32,10 +32,10 @@ impl Component for ClusterShaders {
let all_categories = bundle_categories(&bundle_items);
let items = bundle_packages(
- cluster_content_items(&content),
+ content_items,
&bundle_items,
&bundle_overrides_map(&overrides),
- &package_meta_batch(&meta),
+ &meta,
ContentType::Shader,
);
diff --git a/packages/oneclient_app/src/view/app/cluster/textures.rs b/packages/oneclient_app/src/view/app/cluster/textures.rs
index fc42cbd..9f08ec8 100644
--- a/packages/oneclient_app/src/view/app/cluster/textures.rs
+++ b/packages/oneclient_app/src/view/app/cluster/textures.rs
@@ -1,14 +1,14 @@
use freya::prelude::*;
-use oneclient_core::packages::{ContentType, ProviderId};
+use oneclient_core::packages::ContentType;
use crate::hooks::{
- bundle_overrides_map, bundles_with_status_items, cluster_content_items, package_meta_batch,
- use_bundle_overrides, use_bundles_with_status, use_cluster_content, use_package_meta_batch,
+ bundle_overrides_map, bundles_with_status_items, cluster_content_items, use_bundle_overrides,
+ use_bundles_with_status, use_cluster_content,
};
use crate::layout::cluster_content;
use super::package_manager::{
- PackageManager, bundle_categories, bundle_packages, managed_project_ids,
+ PackageManager, bundle_categories, bundle_packages, use_content_meta,
};
use super::{cluster_not_found, load_cluster};
@@ -23,8 +23,8 @@ impl Component for ClusterTextures {
let bundles = use_bundles_with_status(self.cluster_id);
let overrides = use_bundle_overrides(self.cluster_id);
let bundle_items = bundles_with_status_items(&bundles);
- let project_ids = managed_project_ids(&bundle_items, ContentType::ResourcePack);
- let meta = use_package_meta_batch(ProviderId::Modrinth, project_ids);
+ let content_items = cluster_content_items(&content);
+ let meta = use_content_meta(&content_items, &bundle_items, ContentType::ResourcePack);
let Some(_cluster) = load_cluster(self.cluster_id) else {
return cluster_not_found();
@@ -32,10 +32,10 @@ impl Component for ClusterTextures {
let all_categories = bundle_categories(&bundle_items);
let items = bundle_packages(
- cluster_content_items(&content),
+ content_items,
&bundle_items,
&bundle_overrides_map(&overrides),
- &package_meta_batch(&meta),
+ &meta,
ContentType::ResourcePack,
);
diff --git a/packages/oneclient_app/src/view/app/clusters/page.rs b/packages/oneclient_app/src/view/app/clusters/page.rs
index 14b4e41..3d416fe 100644
--- a/packages/oneclient_app/src/view/app/clusters/page.rs
+++ b/packages/oneclient_app/src/view/app/clusters/page.rs
@@ -8,7 +8,6 @@ use crate::components::{Button, ClusterLandscapeArt, Dropdown, Icon, IconType, M
use crate::hooks::{
use_active_cluster_id, use_clusters, use_dispatch, use_game_snapshot, use_version_metadata,
};
-use crate::view::app::launch_button_state;
use crate::routes::Route;
use crate::theme::colors;
use crate::ui::border_all_color;
@@ -16,6 +15,7 @@ use crate::utils::{
default_loader, default_major, default_minor, group_clusters_by_major, loaders_for_major,
major_pretty_name, minor_label, minor_versions, resolve_cluster,
};
+use crate::view::app::launch_button_state;
const GRID_GAP_PX: f32 = 12.;
const MIN_CARD_WIDTH_PX: f32 = 280.;
@@ -212,7 +212,10 @@ fn detail_sidebar(
.as_ref()
.and_then(|m| m.long_description.clone())
.unwrap_or_else(|| PLACEHOLDER_VERSION_INFO.to_string());
- let tags = metadata.as_ref().map(|m| m.tags.clone()).unwrap_or_default();
+ let tags = metadata
+ .as_ref()
+ .map(|m| m.tags.clone())
+ .unwrap_or_default();
rect()
.width(Size::px(SIDEBAR_WIDTH_PX))
@@ -225,17 +228,9 @@ fn detail_sidebar(
.background(colors::page_elevated())
.border(border_all_color(1., colors::component_border()))
.overflow(Overflow::Clip)
- .child(
- rect()
- .width(Size::fill())
- .max_height(Size::px(140.))
- .child(ClusterLandscapeArt::for_version(
- major,
- minor_value,
- loader_value,
- false,
- )),
- )
+ .child(rect().width(Size::fill()).max_height(Size::px(140.)).child(
+ ClusterLandscapeArt::for_version(major, minor_value, loader_value, false),
+ ))
.child(
rect()
.vertical()
@@ -274,7 +269,11 @@ fn detail_sidebar(
.content(Content::Flex)
.main_align(Alignment::Center)
.spacing(8.)
- .child(play_button(cluster_id, dispatch, launch_button_state(&game, cluster_id)))
+ .child(play_button(
+ cluster_id,
+ dispatch,
+ launch_button_state(&game, cluster_id),
+ ))
.child(view_button(cluster_id, active_id)),
),
)
diff --git a/packages/oneclient_app/src/view/app/mod.rs b/packages/oneclient_app/src/view/app/mod.rs
index 9df3240..1cf9396 100644
--- a/packages/oneclient_app/src/view/app/mod.rs
+++ b/packages/oneclient_app/src/view/app/mod.rs
@@ -1,14 +1,14 @@
-mod analytics;
-mod home;
mod accounts;
+mod analytics;
mod clusters;
mod debug;
+mod home;
mod skins;
mod stats;
pub mod browser;
-pub mod settings;
pub mod cluster;
+pub mod settings;
use crate::bridge::GameSnapshot;
use oneclient_core::notification::LaunchStage;
@@ -23,10 +23,10 @@ pub fn launch_button_state(game: &GameSnapshot, cluster_id: i64) -> (&'static st
}
}
-pub(crate) use analytics::analytics_body;
pub use accounts::Accounts;
-pub use home::Home;
+pub(crate) use analytics::analytics_body;
pub use clusters::Clusters;
pub use debug::Debug;
+pub use home::Home;
pub use skins::AccountSkins;
pub use stats::Stats;
diff --git a/packages/oneclient_app/src/view/app/settings/apis.rs b/packages/oneclient_app/src/view/app/settings/apis.rs
index ac42047..13dd672 100644
--- a/packages/oneclient_app/src/view/app/settings/apis.rs
+++ b/packages/oneclient_app/src/view/app/settings/apis.rs
@@ -1,8 +1,8 @@
use freya::prelude::*;
+use super::settings_page;
use crate::components::{IconType, TextInput};
use crate::hooks::{use_dispatch, use_settings_snapshot};
-use super::settings_page;
use crate::view::app::settings::{section_header, settings_row};
fn normalize(value: &str) -> Option<String> {
diff --git a/packages/oneclient_app/src/view/app/settings/appearance.rs b/packages/oneclient_app/src/view/app/settings/appearance.rs
index 7a43f38..3edc388 100644
--- a/packages/oneclient_app/src/view/app/settings/appearance.rs
+++ b/packages/oneclient_app/src/view/app/settings/appearance.rs
@@ -1,8 +1,8 @@
use freya::prelude::*;
+use super::settings_page;
use crate::components::{Icon, IconType, toggle};
use crate::hooks::{use_dispatch, use_settings_snapshot};
-use super::settings_page;
use crate::theme::colors;
use crate::ui::border_all_color;
use crate::view::app::settings::settings_row;
diff --git a/packages/oneclient_app/src/view/app/settings/changelog.rs b/packages/oneclient_app/src/view/app/settings/changelog.rs
index 8238e3f..791bf81 100644
--- a/packages/oneclient_app/src/view/app/settings/changelog.rs
+++ b/packages/oneclient_app/src/view/app/settings/changelog.rs
@@ -1,8 +1,8 @@
use freya::prelude::*;
+use super::settings_page;
use crate::components::{Icon, IconType};
use crate::hooks::{changelog_error, changelog_groups, changelog_is_loading, use_changelog};
-use super::settings_page;
use crate::theme::colors;
#[derive(PartialEq)]
@@ -119,14 +119,16 @@ impl Component for ReleaseCard {
.spacing(4.)
.padding(Gaps::new(0., 0., 0., 6.))
.children(if changes.is_empty() {
- vec![rect()
- .child(
- label()
- .text("No changes recorded for this version.")
- .font_size(12.)
- .color(colors::fg_secondary()),
- )
- .into_element()]
+ vec![
+ rect()
+ .child(
+ label()
+ .text("No changes recorded for this version.")
+ .font_size(12.)
+ .color(colors::fg_secondary()),
+ )
+ .into_element(),
+ ]
} else {
changes
.into_iter()
diff --git a/packages/oneclient_app/src/view/app/settings/developer.rs b/packages/oneclient_app/src/view/app/settings/developer.rs
index 4e5983c..3a35490 100644
--- a/packages/oneclient_app/src/view/app/settings/developer.rs
+++ b/packages/oneclient_app/src/view/app/settings/developer.rs
@@ -1,10 +1,10 @@
use freya::prelude::*;
use freya::router::RouterContext;
+use super::settings_page;
use crate::Route;
use crate::components::{Button, IconType, link_button, toggle};
use crate::hooks::{use_browser_compat, use_dispatch, use_settings_snapshot};
-use super::settings_page;
use crate::view::app::settings::{section_header, settings_row};
#[derive(PartialEq)]
@@ -67,7 +67,7 @@ impl Component for SettingsDeveloper {
"Debug Page",
"View the debug page.",
link_button().on_press(move |_| {
- let _ = RouterContext::get().push(Route::Debug { });
+ let _ = RouterContext::get().push(Route::Debug {});
}),
))
.into_element()
diff --git a/packages/oneclient_app/src/view/app/settings/java.rs b/packages/oneclient_app/src/view/app/settings/java.rs
index 5a9c3e6..bc8cda8 100644
--- a/packages/oneclient_app/src/view/app/settings/java.rs
+++ b/packages/oneclient_app/src/view/app/settings/java.rs
@@ -1,12 +1,12 @@
use freya::prelude::*;
use oneclient_core::java::{AvailableJava, JavaRuntime, JavaVendor};
+use super::settings_page;
use crate::components::{Button, Dropdown, Icon, IconType, OverlayPopup, ScrollArea};
use crate::hooks::{
- java_runtimes, provider_versions, use_dispatch, use_java_runtimes, use_provider_versions,
- BridgeDispatch,
+ BridgeDispatch, java_runtimes, provider_versions, use_dispatch, use_java_runtimes,
+ use_provider_versions,
};
-use super::settings_page;
use crate::theme::colors;
use crate::ui::border_all_color;
use crate::view::app::settings::section_header;
@@ -151,15 +151,13 @@ fn table_header() -> impl IntoElement {
.child(head("VENDOR", Size::px(130.)))
.child(head("VERSION", Size::px(90.)))
.child(
- rect()
- .width(Size::flex(1.0))
- .child(
- label()
- .text("PATH")
- .font_size(11.)
- .font_weight(FontWeight::MEDIUM)
- .color(colors::fg_secondary()),
- ),
+ rect().width(Size::flex(1.0)).child(
+ label()
+ .text("PATH")
+ .font_size(11.)
+ .font_weight(FontWeight::MEDIUM)
+ .color(colors::fg_secondary()),
+ ),
)
.child(rect().width(Size::px(34.)))
.into_element()
@@ -206,7 +204,10 @@ impl Component for RuntimeRow {
.fill(colors::component_border()),
)
})
- .child(cell(format!("{} {}", runtime.vendor, runtime.major), Size::px(130.)))
+ .child(cell(
+ format!("{} {}", runtime.vendor, runtime.major),
+ Size::px(130.),
+ ))
.child(cell(runtime.version.clone(), Size::px(90.)))
.child(
rect()
@@ -241,7 +242,11 @@ fn remove_button(dispatch: BridgeDispatch, path: String) -> impl IntoElement {
.ghost()
.small()
.on_press(move |_| dispatch.remove_java_runtime(path.clone()))
- .child(Icon::new(IconType::Trash01).size(14.).color(colors::danger()))
+ .child(
+ Icon::new(IconType::Trash01)
+ .size(14.)
+ .color(colors::danger()),
+ )
.into_element()
}
diff --git a/packages/oneclient_app/src/view/app/settings/language.rs b/packages/oneclient_app/src/view/app/settings/language.rs
index baba05a..0248396 100644
--- a/packages/oneclient_app/src/view/app/settings/language.rs
+++ b/packages/oneclient_app/src/view/app/settings/language.rs
@@ -1,13 +1,11 @@
use freya::prelude::*;
-use crate::components::{IconType, Icon};
use super::settings_page;
+use crate::components::{Icon, IconType};
use crate::theme::colors;
use crate::view::app::settings::section_header;
-const LANGUAGES: &[(&str, &str)] = &[
- ("English", "English (US)"),
-];
+const LANGUAGES: &[(&str, &str)] = &[("English", "English (US)")];
#[derive(PartialEq)]
pub struct SettingsLanguage;
@@ -63,8 +61,11 @@ fn language_row(
.color(colors::fg_secondary()),
),
)
- .maybe_child(
- is_selected.then(|| Icon::new(IconType::Check).size(20.).color(colors::brand()).into_element()),
- )
+ .maybe_child(is_selected.then(|| {
+ Icon::new(IconType::Check)
+ .size(20.)
+ .color(colors::brand())
+ .into_element()
+ }))
.into_element()
}
diff --git a/packages/oneclient_app/src/view/app/settings/launcher.rs b/packages/oneclient_app/src/view/app/settings/launcher.rs
index cc518d2..c5420fb 100644
--- a/packages/oneclient_app/src/view/app/settings/launcher.rs
+++ b/packages/oneclient_app/src/view/app/settings/launcher.rs
@@ -1,8 +1,8 @@
use freya::prelude::*;
+use super::settings_page;
use crate::components::{IconType, link_button, toggle};
use crate::hooks::{use_dispatch, use_launcher, use_settings_snapshot};
-use super::settings_page;
use crate::platform;
use crate::view::app::settings::{section_header, settings_row};
@@ -20,17 +20,24 @@ impl Component for SettingsLauncher {
move || v
});
+ let crash_reporting = use_state({
+ let v = settings.crash_reporting;
+ move || v
+ });
+
let mut first = use_state(|| true);
{
let settings = settings.clone();
use_side_effect(move || {
let discord = *discord_rpc.read();
+ let crash = *crash_reporting.read();
if *first.peek() {
first.set(false);
return;
}
let mut next = settings.clone();
next.discord_enabled = discord;
+ next.crash_reporting = crash;
dispatch.set_settings(next);
});
}
@@ -46,6 +53,12 @@ impl Component for SettingsLauncher {
"Enable Discord Rich Presence.",
toggle(discord_rpc),
))
+ .child(settings_row(
+ IconType::AlertTriangle,
+ "Crash Reporting",
+ "Send anonymous crash and error reports to help fix bugs. Applies on restart.",
+ toggle(crash_reporting),
+ ))
.child(section_header("FOLDERS AND FILES"))
.child(settings_row(
IconType::Folder,
diff --git a/packages/oneclient_app/src/view/app/settings/minecraft.rs b/packages/oneclient_app/src/view/app/settings/minecraft.rs
index 5b88f8d..bbd2041 100644
--- a/packages/oneclient_app/src/view/app/settings/minecraft.rs
+++ b/packages/oneclient_app/src/view/app/settings/minecraft.rs
@@ -2,9 +2,9 @@ use freya::prelude::*;
use oneclient_core::Patch;
use oneclient_core::settings::{ProfileUpdate, Resolution};
+use super::settings_page;
use crate::components::{Icon, IconType, TextInput, toggle, validate_number};
use crate::hooks::{use_dispatch, use_settings_snapshot};
-use super::settings_page;
use crate::theme::colors;
use crate::view::app::settings::{section_header, settings_row};
@@ -21,11 +21,17 @@ impl Component for SettingsMinecraft {
move || v
});
let width = use_state({
- let v = profile.resolution.map(|r| r.width.to_string()).unwrap_or_default();
+ let v = profile
+ .resolution
+ .map(|r| r.width.to_string())
+ .unwrap_or_default();
move || v
});
let height = use_state({
- let v = profile.resolution.map(|r| r.height.to_string()).unwrap_or_default();
+ let v = profile
+ .resolution
+ .map(|r| r.height.to_string())
+ .unwrap_or_default();
move || v
});
let memory = use_state({
@@ -165,7 +171,11 @@ fn resolution_field(width: State<String>, height: State<String>) -> impl IntoEle
.width(Size::px(70.))
.text_align(TextAlign::Center),
)
- .child(Icon::new(IconType::X).size(14.).color(colors::fg_secondary()))
+ .child(
+ Icon::new(IconType::X)
+ .size(14.)
+ .color(colors::fg_secondary()),
+ )
.child(
TextInput::new(height)
.placeholder("480")
diff --git a/packages/oneclient_app/src/view/app/settings/mod.rs b/packages/oneclient_app/src/view/app/settings/mod.rs
index 88e39f5..16a2434 100644
--- a/packages/oneclient_app/src/view/app/settings/mod.rs
+++ b/packages/oneclient_app/src/view/app/settings/mod.rs
@@ -15,14 +15,14 @@ pub use apis::SettingsApis;
pub use changelog::SettingsChangelog;
pub use developer::SettingsDeveloper;
// pub use feedback::SettingsFeedback;
-pub use java::SettingsJava;
pub use appearance::SettingsAppearance;
+pub use java::SettingsJava;
pub use language::SettingsLanguage;
pub use launcher::SettingsLauncher;
pub use minecraft::SettingsMinecraft;
use crate::{
- components::{IconType, Icon},
+ components::{Icon, IconType},
theme::colors,
};
diff --git a/packages/oneclient_app/src/view/app/skins.rs b/packages/oneclient_app/src/view/app/skins.rs
index 121faa4..b9f4b39 100644
--- a/packages/oneclient_app/src/view/app/skins.rs
+++ b/packages/oneclient_app/src/view/app/skins.rs
@@ -1,4 +1,3 @@
-
use freya::prelude::*;
use crate::components::{Button, ButtonVariant, Icon, IconType, PlayerModel};
@@ -120,7 +119,11 @@ fn side_panel() -> impl IntoElement {
fn action_button(text: &'static str, icon: IconType, primary: bool) -> impl IntoElement {
Button::new()
- .variant(if primary { ButtonVariant::Primary } else { ButtonVariant::Secondary })
+ .variant(if primary {
+ ButtonVariant::Primary
+ } else {
+ ButtonVariant::Secondary
+ })
.child(Icon::new(icon).size(16.).color(colors::fg_primary()))
.text(text)
.into_element()
diff --git a/packages/oneclient_app/src/view/mod.rs b/packages/oneclient_app/src/view/mod.rs
index cf49c6a..6714e42 100644
--- a/packages/oneclient_app/src/view/mod.rs
+++ b/packages/oneclient_app/src/view/mod.rs
@@ -1,8 +1,8 @@
mod not_found;
mod startup;
-pub mod onboarding;
pub mod app;
+pub mod onboarding;
pub use not_found::NotFound;
-pub use startup::Startup;
\ No newline at end of file
+pub use startup::Startup;
diff --git a/packages/oneclient_app/src/view/not_found.rs b/packages/oneclient_app/src/view/not_found.rs
index 4edd701..8dfac53 100644
--- a/packages/oneclient_app/src/view/not_found.rs
+++ b/packages/oneclient_app/src/view/not_found.rs
@@ -32,4 +32,4 @@ impl Component for NotFound {
.color(theme::colors::fg_secondary()),
)
}
-}
\ No newline at end of file
+}
diff --git a/packages/oneclient_app/src/view/onboarding/account.rs b/packages/oneclient_app/src/view/onboarding/account.rs
index 707bd39..7298282 100644
--- a/packages/oneclient_app/src/view/onboarding/account.rs
+++ b/packages/oneclient_app/src/view/onboarding/account.rs
@@ -125,12 +125,7 @@ fn sign_in_card(
.size(13.)
.color(colors::danger()),
)
- .child(
- label()
- .text(message)
- .font_size(12.)
- .color(colors::danger()),
- )
+ .child(label().text(message).font_size(12.).color(colors::danger()))
.into_element()
}))
.into_element()
diff --git a/packages/oneclient_app/src/view/onboarding/bundles/card.rs b/packages/oneclient_app/src/view/onboarding/bundles/card.rs
index 8398ecf..4263ad2 100644
--- a/packages/oneclient_app/src/view/onboarding/bundles/card.rs
+++ b/packages/oneclient_app/src/view/onboarding/bundles/card.rs
@@ -2,9 +2,7 @@ use super::*;
use oneclient_core::clusters::Cluster;
-use crate::components::{
- Button, DynamicArt, Icon, IconType,
-};
+use crate::components::{Button, DynamicArt, Icon, IconType};
use crate::theme::colors;
use crate::ui::border_all_color;
diff --git a/packages/oneclient_app/src/view/onboarding/bundles/mod.rs b/packages/oneclient_app/src/view/onboarding/bundles/mod.rs
index dff095c..78a1643 100644
--- a/packages/oneclient_app/src/view/onboarding/bundles/mod.rs
+++ b/packages/oneclient_app/src/view/onboarding/bundles/mod.rs
@@ -8,8 +8,7 @@ use oneclient_core::{BundleArchive, BundleFile, BundleFileKind};
use crate::components::ScrollArea;
use crate::hooks::{
- ClusterBundles, onboarding_bundles_items,
- use_onboarding_bundles, use_onboarding_selection,
+ ClusterBundles, onboarding_bundles_items, use_onboarding_bundles, use_onboarding_selection,
};
use crate::routes::Route;
use crate::theme::colors;
@@ -321,4 +320,3 @@ impl Component for OnboardingBundles {
.maybe_child(popup)
}
}
-
diff --git a/packages/oneclient_app/src/view/onboarding/bundles/popup.rs b/packages/oneclient_app/src/view/onboarding/bundles/popup.rs
index 5259193..05d888d 100644
--- a/packages/oneclient_app/src/view/onboarding/bundles/popup.rs
+++ b/packages/oneclient_app/src/view/onboarding/bundles/popup.rs
@@ -6,12 +6,8 @@ use freya::query::QueryStateData;
use oneclient_core::packages::{CachedPackageMeta, ProviderId};
use oneclient_core::{BundleArchive, BundleFileKind};
-use crate::components::{
- Button, Icon, OverlayPopup, ScrollArea, TabBar, TabItem,
-};
-use crate::hooks::{
- package_meta_batch, use_cached_image, use_package_meta_batch,
-};
+use crate::components::{Button, Icon, OverlayPopup, ScrollArea, TabBar, TabItem};
+use crate::hooks::{package_meta_batch, use_cached_image, use_package_meta_batch};
use crate::theme::colors;
use crate::ui::border_all_color;
use crate::utils::format_size;
diff --git a/packages/oneclient_app/src/view/onboarding/downloading/summary.rs b/packages/oneclient_app/src/view/onboarding/downloading/summary.rs
index 5e9dcc9..ff5d2c9 100644
--- a/packages/oneclient_app/src/view/onboarding/downloading/summary.rs
+++ b/packages/oneclient_app/src/view/onboarding/downloading/summary.rs
@@ -46,7 +46,10 @@ pub(super) fn summary_view(
let migration_section: Element = match migration {
Some((source, folder, target)) => {
- let mut rows = vec![summary_line("From", &source), summary_line("Files", &folder)];
+ let mut rows = vec![
+ summary_line("From", &source),
+ summary_line("Files", &folder),
+ ];
if !target.is_empty() {
rows.push(summary_line("Destination", &target));
}
diff --git a/packages/oneclient_app/src/view/onboarding/downloading/tips.rs b/packages/oneclient_app/src/view/onboarding/downloading/tips.rs
index 8025652..75b15ad 100644
--- a/packages/oneclient_app/src/view/onboarding/downloading/tips.rs
+++ b/packages/oneclient_app/src/view/onboarding/downloading/tips.rs
@@ -50,7 +50,14 @@ pub(super) fn use_onboarding_tip() -> String {
let Ok(state) = oneclient_core::LauncherState::get() else {
return;
};
- match state.services.requester.http().get(FUNFACTS_URL).send().await {
+ 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);
diff --git a/packages/oneclient_app/src/view/onboarding/language.rs b/packages/oneclient_app/src/view/onboarding/language.rs
index c4bb80a..7166f53 100644
--- a/packages/oneclient_app/src/view/onboarding/language.rs
+++ b/packages/oneclient_app/src/view/onboarding/language.rs
@@ -14,9 +14,10 @@ struct Language {
percentage: u32,
}
-const LANGUAGES: [Language; 1] = [
- Language { name: "English", percentage: 100 },
-];
+const LANGUAGES: [Language; 1] = [Language {
+ name: "English",
+ percentage: 100,
+}];
#[derive(PartialEq)]
pub struct OnboardingLanguage;
@@ -29,15 +30,13 @@ impl Component for OnboardingLanguage {
let back = if has_migration_data(&migration_query) {
Route::OnboardingMigration {}
} else {
- Route::OnboardingWelcome {}
+ Route::OnboardingTerms {}
};
let rows: Vec<Element> = LANGUAGES
.iter()
.enumerate()
- .map(|(index, lang)| {
- language_row(lang, index == *selected.read(), selected, language)
- })
+ .map(|(index, lang)| language_row(lang, index == *selected.read(), selected, language))
.collect();
let content = rect()
@@ -55,7 +54,13 @@ impl Component for OnboardingLanguage {
.font_weight(FontWeight::MEDIUM)
.color(colors::fg_primary()),
)
- .child(rect().vertical().width(Size::fill()).spacing(4.).children(rows))
+ .child(
+ rect()
+ .vertical()
+ .width(Size::fill())
+ .spacing(4.)
+ .children(rows),
+ )
.into_element();
onboarding_page(
@@ -92,7 +97,9 @@ fn language_row(
.padding(Gaps::new_symmetric(14., 20.))
.corner_radius(CornerRadius::new_all(10.))
.background(background)
- .maybe(active, |el| el.border(border_all_color(1., colors::brand())))
+ .maybe(active, |el| {
+ el.border(border_all_color(1., colors::brand()))
+ })
.a11y_role(AccessibilityRole::Button)
.on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
.on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
diff --git a/packages/oneclient_app/src/view/onboarding/migration.rs b/packages/oneclient_app/src/view/onboarding/migration.rs
index aca061c..f457a13 100644
--- a/packages/oneclient_app/src/view/onboarding/migration.rs
+++ b/packages/oneclient_app/src/view/onboarding/migration.rs
@@ -27,9 +27,10 @@ fn build_migrated_selection(source: &[SourceInstance], new: &[ClusterBundles]) -
let mut selected = HashSet::new();
for cb in new {
- let Some(instance) = source.iter().find(|o| {
- o.mc_version == cb.cluster.mc_version && o.mc_loader == cb.cluster.mc_loader
- }) else {
+ let Some(instance) = source
+ .iter()
+ .find(|o| o.mc_version == cb.cluster.mc_version && o.mc_loader == cb.cluster.mc_loader)
+ else {
continue;
};
@@ -89,7 +90,10 @@ impl Component for OnboardingMigration {
// No detection yet -> render nothing meaningful; the shell only routes
// here when a source was detected, so this is a transient loading state.
let Some(detection) = migration_detection(&migration_query) else {
- return rect().width(Size::fill()).height(Size::fill()).into_element();
+ return rect()
+ .width(Size::fill())
+ .height(Size::fill())
+ .into_element();
};
let source_name = detection.source.display_name();
let new_clusters = onboarding_bundles_items(&bundles_query).unwrap_or_default();
@@ -103,8 +107,7 @@ impl Component for OnboardingMigration {
let mut version_cards: Vec<Element> = Vec::new();
for instance in &detection.instances {
let selected_import = chosen_folder.as_deref() == Some(instance.folder_name.as_str());
- let dedicated_available =
- matching_new_cluster_id(instance, &new_clusters).is_some();
+ let dedicated_available = matching_new_cluster_id(instance, &new_clusters).is_some();
version_cards.push(version_card(
instance,
@@ -259,26 +262,16 @@ fn version_card(
.font_weight(FontWeight::MEDIUM)
.color(colors::fg_primary()),
)
- .child(
- label()
- .text(subtitle)
- .font_size(11.)
- .color(if selected {
- colors::fg_primary()
- } else {
- colors::fg_secondary()
- }),
- )
- .child(
- label()
- .text(categories)
- .font_size(11.)
- .color(if selected {
- colors::fg_primary()
- } else {
- colors::fg_secondary()
- }),
- ),
+ .child(label().text(subtitle).font_size(11.).color(if selected {
+ colors::fg_primary()
+ } else {
+ colors::fg_secondary()
+ }))
+ .child(label().text(categories).font_size(11.).color(if selected {
+ colors::fg_primary()
+ } else {
+ colors::fg_secondary()
+ })),
)
// Selection affordance only when the instance actually has files.
.maybe_child(importable.then(|| {
@@ -471,7 +464,7 @@ fn migration_nav(next_enabled: bool, on_next: impl FnMut() + 'static) -> impl In
.secondary()
.width(Size::px(128.))
.on_press(move |_| {
- let _ = RouterContext::get().replace(Route::OnboardingWelcome {});
+ let _ = RouterContext::get().replace(Route::OnboardingTerms {});
})
.text("Back"),
)
diff --git a/packages/oneclient_app/src/view/onboarding/mod.rs b/packages/oneclient_app/src/view/onboarding/mod.rs
index d9e14f9..b7403f1 100644
--- a/packages/oneclient_app/src/view/onboarding/mod.rs
+++ b/packages/oneclient_app/src/view/onboarding/mod.rs
@@ -4,6 +4,7 @@ mod downloading;
mod language;
mod migration;
mod preferences;
+mod terms;
mod welcome;
pub use account::OnboardingAccount;
@@ -13,6 +14,7 @@ pub use language::OnboardingLanguage;
pub use migration::OnboardingMigration;
pub(crate) use migration::matching_new_cluster_id;
pub use preferences::OnboardingPreferences;
+pub use terms::OnboardingTerms;
pub use welcome::OnboardingWelcome;
use std::sync::atomic::{AtomicUsize, Ordering};
@@ -30,7 +32,7 @@ use crate::ui::{border_all_color, entrance_motion_layer};
/// Number of onboarding steps. The v1-migration step only exists (and is only
/// counted) when old launcher data was detected.
pub fn onboarding_total(has_migration: bool) -> usize {
- if has_migration { 7 } else { 6 }
+ if has_migration { 8 } else { 7 }
}
pub fn onboarding_step_index(route: &Route, has_migration: bool) -> usize {
@@ -38,12 +40,13 @@ pub fn onboarding_step_index(route: &Route, has_migration: bool) -> usize {
match route {
Route::OnboardingWelcome {} => 0,
- Route::OnboardingMigration {} => 1,
- Route::OnboardingLanguage {} => 1 + shift,
- Route::OnboardingAccount {} => 2 + shift,
- Route::OnboardingBundles {} => 3 + shift,
- Route::OnboardingPreferences {} => 4 + shift,
- Route::OnboardingDownloading {} => 5 + shift,
+ Route::OnboardingTerms {} => 1,
+ Route::OnboardingMigration {} => 2,
+ Route::OnboardingLanguage {} => 2 + shift,
+ Route::OnboardingAccount {} => 3 + shift,
+ Route::OnboardingBundles {} => 4 + shift,
+ Route::OnboardingPreferences {} => 5 + shift,
+ Route::OnboardingDownloading {} => 6 + shift,
_ => 0,
}
}
@@ -80,8 +83,16 @@ pub(crate) fn onboarding_slide(content: impl IntoElement) -> impl IntoElement {
}
});
- let p = if anim_finished { 1.0 } else { anim.get().value() };
- let slide_x = if motion { direction * (1. - p) * SLIDE_DISTANCE } else { 0. };
+ let p = if anim_finished {
+ 1.0
+ } else {
+ anim.get().value()
+ };
+ let slide_x = if motion {
+ direction * (1. - p) * SLIDE_DISTANCE
+ } else {
+ 0.
+ };
let opacity = if motion && !anim_finished { p } else { 1. };
rect()
@@ -91,7 +102,11 @@ pub(crate) fn onboarding_slide(content: impl IntoElement) -> impl IntoElement {
.into_element()
}
-pub(crate) fn onboarding_page(illustration: impl IntoElement, content: impl IntoElement, nav: impl IntoElement) -> impl IntoElement {
+pub(crate) fn onboarding_page(
+ illustration: impl IntoElement,
+ content: impl IntoElement,
+ nav: impl IntoElement,
+) -> impl IntoElement {
rect()
.vertical()
.width(Size::fill())
@@ -130,7 +145,11 @@ pub(crate) fn onboarding_illustration(icon: IconType) -> impl IntoElement {
Icon::new(icon).size(240.).color(Color::WHITE)
}
-pub(crate) fn onboarding_nav(back: Option<Route>, next: Route, next_enabled: bool) -> impl IntoElement {
+pub(crate) fn onboarding_nav(
+ back: Option<Route>,
+ next: Route,
+ next_enabled: bool,
+) -> impl IntoElement {
rect()
.horizontal()
.width(Size::fill())
diff --git a/packages/oneclient_app/src/view/onboarding/terms.rs b/packages/oneclient_app/src/view/onboarding/terms.rs
new file mode 100644
index 0000000..7b4d746
--- /dev/null
+++ b/packages/oneclient_app/src/view/onboarding/terms.rs
@@ -0,0 +1,304 @@
+use freya::prelude::*;
+use freya::query::UseQuery;
+use freya::router::RouterContext;
+
+use crate::components::{Button, Icon, IconType, ScrollArea, Segment, SegmentedControl, toggle};
+use crate::hooks::{
+ TermsQuery, has_migration_data, terms_document, terms_error, terms_is_loading, use_dispatch,
+ use_migration, use_settings_snapshot, use_terms,
+};
+use crate::platform::open_url;
+use crate::routes::Route;
+use crate::theme::colors;
+use crate::ui::border_all_color;
+use crate::view::onboarding::{onboarding_illustration, onboarding_page, step_heading};
+
+#[derive(Clone, Copy, PartialEq)]
+enum LegalTab {
+ Terms,
+ Privacy,
+}
+
+#[derive(PartialEq)]
+pub struct OnboardingTerms;
+
+impl Component for OnboardingTerms {
+ fn render(&self) -> impl IntoElement {
+ let settings = use_settings_snapshot().settings;
+ let dispatch = use_dispatch();
+ let query = use_terms();
+ let migration_query = use_migration();
+
+ let document = terms_document(&query);
+ let error = terms_error(&query);
+ let loading = terms_is_loading(&query);
+
+ let accepted = use_state(|| false);
+ let tab = use_state(|| LegalTab::Terms);
+
+ let returning = settings.seen_onboarding;
+ let next = if returning {
+ Route::Home {}
+ } else if has_migration_data(&migration_query) {
+ Route::OnboardingMigration {}
+ } else {
+ Route::OnboardingLanguage {}
+ };
+ let back = (!returning).then_some(Route::OnboardingWelcome {});
+
+ let terms_version = document.as_ref().map(|doc| doc.version).unwrap_or(1);
+ let privacy_version = document
+ .as_ref()
+ .map(|doc| doc.privacy_version())
+ .unwrap_or(1);
+ let terms_url = document
+ .as_ref()
+ .map(|doc| doc.terms_url().to_string())
+ .unwrap_or_else(|| oneclient_core::constants::TOS_URL.to_string());
+ let privacy_url = document
+ .as_ref()
+ .map(|doc| doc.privacy_url().to_string())
+ .unwrap_or_else(|| oneclient_core::constants::PRIVACY_URL.to_string());
+
+ let privacy_body = document.as_ref().and_then(|doc| doc.privacy_body());
+ let tabs = privacy_body.is_some().then(|| tab_switcher(tab));
+
+ let body = if loading {
+ loading_body()
+ } else if let Some(document) = document.as_ref() {
+ match (*tab.read(), privacy_body) {
+ (LegalTab::Privacy, Some(privacy)) => markdown_body(privacy),
+ _ => markdown_body(&document.terms),
+ }
+ } else {
+ fallback_body(error.as_deref(), query)
+ };
+
+ let content = rect()
+ .vertical()
+ .width(Size::fill())
+ .spacing(16.)
+ .child(step_heading(
+ "Terms & Privacy",
+ "Please read and accept these before continuing.",
+ ))
+ .maybe_child(tabs)
+ .child(body)
+ .child(link_row(terms_url, privacy_url))
+ .child(accept_row(accepted))
+ .into_element();
+
+ onboarding_page(
+ onboarding_illustration(IconType::File02),
+ content,
+ terms_nav(back, *accepted.read() && !loading, move || {
+ dispatch.accept_tos(terms_version, privacy_version);
+ let _ = RouterContext::get().replace(next.clone());
+ }),
+ )
+ }
+}
+
+fn panel(child: impl IntoElement) -> impl IntoElement {
+ rect()
+ .vertical()
+ .width(Size::fill())
+ .height(Size::px(220.))
+ .content(Content::Flex)
+ .corner_radius(CornerRadius::new_all(12.))
+ .background(colors::page_elevated())
+ .border(border_all_color(1., colors::component_border()))
+ .child(child)
+}
+
+fn loading_body() -> Element {
+ panel(
+ rect()
+ .width(Size::fill())
+ .height(Size::fill())
+ .center()
+ .child(
+ label()
+ .text("Loading the Terms of Service and Privacy Policy...")
+ .font_size(13.)
+ .color(colors::fg_secondary()),
+ ),
+ )
+ .into_element()
+}
+
+fn tab_switcher(tab: State<LegalTab>) -> Element {
+ SegmentedControl::new(tab)
+ .segment(Segment::new(LegalTab::Terms).label("Terms"))
+ .segment(Segment::new(LegalTab::Privacy).label("Privacy"))
+ .into_element()
+}
+
+fn markdown_body(markdown: &str) -> Element {
+ panel(
+ ScrollArea::new()
+ .width(Size::fill())
+ .height(Size::flex(1.0))
+ .padding(Gaps::new_all(16.))
+ .child(
+ MarkdownViewer::new(markdown.to_string())
+ .width(Size::fill())
+ .color(colors::fg_primary())
+ .color_link(colors::code_info())
+ .background_code(colors::component_bg())
+ .color_code(colors::fg_primary())
+ .background_blockquote(colors::component_bg())
+ .border_blockquote(colors::brand())
+ .background_divider(colors::component_border())
+ .heading_h1(20.)
+ .heading_h2(17.)
+ .heading_h3(15.)
+ .heading_h4(14.)
+ .heading_h5(13.)
+ .heading_h6(12.)
+ .paragraph_size(13.)
+ .code_font_size(12.),
+ ),
+ )
+ .into_element()
+}
+
+fn fallback_body(error: Option<&str>, query: UseQuery<TermsQuery>) -> Element {
+ panel(
+ rect()
+ .vertical()
+ .width(Size::fill())
+ .height(Size::fill())
+ .center()
+ .spacing(10.)
+ .padding(Gaps::new_all(20.))
+ .child(
+ Icon::new(IconType::AlertTriangle)
+ .size(22.)
+ .color(colors::code_warn()),
+ )
+ .child(
+ label()
+ .text("Couldn't load the Terms of Service or Privacy Policy.")
+ .font_size(14.)
+ .font_weight(FontWeight::MEDIUM)
+ .color(colors::fg_primary()),
+ )
+ .child(
+ label()
+ .text(
+ "Open them in your browser using the links below, or try again once \
+ you're back online.",
+ )
+ .font_size(12.)
+ .text_align(TextAlign::Center)
+ .color(colors::fg_secondary()),
+ )
+ .maybe_child(error.map(|err| {
+ label()
+ .text(err.to_string())
+ .font_size(11.)
+ .max_lines(2)
+ .color(colors::fg_secondary().with_a(160))
+ }))
+ .child(
+ Button::new()
+ .secondary()
+ .small()
+ .on_press(move |_| {
+ query.invalidate();
+ })
+ .text("Retry")
+ .child(Icon::new(IconType::RefreshCw01).size(14.)),
+ ),
+ )
+ .into_element()
+}
+
+fn link_row(terms_url: String, privacy_url: String) -> impl IntoElement {
+ rect()
+ .horizontal()
+ .width(Size::fill())
+ .spacing(10.)
+ .child(external_link_button("Terms of Service", terms_url))
+ .child(external_link_button("Privacy Policy", privacy_url))
+ .into_element()
+}
+
+fn external_link_button(text: &'static str, url: String) -> impl IntoElement {
+ Button::new()
+ .secondary()
+ .small()
+ .on_press(move |_| open_url(&url))
+ .text(text)
+ .child(Icon::new(IconType::LinkExternal01).size(14.))
+}
+
+fn accept_row(accepted: State<bool>) -> impl IntoElement {
+ rect()
+ .horizontal()
+ .width(Size::fill())
+ .cross_align(Alignment::Center)
+ .spacing(16.)
+ .content(Content::Flex)
+ .padding(Gaps::new_symmetric(12., 16.))
+ .corner_radius(CornerRadius::new_all(12.))
+ .background(colors::page_elevated())
+ .border(border_all_color(1., colors::component_border()))
+ .child(
+ rect()
+ .vertical()
+ .width(Size::flex(1.0))
+ .spacing(3.)
+ .child(
+ label()
+ .text("I accept the Terms of Service and Privacy Policy")
+ .font_size(14.)
+ .font_weight(FontWeight::MEDIUM)
+ .color(colors::fg_primary()),
+ )
+ .child(
+ label()
+ .text("Required to use OneClient.")
+ .font_size(11.)
+ .color(colors::fg_secondary()),
+ ),
+ )
+ .child(toggle(accepted))
+ .into_element()
+}
+
+fn terms_nav(
+ back: Option<Route>,
+ next_enabled: bool,
+ on_next: impl FnMut() + 'static,
+) -> impl IntoElement {
+ let mut on_next = on_next;
+ rect()
+ .horizontal()
+ .width(Size::fill())
+ .main_align(Alignment::End)
+ .cross_align(Alignment::Center)
+ .spacing(12.)
+ .padding(Gaps::new(0., 40., 32., 40.))
+ .maybe_child(back.map(|route| {
+ Button::new()
+ .secondary()
+ .width(Size::px(128.))
+ .on_press(move |_| {
+ let _ = RouterContext::get().replace(route.clone());
+ })
+ .text("Back")
+ .into_element()
+ }))
+ .child(
+ Button::new()
+ .primary()
+ .width(Size::px(140.))
+ .enabled(next_enabled)
+ .on_press(move |_| on_next())
+ .text("Next")
+ .child(Icon::new(IconType::ArrowRight).size(16.)),
+ )
+ .into_element()
+}
diff --git a/packages/oneclient_app/src/view/onboarding/welcome.rs b/packages/oneclient_app/src/view/onboarding/welcome.rs
index 325c883..37ad66e 100644
--- a/packages/oneclient_app/src/view/onboarding/welcome.rs
+++ b/packages/oneclient_app/src/view/onboarding/welcome.rs
@@ -1,7 +1,6 @@
use freya::prelude::*;
use crate::components::IconType;
-use crate::hooks::{has_migration_data, use_migration};
use crate::routes::Route;
use crate::theme::colors;
use crate::view::onboarding::{
@@ -13,14 +12,6 @@ pub struct OnboardingWelcome;
impl Component for OnboardingWelcome {
fn render(&self) -> impl IntoElement {
- let migration_query = use_migration();
-
- let next = if has_migration_data(&migration_query) {
- Route::OnboardingMigration {}
- } else {
- Route::OnboardingLanguage {}
- };
-
let content = rect()
.vertical()
.width(Size::fill())
@@ -40,7 +31,7 @@ impl Component for OnboardingWelcome {
onboarding_page(
onboarding_illustration(IconType::OnboardingWelcome),
content,
- onboarding_nav(None, next, true),
+ onboarding_nav(None, Route::OnboardingTerms {}, true),
)
}
}
diff --git a/packages/oneclient_app/src/view/startup.rs b/packages/oneclient_app/src/view/startup.rs
index e5540bf..db41121 100644
--- a/packages/oneclient_app/src/view/startup.rs
+++ b/packages/oneclient_app/src/view/startup.rs
@@ -3,7 +3,10 @@ use freya::{prelude::*, router::*};
use crate::AppAssets;
use crate::Route;
-use crate::hooks::{use_launcher, use_notifications_snapshot, use_settings_snapshot};
+use crate::hooks::{
+ terms_document, terms_is_loading, use_launcher, use_notifications_snapshot,
+ use_settings_snapshot, use_terms,
+};
use crate::theme::colors;
#[derive(PartialEq)]
@@ -14,6 +17,7 @@ impl Component for Startup {
let launcher = use_launcher();
let settings = use_settings_snapshot();
let notifications = use_notifications_snapshot();
+ let terms = use_terms();
let intro = use_animation(|conf| {
conf.on_creation(OnCreation::Run);
@@ -34,37 +38,49 @@ impl Component for Startup {
let logo = use_memo(|| AppAssets::get_bytes("logo.svg").unwrap_or_default());
- if launcher.ready && !launcher.fetching {
- let destination = if settings.settings.seen_onboarding {
- Route::Home {}
- } else {
+ if launcher.ready && !launcher.fetching && !terms_is_loading(&terms) {
+ let document = terms_document(&terms);
+ let required_terms = document.as_ref().map(|doc| doc.version).unwrap_or(1);
+ let required_privacy = document
+ .as_ref()
+ .map(|doc| doc.privacy_version())
+ .unwrap_or(1);
+
+ let stale = settings.settings.accepted_tos_version < required_terms
+ || settings.settings.accepted_privacy_version < required_privacy;
+
+ let destination = if !settings.settings.seen_onboarding {
Route::OnboardingWelcome {}
+ } else if stale {
+ Route::OnboardingTerms {}
+ } else {
+ Route::Home {}
};
let _ = RouterContext::get().replace(destination);
return rect().into_element();
}
- let active = notifications
- .inbox
- .iter()
- .find(|entry| entry.is_loading);
-
- let (message, detail): (String, Option<String>) = if let Some(err) = launcher.error.as_deref() {
- ("Couldn't start OneClient".to_string(), Some(err.to_string()))
-
- } else if let Some(entry) = active {
- let detail = entry
- .tasks
- .iter()
- .find(|t| t.total == 0 || t.current < t.total)
- .map(|t| t.label.clone())
- .or_else(|| entry.progress.map(|(c, t)| format!("{c} / {t}")));
- (entry.title.clone(), detail)
- } else if launcher.ready {
- ("Fetching versions and bundles...".to_string(), None)
- } else {
- ("Starting OneClient...".to_string(), None)
- };
+ let active = notifications.inbox.iter().find(|entry| entry.is_loading);
+
+ let (message, detail): (String, Option<String>) =
+ if let Some(err) = launcher.error.as_deref() {
+ (
+ "Couldn't start OneClient".to_string(),
+ Some(err.to_string()),
+ )
+ } else if let Some(entry) = active {
+ let detail = entry
+ .tasks
+ .iter()
+ .find(|t| t.total == 0 || t.current < t.total)
+ .map(|t| t.label.clone())
+ .or_else(|| entry.progress.map(|(c, t)| format!("{c} / {t}")));
+ (entry.title.clone(), detail)
+ } else if launcher.ready {
+ ("Fetching versions and bundles...".to_string(), None)
+ } else {
+ ("Starting OneClient...".to_string(), None)
+ };
let is_error = launcher.error.is_some();
diff --git a/packages/oneclient_core/Cargo.toml b/packages/oneclient_core/Cargo.toml
index 89cfd4d..e788dd2 100644
--- a/packages/oneclient_core/Cargo.toml
+++ b/packages/oneclient_core/Cargo.toml
@@ -56,9 +56,17 @@ path = "examples/launch_game.rs"
name = "notifications"
path = "examples/notifications.rs"
+[[example]]
+name = "discord_rpc"
+path = "examples/discord_rpc.rs"
+
+[dev-dependencies]
+async-tungstenite = { version = "=0.32.1", features = ["tokio-runtime"] }
+
[dependencies]
# core (extra)
# gamemode_rs.workspace = true
+discord-rich-presence.workspace = true
interfrost.workspace = true
oneclient_db.workspace = true
polyio.workspace = true
@@ -68,6 +76,9 @@ tracing.workspace = true
tracing-futures.workspace = true
tracing-subscriber.workspace = true
+sentry.workspace = true
+sentry-tracing.workspace = true
+
# database
sqlx.workspace = true
@@ -76,9 +87,10 @@ async-trait.workspace = true
tokio.workspace = true
tokio-stream.workspace = true
tokio-util.workspace = true
-futures-util = { workspace = true, features = ["std"] }
+futures-util = { workspace = true, features = ["std", "sink"] }
futures-lite.workspace = true
reqwest.workspace = true
+reqwest-websocket.workspace = true
async_zip.workspace = true
async-tempfile.workspace = true
async-stream.workspace = true
diff --git a/packages/oneclient_core/examples/discord_rpc.rs b/packages/oneclient_core/examples/discord_rpc.rs
new file mode 100644
index 0000000..b2fa726
--- /dev/null
+++ b/packages/oneclient_core/examples/discord_rpc.rs
@@ -0,0 +1,33 @@
+use std::time::Duration;
+
+use oneclient_core::discord::{DiscordRpc, Presence};
+
+fn main() {
+ tracing_subscriber::fmt()
+ .with_max_level(tracing::Level::DEBUG)
+ .init();
+
+ let rpc = DiscordRpc::spawn(true);
+
+ println!("idle presence for 10s...");
+ std::thread::sleep(Duration::from_secs(10));
+
+ println!("playing presence for 10s...");
+ rpc.set_presence(Presence::Playing {
+ cluster: "Example Cluster".to_owned(),
+ mc_version: "1.8.9".to_owned(),
+ });
+ std::thread::sleep(Duration::from_secs(10));
+
+ println!("disabling for 5s...");
+ rpc.set_enabled(false);
+ std::thread::sleep(Duration::from_secs(5));
+
+ println!("re-enabling for 10s...");
+ rpc.set_enabled(true);
+ std::thread::sleep(Duration::from_secs(10));
+
+ println!("shutting down");
+ rpc.shutdown();
+ std::thread::sleep(Duration::from_secs(1));
+}
diff --git a/packages/oneclient_core/src/constants.rs b/packages/oneclient_core/src/constants.rs
index ee2411d..6d01a60 100644
--- a/packages/oneclient_core/src/constants.rs
+++ b/packages/oneclient_core/src/constants.rs
@@ -16,6 +16,14 @@ pub const MCLOGS_API_URL: &str = "https://api.mclo.gs/1";
pub const SKYCLIENT_BASE_URL: &str =
"https://raw.githubusercontent.com/SkyblockClient/SkyblockClient-REPO/refs/heads/main/v1";
pub const META_URL_BASE: &str = "https://data-v2.polyfrost.org";
+pub const TOS_URL: &str = "https://polyfrost.org/legal/terms";
+pub const PRIVACY_URL: &str = "https://polyfrost.org/legal/privacy";
+pub const PLUS_BACKEND_URL: &str = "https://plus.polyfrost.org";
+
+pub const SENTRY_DSN: &str = match option_env!("ONECLIENT_SENTRY_DSN") {
+ Some(dsn) => dsn,
+ None => "https://e7dff7e07427e1a28b9212cfcc8ddc1e@o4511714343124992.ingest.us.sentry.io/4511714354135040",
+};
pub const TARGET_OS: &str = cfg_select! {
target_os = "windows" => "windows",
diff --git a/packages/oneclient_core/src/crypto.rs b/packages/oneclient_core/src/crypto.rs
index 52f04cd..8bc68f5 100644
--- a/packages/oneclient_core/src/crypto.rs
+++ b/packages/oneclient_core/src/crypto.rs
@@ -33,7 +33,7 @@ pub fn normalize_hash(hash: &str) -> String {
hash.trim().to_ascii_lowercase()
}
-fn to_hex(data: &[u8]) -> String {
+pub(crate) fn to_hex(data: &[u8]) -> String {
data.iter().fold(String::new(), |mut out, b| {
let _ = write!(out, "{b:02x}");
out
diff --git a/packages/oneclient_core/src/dev.rs b/packages/oneclient_core/src/dev.rs
index 3e6595d..97dbffa 100644
--- a/packages/oneclient_core/src/dev.rs
+++ b/packages/oneclient_core/src/dev.rs
@@ -239,6 +239,7 @@ pub async fn ephemeral_state() -> LauncherResult<Arc<LauncherState>> {
versions: Arc::new(crate::versions::VersionsManager::new()),
images: crate::images::ImageCacheStore::new(),
games: crate::game::GameProcessManager::new(),
+ discord: crate::discord::DiscordRpc::spawn(false),
}))
}
diff --git a/packages/oneclient_core/src/discord.rs b/packages/oneclient_core/src/discord.rs
new file mode 100644
index 0000000..ac18739
--- /dev/null
+++ b/packages/oneclient_core/src/discord.rs
@@ -0,0 +1,226 @@
+use std::sync::mpsc::{self, RecvTimeoutError, Sender};
+use std::time::{Duration, Instant};
+
+use discord_rich_presence::activity::{Activity, Assets, Button, Timestamps};
+use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
+
+use crate::constants::DISCORD_CLIENT_ID;
+
+const RECONNECT_BACKOFF: Duration = Duration::from_secs(60);
+
+const IDLE_POLL: Duration = Duration::from_secs(3600);
+
+const LARGE_IMAGE: &str = "onelauncher_logo_512";
+
+#[derive(Debug, Clone, PartialEq, Eq, Default)]
+pub enum Presence {
+ #[default]
+ Idle,
+ Playing {
+ cluster: String,
+ mc_version: String,
+ },
+}
+
+impl Presence {
+ fn details(&self) -> &'static str {
+ match self {
+ Self::Idle => "Idle",
+ Self::Playing { .. } => "Playing Minecraft",
+ }
+ }
+
+ fn state(&self) -> String {
+ match self {
+ Self::Idle => "In the launcher".to_owned(),
+ Self::Playing {
+ cluster,
+ mc_version,
+ } => format!("{cluster} ({mc_version})"),
+ }
+ }
+}
+
+enum Command {
+ SetPresence(Presence),
+ SetEnabled(bool),
+ Shutdown,
+}
+
+#[derive(Clone)]
+pub struct DiscordRpc {
+ tx: Sender<Command>,
+}
+
+impl DiscordRpc {
+ pub fn spawn(enabled: bool) -> Self {
+ let (tx, rx) = mpsc::channel();
+
+ std::thread::Builder::new()
+ .name("discord-rpc".to_owned())
+ .spawn(move || Worker::new(enabled).run(&rx))
+ .expect("failed to spawn the Discord RPC thread");
+
+ Self { tx }
+ }
+
+ pub fn set_presence(&self, presence: Presence) {
+ self.send(Command::SetPresence(presence));
+ }
+
+ pub fn set_enabled(&self, enabled: bool) {
+ self.send(Command::SetEnabled(enabled));
+ }
+
+ pub fn shutdown(&self) {
+ self.send(Command::Shutdown);
+ }
+
+ fn send(&self, command: Command) {
+ if self.tx.send(command).is_err() {
+ tracing::debug!("Discord RPC worker is gone; dropping command");
+ }
+ }
+}
+
+struct Worker {
+ client: DiscordIpcClient,
+ enabled: bool,
+ connected: bool,
+ ever_connected: bool,
+ presence: Presence,
+ presence_since: i64,
+ next_attempt: Instant,
+}
+
+impl Worker {
+ fn new(enabled: bool) -> Self {
+ Self {
+ client: DiscordIpcClient::new(DISCORD_CLIENT_ID),
+ enabled,
+ connected: false,
+ ever_connected: false,
+ presence: Presence::Idle,
+ presence_since: chrono::Utc::now().timestamp(),
+ next_attempt: Instant::now(),
+ }
+ }
+
+ fn run(mut self, rx: &mpsc::Receiver<Command>) {
+ self.publish();
+
+ loop {
+ match rx.recv_timeout(self.poll_interval()) {
+ Ok(Command::SetPresence(presence)) => self.set_presence(presence),
+ Ok(Command::SetEnabled(enabled)) => self.set_enabled(enabled),
+ Ok(Command::Shutdown) | Err(RecvTimeoutError::Disconnected) => break,
+ Err(RecvTimeoutError::Timeout) => self.publish(),
+ }
+ }
+
+ self.disconnect();
+ }
+
+ fn poll_interval(&self) -> Duration {
+ if !self.enabled || self.connected {
+ return IDLE_POLL;
+ }
+
+ self.next_attempt
+ .saturating_duration_since(Instant::now())
+ .max(Duration::from_millis(100))
+ }
+
+ fn set_presence(&mut self, presence: Presence) {
+ if self.presence == presence {
+ return;
+ }
+
+ self.presence = presence;
+ self.presence_since = chrono::Utc::now().timestamp();
+ self.publish();
+ }
+
+ fn set_enabled(&mut self, enabled: bool) {
+ if self.enabled == enabled {
+ return;
+ }
+
+ self.enabled = enabled;
+
+ if enabled {
+ self.next_attempt = Instant::now();
+ self.publish();
+ } else {
+ self.disconnect();
+ }
+ }
+
+ fn publish(&mut self) {
+ if !self.enabled || !self.ensure_connected() {
+ return;
+ }
+
+ let state = self.presence.state();
+ let activity = Activity::new()
+ .details(self.presence.details())
+ .state(&state)
+ .timestamps(Timestamps::new().start(self.presence_since))
+ .assets(
+ Assets::new()
+ .large_image(LARGE_IMAGE)
+ .large_text("OneClient"),
+ )
+ .buttons(vec![Button::new("Website", "https://polyfrost.org/")]);
+
+ if let Err(err) = self.client.set_activity(activity) {
+ tracing::warn!("Discord RPC set_activity failed: {err}");
+ self.drop_connection();
+ }
+ }
+
+ fn ensure_connected(&mut self) -> bool {
+ if self.connected {
+ return true;
+ }
+
+ if Instant::now() < self.next_attempt {
+ return false;
+ }
+ self.next_attempt = Instant::now() + RECONNECT_BACKOFF;
+
+ let result = if self.ever_connected {
+ self.client.reconnect()
+ } else {
+ self.client.connect()
+ };
+
+ match result {
+ Ok(()) => {
+ tracing::info!("Discord RPC connected");
+ self.connected = true;
+ self.ever_connected = true;
+ true
+ }
+ Err(err) => {
+ tracing::debug!("Discord RPC connect failed (is Discord running?): {err}");
+ false
+ }
+ }
+ }
+
+ fn drop_connection(&mut self) {
+ self.connected = false;
+ self.next_attempt = Instant::now() + RECONNECT_BACKOFF;
+ }
+
+ fn disconnect(&mut self) {
+ if !self.connected {
+ return;
+ }
+
+ let _ = self.client.clear_activity();
+ let _ = self.client.close();
+ self.connected = false;
+ }
+}
diff --git a/packages/oneclient_core/src/game/launch.rs b/packages/oneclient_core/src/game/launch.rs
index 08cbe32..cc0b801 100644
--- a/packages/oneclient_core/src/game/launch.rs
+++ b/packages/oneclient_core/src/game/launch.rs
@@ -12,6 +12,7 @@ use tokio::sync::Mutex as AsyncMutex;
use crate::auth::MinecraftAccount;
use crate::ClusterStage;
use crate::clusters::ClusterManager;
+use crate::discord::Presence;
use crate::game::{
GameError, arguments, download_minecraft, download_version_info, get_loader_version,
libraries_missing, resolve_minecraft_version,
@@ -298,6 +299,10 @@ pub async fn launch_cluster(
stage(LaunchStage::Running);
state.games.set_pid(cluster_id, pid);
state.games.set_dir(cluster_id, cwd.clone());
+ state.discord.set_presence(Presence::Playing {
+ cluster: cluster.name.clone(),
+ mc_version: cluster.mc_version.clone(),
+ });
let log_path = crate::logs::cluster_output_log(&cluster)?;
if let Some(parent) = log_path.parent() {
@@ -344,6 +349,10 @@ pub async fn launch_cluster(
state.games.remove(cluster_id);
state.services.notifier.game_stage(cluster_id, LaunchStage::Exited);
+ if state.games.running_ids().is_empty() {
+ state.discord.set_presence(Presence::Idle);
+ }
+
if played > Duration::from_secs(1) {
let _ = ClusterManager::add_playtime(&state, cluster_id, played).await;
}
diff --git a/packages/oneclient_core/src/game/shared_dir.rs b/packages/oneclient_core/src/game/shared_dir.rs
index 1cc5b30..b90f3ba 100644
--- a/packages/oneclient_core/src/game/shared_dir.rs
+++ b/packages/oneclient_core/src/game/shared_dir.rs
@@ -154,7 +154,7 @@ pub async fn write_allowed_symlinks(game_dir: &Path) -> LauncherResult<()> {
Ok(())
}
-const EMPTY_NOTE_NAME: &str = "PLEASE_READ_ME.txt";
+const EMPTY_NOTE_NAME: &str = "WHY_NOTHING_HERE.txt";
async fn clear_content_files(dir: &Path) {
let Ok(mut entries) = polyio::read_dir(dir).await else {
diff --git a/packages/oneclient_core/src/lib.rs b/packages/oneclient_core/src/lib.rs
index f01bb60..e10f5cb 100644
--- a/packages/oneclient_core/src/lib.rs
+++ b/packages/oneclient_core/src/lib.rs
@@ -10,6 +10,7 @@ pub mod changelog;
pub mod clusters;
pub mod constants;
pub mod crypto;
+pub mod discord;
mod error;
pub mod game;
pub mod http;
@@ -25,10 +26,13 @@ pub mod os_ext;
pub mod packages;
pub mod patch;
pub mod paths;
+pub mod plus;
pub mod recovery;
+pub mod reporting;
pub mod screenshots;
pub mod settings;
pub mod status;
+pub mod tos;
mod state;
pub mod version;
pub mod versions;
@@ -42,6 +46,8 @@ pub use bundles::{
BundleWithUpdateStatus, FileUpdateStatus, get_bundles_with_update_status,
};
pub use changelog::{fetch_changelog, parse_changelog, ChangelogGroup};
+pub use tos::{fetch_terms, TermsDocument};
+pub use discord::{DiscordRpc, Presence};
pub use clusters::{
Cluster, ClusterError, ClusterManager, ClusterStage, ClusterUpdate, CreateClusterOptions,
ensure_from_bundles, ensure_from_versions, estimate_cluster_download,
diff --git a/packages/oneclient_core/src/logger.rs b/packages/oneclient_core/src/logger.rs
index 94adf4d..7f2a554 100644
--- a/packages/oneclient_core/src/logger.rs
+++ b/packages/oneclient_core/src/logger.rs
@@ -22,11 +22,14 @@ pub fn init_filtered(filter: impl FnOnce() -> EnvFilter) -> LauncherResult<()> {
let stdout_layer = tracing_subscriber::fmt::layer().with_writer(io::stdout);
+ let sentry_layer = sentry_tracing::layer();
+
#[cfg(debug_assertions)]
{
tracing_subscriber::registry()
.with(filter)
.with(stdout_layer)
+ .with(sentry_layer)
.init();
}
@@ -50,6 +53,7 @@ pub fn init_filtered(filter: impl FnOnce() -> EnvFilter) -> LauncherResult<()> {
.with(filter)
.with(stdout_layer)
.with(file_layer)
+ .with(sentry_layer)
.init();
tracing::info!(path = %log_path.display(), "writing logs to file");
diff --git a/packages/oneclient_core/src/logs/manage.rs b/packages/oneclient_core/src/logs/manage.rs
index 1e3ae03..2b41edf 100644
--- a/packages/oneclient_core/src/logs/manage.rs
+++ b/packages/oneclient_core/src/logs/manage.rs
@@ -87,13 +87,22 @@ pub fn list_cluster_logs(cluster: &Cluster) -> LauncherResult<Vec<LogFileInfo>>
push_file(
cluster_output_log(cluster)?,
- "Launcher output".to_string(),
+ "Game output".to_string(),
LogKind::Game {
cluster_id: cluster.id,
},
&mut out,
);
+ if let Ok(logs_dir) = paths::logs_dir() {
+ collect_dir(
+ &logs_dir,
+ LogKind::Launcher,
+ |name| name.ends_with(".log") || name.ends_with(".log.gz"),
+ &mut out,
+ );
+ }
+
out.sort_by_key(|info| std::cmp::Reverse(info.modified));
Ok(out)
}
diff --git a/packages/oneclient_core/src/logs/mod.rs b/packages/oneclient_core/src/logs/mod.rs
index bd20e1d..135bf75 100644
--- a/packages/oneclient_core/src/logs/mod.rs
+++ b/packages/oneclient_core/src/logs/mod.rs
@@ -15,6 +15,7 @@ pub use parse::parse_level;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum LogKind {
Game { cluster_id: i64 },
+ Launcher,
Minecraft,
CrashReport,
Other,
diff --git a/packages/oneclient_core/src/migration/mod.rs b/packages/oneclient_core/src/migration/mod.rs
index 93970ec..9956153 100644
--- a/packages/oneclient_core/src/migration/mod.rs
+++ b/packages/oneclient_core/src/migration/mod.rs
@@ -58,7 +58,7 @@ pub struct MigrationDetection {
/// Where an imported game directory should land in *this* launcher.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ImportTarget {
- /// Copy into the shared `<launcher_dir>/minecraft`.
+ /// Copy into the shared `<launcher_dir>/.minecraft`.
Shared,
/// Copy into a specific new cluster's own folder and mark it dedicated so
/// `Cluster::game_dir()` resolves there instead of the shared dir.
diff --git a/packages/oneclient_core/src/packages/store/mod.rs b/packages/oneclient_core/src/packages/store/mod.rs
index 1291116..8e1ae8f 100644
--- a/packages/oneclient_core/src/packages/store/mod.rs
+++ b/packages/oneclient_core/src/packages/store/mod.rs
@@ -346,16 +346,18 @@ fn ensure_compatible(
return Ok(());
}
- let cluster_loader =
- GameLoader::from_repr(cluster.mc_loader as u8).unwrap_or(GameLoader::Vanilla);
-
- if !version.loaders.is_empty()
- && !version
- .loaders
- .iter()
- .any(|l| cluster_loader.compatible_with(*l))
- {
- return Err(PackageError::IncompatibleLoader.into());
+ if project.content_type == ContentType::Mod {
+ let cluster_loader =
+ GameLoader::from_repr(cluster.mc_loader as u8).unwrap_or(GameLoader::Vanilla);
+
+ if !version.loaders.is_empty()
+ && !version
+ .loaders
+ .iter()
+ .any(|l| cluster_loader.compatible_with(*l))
+ {
+ return Err(PackageError::IncompatibleLoader.into());
+ }
}
if !version.game_versions.is_empty()
@@ -369,3 +371,105 @@ fn ensure_compatible(
Ok(())
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::packages::types::PackageBody;
+ use chrono::Utc;
+
+ fn project(content_type: ContentType) -> ProjectDetail {
+ ProjectDetail {
+ id: "p".into(),
+ slug: "p".into(),
+ provider: ProviderId::Modrinth,
+ content_type,
+ name: "p".into(),
+ summary: String::new(),
+ author: String::new(),
+ members: Vec::new(),
+ gallery: Vec::new(),
+ body: PackageBody::Raw(String::new()),
+ license: None,
+ links: Vec::new(),
+ version_ids: Vec::new(),
+ game_versions: Vec::new(),
+ loaders: Vec::new(),
+ icon_url: None,
+ created: Utc::now(),
+ updated: Utc::now(),
+ downloads: 0,
+ }
+ }
+
+ fn version(loaders: Vec<GameLoader>, game_versions: Vec<&str>) -> VersionDetail {
+ VersionDetail {
+ version_id: "v".into(),
+ project_id: "p".into(),
+ name: "v".into(),
+ version_number: "1".into(),
+ changelog: None,
+ game_versions: game_versions.into_iter().map(Into::into).collect(),
+ loaders,
+ published: Utc::now(),
+ downloads: 0,
+ files: Vec::new(),
+ }
+ }
+
+ fn cluster(loader: GameLoader, mc_version: &str) -> ClusterRow {
+ ClusterRow {
+ id: 1,
+ name: "c".into(),
+ folder_name: "c".into(),
+ setting_profile_name: None,
+ mc_version: mc_version.into(),
+ mc_loader: loader as i64,
+ stage: 0,
+ mc_loader_version: None,
+ created_at: None,
+ last_played: None,
+ overall_played: None,
+ linked_modpack_hash: None,
+ }
+ }
+
+ #[test]
+ fn resource_pack_installs_into_modded_cluster() {
+ let result = ensure_compatible(
+ &project(ContentType::ResourcePack),
+ &version(vec![GameLoader::Vanilla], vec!["1.21.4"]),
+ &cluster(GameLoader::Fabric, "1.21.4"),
+ );
+
+ assert!(result.is_ok(), "resource packs are not bound to the loader");
+ }
+
+ #[test]
+ fn mod_still_rejects_incompatible_loader() {
+ let result = ensure_compatible(
+ &project(ContentType::Mod),
+ &version(vec![GameLoader::Forge], vec!["1.21.4"]),
+ &cluster(GameLoader::Fabric, "1.21.4"),
+ );
+
+ assert!(matches!(
+ result,
+ Err(LauncherError::PackageError(PackageError::IncompatibleLoader))
+ ));
+ }
+
+ #[test]
+ fn resource_pack_still_rejects_incompatible_mc_version() {
+ let result = ensure_compatible(
+ &project(ContentType::ResourcePack),
+ &version(vec![GameLoader::Vanilla], vec!["1.7.10"]),
+ &cluster(GameLoader::Fabric, "1.21.4"),
+ );
+
+ assert!(matches!(
+ result,
+ Err(LauncherError::PackageError(PackageError::IncompatibleMcVersion))
+ ));
+ }
+}
diff --git a/packages/oneclient_core/src/plus.rs b/packages/oneclient_core/src/plus.rs
new file mode 100644
index 0000000..7e3b442
--- /dev/null
+++ b/packages/oneclient_core/src/plus.rs
@@ -0,0 +1,333 @@
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::time::Duration;
+
+use bytes::Bytes;
+use futures_util::{SinkExt, TryStreamExt};
+use rand::Rng;
+use reqwest_websocket::{Message, Upgrade};
+use uuid::Uuid;
+
+use crate::LauncherResult;
+use crate::auth::MinecraftAccount;
+use crate::constants;
+
+const MOJANG_JOIN_URL: &str = "https://sessionserver.mojang.com/session/minecraft/join";
+
+const PING_INTERVAL: Duration = Duration::from_secs(30);
+
+const RECONNECT_DELAY: Duration = Duration::from_secs(15);
+const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(300);
+
+static STARTED: AtomicBool = AtomicBool::new(false);
+
+#[derive(Debug, thiserror::Error)]
+pub enum PlusError {
+ #[error("Mojang rejected the Poly+ session join (HTTP {status}): {body}")]
+ JoinRejected { status: u16, body: String },
+
+ #[error("Poly+ rejected the login for {username} (HTTP {status}): {body}")]
+ LoginRejected {
+ username: String,
+ status: u16,
+ body: String,
+ },
+
+ #[error(transparent)]
+ Request(#[from] reqwest::Error),
+
+ #[error(transparent)]
+ Websocket(#[from] reqwest_websocket::Error),
+
+ #[error(transparent)]
+ Url(#[from] url::ParseError),
+}
+
+#[derive(serde::Deserialize)]
+struct LoginResponse {
+ token: String,
+}
+
+enum Outcome {
+ Connected,
+ NoAccount,
+ Failed,
+}
+
+enum Event {
+ Ping,
+ Message(Option<Message>),
+}
+
+pub fn start() {
+ if STARTED.swap(true, Ordering::SeqCst) {
+ return;
+ }
+
+ tokio::spawn(async move {
+ let client = match build_client() {
+ Ok(client) => client,
+ Err(err) => {
+ tracing::error!("[plus] could not build the playtime client: {err}");
+ return;
+ }
+ };
+
+ let mut delay = RECONNECT_DELAY;
+
+ loop {
+ delay = match session(&client).await {
+ Outcome::Connected | Outcome::NoAccount => RECONNECT_DELAY,
+ Outcome::Failed => (delay * 2).min(MAX_RECONNECT_DELAY),
+ };
+
+ tokio::time::sleep(delay).await;
+ }
+ });
+}
+
+fn build_client() -> Result<reqwest::Client, reqwest::Error> {
+ reqwest::Client::builder()
+ .tcp_keepalive(Some(Duration::from_secs(15)))
+ .connect_timeout(Duration::from_secs(10))
+ .tls_backend_rustls()
+ .user_agent(format!(
+ "OneClient {} ({})",
+ env!("CARGO_PKG_VERSION"),
+ env!("CARGO_PKG_HOMEPAGE")
+ ))
+ .build()
+}
+
+fn base_url() -> &'static str {
+ constants::PLUS_BACKEND_URL.trim_end_matches('/')
+}
+
+async fn session(client: &reqwest::Client) -> Outcome {
+ let account = match current_account().await {
+ Ok(Some(account)) => account,
+ Ok(None) => return Outcome::NoAccount,
+ Err(err) => {
+ tracing::warn!("[plus] no account to track playtime for: {err}");
+ return Outcome::Failed;
+ }
+ };
+
+ let id = account.id;
+ let mut connected = false;
+
+ match run_session(client, &account, &mut connected).await {
+ Ok(()) => tracing::info!("[plus] playtime session for {id} ended"),
+ Err(err) => tracing::warn!("[plus] playtime session for {id} failed: {err}"),
+ }
+
+ if connected {
+ Outcome::Connected
+ } else {
+ Outcome::Failed
+ }
+}
+
+async fn current_account() -> LauncherResult<Option<MinecraftAccount>> {
+ let Some(account) = crate::auth::default_account_for_launch().await? else {
+ return Ok(None);
+ };
+
+ Ok(account.is_microsoft().then_some(account))
+}
+
+async fn run_session(
+ client: &reqwest::Client,
+ account: &MinecraftAccount,
+ connected: &mut bool,
+) -> Result<(), PlusError> {
+ let token = login(client, account).await?;
+
+ let mut websocket = client
+ .get(format!("{}/websocket", base_url()))
+ .bearer_auth(&token)
+ .upgrade()
+ .send()
+ .await?
+ .into_websocket()
+ .await?;
+
+ *connected = true;
+ tracing::info!("[plus] playtime websocket connected for {}", account.id);
+
+ pump(&mut websocket, PING_INTERVAL, account.id).await
+}
+
+async fn pump(
+ websocket: &mut reqwest_websocket::WebSocket,
+ ping_interval: Duration,
+ account_id: Uuid,
+) -> Result<(), PlusError> {
+ let mut ping = tokio::time::interval(ping_interval);
+ ping.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
+ ping.tick().await;
+
+ loop {
+ let event = tokio::select! {
+ _ = ping.tick() => Event::Ping,
+ message = websocket.try_next() => Event::Message(message?),
+ };
+
+ match event {
+ Event::Ping => {
+ if account_changed(account_id).await {
+ tracing::info!(
+ "[plus] default account changed, ending playtime session for {account_id}"
+ );
+ break;
+ }
+
+ websocket.send(Message::Ping(Bytes::new())).await?;
+ }
+ Event::Message(Some(Message::Close { code, reason })) => {
+ tracing::info!("[plus] playtime websocket closed by the server: {code:?} {reason}");
+ break;
+ }
+ Event::Message(Some(_)) => {}
+ Event::Message(None) => break,
+ }
+ }
+
+ Ok(())
+}
+
+async fn login(
+ client: &reqwest::Client,
+ account: &MinecraftAccount,
+) -> Result<String, PlusError> {
+ let server_id = generate_server_id();
+
+ let join = client
+ .post(MOJANG_JOIN_URL)
+ .header("Accept", "application/json")
+ .json(&serde_json::json!({
+ "accessToken": account.access_token,
+ "selectedProfile": account.id.simple().to_string(),
+ "serverId": server_id,
+ }))
+ .send()
+ .await?;
+
+ if !join.status().is_success() {
+ return Err(PlusError::JoinRejected {
+ status: join.status().as_u16(),
+ body: join.text().await.unwrap_or_default(),
+ });
+ }
+
+ let mut url = reqwest::Url::parse(&format!("{}/account/login", base_url()))?;
+ url.query_pairs_mut()
+ .append_pair("username", &account.username)
+ .append_pair("server_id", &server_id);
+
+ let login = client
+ .post(url)
+ .header("Accept", "application/json")
+ .send()
+ .await?;
+
+ if !login.status().is_success() {
+ return Err(PlusError::LoginRejected {
+ username: account.username.clone(),
+ status: login.status().as_u16(),
+ body: login.text().await.unwrap_or_default(),
+ });
+ }
+
+ Ok(login.json::<LoginResponse>().await?.token)
+}
+
+async fn account_changed(id: Uuid) -> bool {
+ match crate::auth::get_default_account().await {
+ Ok(Some(account)) => account.id != id,
+ Ok(None) => true,
+ Err(_) => false,
+ }
+}
+
+fn generate_server_id() -> String {
+ let mut bytes = [0u8; 20];
+ rand::rng().fill_bytes(&mut bytes);
+ crate::crypto::to_hex(&bytes)
+}
+
+#[cfg(test)]
+mod tests {
+ use std::sync::Arc;
+ use std::sync::atomic::AtomicUsize;
+
+ use async_tungstenite::tokio::accept_async;
+ use async_tungstenite::tungstenite::Message as ServerMessage;
+ use futures_util::StreamExt;
+ use tokio::net::TcpListener;
+
+ use super::*;
+
+ async fn ping_counting_server(close_after_pings: usize) -> (String, Arc<AtomicUsize>) {
+ let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
+ let port = listener.local_addr().expect("addr").port();
+ let pings = Arc::new(AtomicUsize::new(0));
+
+ tokio::spawn({
+ let pings = Arc::clone(&pings);
+ async move {
+ let (stream, _) = listener.accept().await.expect("accept");
+ let mut server = accept_async(stream).await.expect("handshake");
+
+ while let Some(Ok(message)) = server.next().await {
+ if let ServerMessage::Ping(_) = message
+ && pings.fetch_add(1, Ordering::SeqCst) + 1 >= close_after_pings
+ {
+ let _ = server.close(None).await;
+ break;
+ }
+ }
+ }
+ });
+
+ (format!("ws://127.0.0.1:{port}/websocket"), pings)
+ }
+
+ async fn connect(url: &str) -> reqwest_websocket::WebSocket {
+ build_client()
+ .expect("client")
+ .get(url)
+ .upgrade()
+ .send()
+ .await
+ .expect("upgrade")
+ .into_websocket()
+ .await
+ .expect("websocket")
+ }
+
+ #[tokio::test]
+ async fn pings_on_every_interval_and_stops_when_the_server_closes() {
+ assert!(!account_changed(Uuid::nil()).await);
+
+ let (url, pings) = ping_counting_server(3).await;
+ let mut websocket = connect(&url).await;
+
+ let pumped = tokio::time::timeout(
+ Duration::from_secs(10),
+ pump(&mut websocket, Duration::from_millis(50), Uuid::nil()),
+ )
+ .await
+ .expect("pump should return once the server closes the socket");
+
+ pumped.expect("pump should treat a server close as a clean end");
+ assert_eq!(pings.load(Ordering::SeqCst), 3);
+ }
+
+ #[test]
+ fn server_ids_are_random_20_byte_hex() {
+ let id = generate_server_id();
+ assert_eq!(id.len(), 40);
+ assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
+ assert_ne!(id, generate_server_id());
+ }
+}
diff --git a/packages/oneclient_core/src/reporting.rs b/packages/oneclient_core/src/reporting.rs
new file mode 100644
index 0000000..45b3c88
--- /dev/null
+++ b/packages/oneclient_core/src/reporting.rs
@@ -0,0 +1,76 @@
+use std::panic;
+use std::time::Duration;
+
+use sentry::{ClientInitGuard, ClientOptions};
+
+use crate::constants::SENTRY_DSN;
+
+const PANIC_FLUSH_TIMEOUT: Duration = Duration::from_secs(2);
+
+const ENVIRONMENT: &str = if cfg!(debug_assertions) {
+ "development"
+} else {
+ "production"
+};
+
+pub fn init(enabled: bool) -> Option<ClientInitGuard> {
+ if !enabled {
+ tracing::debug!("crash reporting disabled by settings");
+ return None;
+ }
+
+ if cfg!(debug_assertions) && option_env!("ONECLIENT_SENTRY_DSN").is_none() {
+ tracing::debug!("crash reporting skipped: debug build without an explicit DSN");
+ return None;
+ }
+
+ let guard = sentry::init((
+ SENTRY_DSN,
+ ClientOptions {
+ release: Some(format!("oneclient@{}", env!("CARGO_PKG_VERSION")).into()),
+ environment: Some(ENVIRONMENT.into()),
+ attach_stacktrace: true,
+ send_default_pii: false,
+ ..Default::default()
+ },
+ ));
+
+ if !guard.is_enabled() {
+ tracing::warn!("sentry client failed to start; crash reports will not be sent");
+ return None;
+ }
+
+ install_panic_flush();
+
+ tracing::info!(environment = ENVIRONMENT, "crash reporting enabled");
+ Some(guard)
+}
+
+fn install_panic_flush() {
+ let capture = panic::take_hook();
+
+ panic::set_hook(Box::new(move |info| {
+ capture(info);
+
+ if let Some(client) = sentry::Hub::current().client() {
+ client.flush(Some(PANIC_FLUSH_TIMEOUT));
+ }
+ }));
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn bundled_dsn_parses() {
+ SENTRY_DSN
+ .parse::<sentry::types::Dsn>()
+ .expect("bundled sentry DSN should be valid");
+ }
+
+ #[test]
+ fn opting_out_skips_the_client() {
+ assert!(init(false).is_none());
+ }
+}
diff --git a/packages/oneclient_core/src/settings/launcher.rs b/packages/oneclient_core/src/settings/launcher.rs
index 9991984..9de8c72 100644
--- a/packages/oneclient_core/src/settings/launcher.rs
+++ b/packages/oneclient_core/src/settings/launcher.rs
@@ -26,6 +26,7 @@ pub struct LauncherSettings {
pub settings_version: u32,
pub log_debug: bool,
pub auto_update: bool,
+ pub crash_reporting: bool,
pub enable_gamemode: bool,
pub discord_enabled: bool,
pub max_concurrent_requests: usize,
@@ -34,6 +35,8 @@ pub struct LauncherSettings {
pub dynamic_background_enabled: bool,
pub view_states: BTreeMap<String, ViewState>,
pub seen_onboarding: bool,
+ pub accepted_tos_version: u32,
+ pub accepted_privacy_version: u32,
pub seen_versions: Vec<String>,
pub modrinth_api_key: Option<String>,
pub curseforge_api_key: Option<String>,
@@ -57,6 +60,7 @@ impl Default for LauncherSettings {
settings_version: 1,
log_debug: false,
auto_update: true,
+ crash_reporting: true,
discord_enabled: true,
enable_gamemode: false,
max_concurrent_requests: 25,
@@ -65,6 +69,8 @@ impl Default for LauncherSettings {
dynamic_background_enabled: true,
view_states: BTreeMap::new(),
seen_onboarding: false,
+ accepted_tos_version: 0,
+ accepted_privacy_version: 0,
seen_versions: Vec::new(),
modrinth_api_key: None,
curseforge_api_key: None,
diff --git a/packages/oneclient_core/src/state.rs b/packages/oneclient_core/src/state.rs
index 80a187e..2fc0af2 100644
--- a/packages/oneclient_core/src/state.rs
+++ b/packages/oneclient_core/src/state.rs
@@ -6,6 +6,7 @@ use tokio::sync::{Mutex, OnceCell};
use crate::auth::{CredentialsStore, PendingBrowserLogin};
use crate::bundles::BundlesManager;
+use crate::discord::DiscordRpc;
use crate::http::RequestClient;
use crate::images::ImageCacheStore;
use crate::java::JavaManager;
@@ -38,6 +39,7 @@ pub struct LauncherState {
pub versions: Arc<VersionsManager>,
pub images: ImageCacheStore,
pub games: crate::game::GameProcessManager,
+ pub discord: DiscordRpc,
}
impl LauncherState {
@@ -59,6 +61,7 @@ impl LauncherState {
let settings = store::load_settings(Some(&services.notifier)).await;
let auth = CredentialsStore::load().await?;
let java = JavaManager;
+ let discord = DiscordRpc::spawn(settings.discord_enabled);
let state = Arc::new(Self {
services,
@@ -71,6 +74,7 @@ impl LauncherState {
versions: Arc::new(VersionsManager::new()),
images: ImageCacheStore::new(),
games: crate::game::GameProcessManager::new(),
+ discord,
});
STATE
diff --git a/packages/oneclient_core/src/tos.rs b/packages/oneclient_core/src/tos.rs
new file mode 100644
index 0000000..c30bb5e
--- /dev/null
+++ b/packages/oneclient_core/src/tos.rs
@@ -0,0 +1,197 @@
+use std::path::Path;
+
+use reqwest::header;
+use reqwest::{Method, StatusCode};
+use serde::{Deserialize, Serialize};
+
+use crate::api_config::meta_url_base;
+use crate::constants;
+use crate::paths;
+use crate::state::LauncherServices;
+use crate::{LauncherError, LauncherResult};
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct TermsDocument {
+ pub version: u32,
+ #[serde(default)]
+ pub privacy_version: Option<u32>,
+ #[serde(default)]
+ pub updated_at: Option<String>,
+ pub terms: String,
+ #[serde(default)]
+ pub privacy: Option<String>,
+ #[serde(default)]
+ pub terms_url: Option<String>,
+ #[serde(default)]
+ pub privacy_url: Option<String>,
+}
+
+impl TermsDocument {
+ pub fn terms_url(&self) -> &str {
+ self.terms_url
+ .as_deref()
+ .filter(|url| !url.trim().is_empty())
+ .unwrap_or(constants::TOS_URL)
+ }
+
+ pub fn privacy_url(&self) -> &str {
+ self.privacy_url
+ .as_deref()
+ .filter(|url| !url.trim().is_empty())
+ .unwrap_or(constants::PRIVACY_URL)
+ }
+
+ pub fn privacy_version(&self) -> u32 {
+ self.privacy_version.unwrap_or(self.version)
+ }
+
+ pub fn privacy_body(&self) -> Option<&str> {
+ self.privacy
+ .as_deref()
+ .filter(|body| !body.trim().is_empty())
+ }
+}
+
+#[tracing::instrument(level = "debug", skip(services))]
+pub async fn fetch_terms(services: &LauncherServices) -> LauncherResult<TermsDocument> {
+ let url = format!("{}/oneclient/tos.json", meta_url_base());
+ let cache_path = paths::caches_dir()?.join("TERMS.json");
+ let etag_path = paths::caches_dir()?.join("TERMS.json.etag");
+
+ let stored_etag = read_sidecar_etag(&etag_path).await;
+ let url_parsed = url.parse().map_err(LauncherError::UrlError)?;
+
+ let mut request = reqwest::Request::new(Method::GET, url_parsed);
+ if let Some(etag) = &stored_etag {
+ insert_if_none_match(&mut request, etag);
+ }
+
+ match services.requester.send(request).await {
+ Ok(res) if res.status() == StatusCode::NOT_MODIFIED => {
+ tracing::debug!("terms cache hit (304)");
+ read_cached_terms(&cache_path).await
+ }
+ Ok(res) if res.status().is_success() => {
+ let server_etag = etag_from_response(&res);
+ let bytes = res
+ .bytes()
+ .await
+ .map_err(|err| LauncherError::InvalidSettingsProfile {
+ reason: err.to_string(),
+ })?;
+
+ let document: TermsDocument = serde_json::from_slice(&bytes)?;
+
+ if let Some(parent) = cache_path.parent() {
+ polyio::create_dir_all(parent).await?;
+ }
+ polyio::write(&cache_path, &bytes).await?;
+ if let Some(etag) = server_etag {
+ let _ = polyio::write(&etag_path, etag.as_bytes()).await;
+ }
+
+ Ok(document)
+ }
+ Err(err) if cache_path.exists() => {
+ tracing::debug!("falling back to cached terms after remote fetch failed: {err}");
+ read_cached_terms(&cache_path).await
+ }
+ Err(err) => {
+ tracing::error!("failed to fetch terms from remote: {err}");
+ Err(LauncherError::InvalidSettingsProfile {
+ reason: err.to_string(),
+ })
+ }
+ Ok(res) => {
+ tracing::warn!(
+ status = %res.status(),
+ "unexpected terms response; using cache when available"
+ );
+ if cache_path.exists() {
+ read_cached_terms(&cache_path).await
+ } else {
+ Err(LauncherError::InvalidSettingsProfile {
+ reason: format!("terms request failed with HTTP {}", res.status()),
+ })
+ }
+ }
+ }
+}
+
+async fn read_cached_terms(path: &Path) -> LauncherResult<TermsDocument> {
+ let bytes = polyio::read(path).await?;
+ Ok(serde_json::from_slice(&bytes)?)
+}
+
+fn insert_if_none_match(request: &mut reqwest::Request, etag: &str) {
+ if let Ok(value) = header::HeaderValue::from_str(etag) {
+ request.headers_mut().insert(header::IF_NONE_MATCH, value);
+ }
+}
+
+fn etag_from_response(res: &reqwest::Response) -> Option<String> {
+ res.headers()
+ .get(header::ETAG)
+ .and_then(|v| v.to_str().ok())
+ .map(str::to_string)
+}
+
+async fn read_sidecar_etag(path: &Path) -> Option<String> {
+ polyio::read(path)
+ .await
+ .ok()
+ .and_then(|bytes| String::from_utf8(bytes).ok())
+ .filter(|etag| !etag.is_empty())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parses_minimal_document() {
+ let raw = br###"{"version":2,"terms":"## Terms\n\nBe nice."}"###;
+ let document: TermsDocument = serde_json::from_slice(raw).unwrap();
+
+ assert_eq!(document.version, 2);
+ assert_eq!(document.terms, "## Terms\n\nBe nice.");
+ assert_eq!(document.terms_url(), constants::TOS_URL);
+ assert_eq!(document.privacy_url(), constants::PRIVACY_URL);
+ }
+
+ #[test]
+ fn privacy_version_falls_back_to_terms_version() {
+ let raw = br#"{"version":4,"terms":"x"}"#;
+ let document: TermsDocument = serde_json::from_slice(raw).unwrap();
+
+ assert_eq!(document.privacy_version(), 4);
+ assert_eq!(document.privacy_body(), None);
+ }
+
+ #[test]
+ fn privacy_version_is_independent_when_published() {
+ let raw = br###"{"version":4,"privacy_version":2,"terms":"x","privacy":"## Privacy"}"###;
+ let document: TermsDocument = serde_json::from_slice(raw).unwrap();
+
+ assert_eq!(document.version, 4);
+ assert_eq!(document.privacy_version(), 2);
+ assert_eq!(document.privacy_body(), Some("## Privacy"));
+ }
+
+ #[test]
+ fn blank_privacy_body_is_treated_as_absent() {
+ let raw = br#"{"version":1,"terms":"x","privacy":" "}"#;
+ let document: TermsDocument = serde_json::from_slice(raw).unwrap();
+
+ assert_eq!(document.privacy_body(), None);
+ }
+
+ #[test]
+ fn document_urls_override_constants() {
+ let raw = br#"{"version":1,"terms":"x","terms_url":"https://example.com/tos","privacy_url":" "}"#;
+ let document: TermsDocument = serde_json::from_slice(raw).unwrap();
+
+ assert_eq!(document.terms_url(), "https://example.com/tos");
+ assert_eq!(document.privacy_url(), constants::PRIVACY_URL);
+ }
+}