Fixed #492 Fixed #510 Fixed #506 Fixed #568 Fixed #582 Fixed #578 Fixed #567 Fixed #588 Fixed #585 Fixed #571 Fixed #587 Fixed #584
5975 lines
220 KiB
Diff
5975 lines
220 KiB
Diff
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);
|
||
+ }
|
||
+}
|