From 94eaa2abb0eab6080f57139fcffd404eb32f91d4 Mon Sep 17 00:00:00 2001 From: Alessandro Autiero Date: Tue, 6 Sep 2022 14:18:31 +0200 Subject: [PATCH] Switched to getx for state management Fixed last remaining bug --- assets/binaries/console.dll | Bin 302080 -> 302592 bytes assets/binaries/stop.bat | 3 +- lib/main.dart | 4 + lib/src/model/fortnite_version.dart | 8 +- lib/src/page/home_page.dart | 144 ++--------------- lib/src/page/info_page.dart | 11 +- lib/src/page/launcher_page.dart | 76 +++------ lib/src/page/server_page.dart | 70 +++------ lib/src/util/builds_scraper.dart | 75 --------- lib/src/util/download_build.dart | 44 ------ lib/src/util/game_process_controller.dart | 13 -- lib/src/util/generic_controller.dart | 5 - lib/src/util/injector.dart | 5 +- lib/src/util/locate_binary.dart | 18 --- lib/src/util/reboot.dart | 11 +- lib/src/util/server.dart | 37 ++--- lib/src/util/version_controller.dart | 44 ------ lib/src/widget/add_local_version.dart | 13 +- lib/src/widget/add_server_version.dart | 117 +++++++------- lib/src/widget/build_selector.dart | 38 ++--- lib/src/widget/deployment_selector.dart | 25 +-- lib/src/widget/host_input.dart | 33 ++-- lib/src/widget/launch_button.dart | 144 +++++++++-------- lib/src/widget/local_server_switch.dart | 15 +- lib/src/widget/port_input.dart | 29 ++-- lib/src/widget/server_button.dart | 68 +++------ lib/src/widget/smart_input.dart | 50 +----- lib/src/widget/smart_selector.dart | 111 -------------- lib/src/widget/smart_switch.dart | 31 +--- lib/src/widget/username_box.dart | 22 +-- lib/src/widget/version_name_input.dart | 14 +- lib/src/widget/version_selector.dart | 178 +++++++++------------- pubspec.yaml | 6 +- 33 files changed, 429 insertions(+), 1033 deletions(-) delete mode 100644 lib/src/util/builds_scraper.dart delete mode 100644 lib/src/util/download_build.dart delete mode 100644 lib/src/util/game_process_controller.dart delete mode 100644 lib/src/util/generic_controller.dart delete mode 100644 lib/src/util/locate_binary.dart delete mode 100644 lib/src/util/version_controller.dart delete mode 100644 lib/src/widget/smart_selector.dart diff --git a/assets/binaries/console.dll b/assets/binaries/console.dll index 4f81c63651382900769ba909a14b11975ed1269f..ecb5dadd3227a298a631c45c292160f2bba3a7eb 100644 GIT binary patch delta 53450 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 delta 53136 zcmbS!30xCL^#9I65K(S|AR<9P5fM>D5JZE528D_@-gx5)Dk|0^C`O49Mc2C4qQ$HA zs-m_gp2Zs#Egn_0TJdNNqLo&wc+~vg%5F| zras-muB;*5S zef>zdFM|70{-^Ax%6X29lZrvUe6Lap6V2-Cw_fx?+I>j3r7agb2=R;5L{JF1+M zwkG+#Sekl$8fDra@c`yjx?hZ8ICI@F|4^0gyej96V&rKdd*Z9|WOH#YT7)xDnF79n z94sgRKZ)`4u}-+UDs8MSL@gyrn1-?dMXL$cz=!syuwQr6-5ziv!hYq_lhWD3cA zm}xR!`ZyPNMM>iH%wCyiJsD(ZlJ{n=06v`H?WS>fL|@FEl}dLSb3YVwk9q3$2~X#e z8CzLP#mlh*Kb0*%<>zu&!q4wrNZBKqO*UIlA`@G0De`7Y>UFm53_z)|+=p?nRU&t$ zx2>YX@XkORJ{{N6n3satX@mx*oYit?EO4R@g42R}A!jgW!~cJQ+Q z2ZS)s?V6*0Nw4bhOn-+K3hFPKN_g`SQvGHNw<_I52qo%@6LlAHoD=hn!&&;-#e<_A zE09}5AE2=Mx2j*F?p&0j%x^^j?Ckx;mDq8?6ZL09?N{~qimGp2t5VDtwpa- zp-j6Y3ZON(HJ=~zbabN zhuW|4&1rJm-kMUrK8q5xHY(i|e-$VQ&N4xF#n_$s*0-_yRv@rsCK6}re()FH;iysM zM?~es$fjCTrdd!I`wvsBDdXw?m~MVogF~^t#+pvX>Q1VnPtQE2J6}1%WHKe_E*gKb z!u@m}%Y+ik!~#a?e+TViyaN2tCZlx< z`=h^^CN0{dDNLUhxnVPXF*mri9S0Q?g>`hMP4Xp8Av#a?9CVl0GGI3pvwPqT(=gDF z_Vy(j6T;E4%ol<8ks}k*GK8*p&x~j}lXm^cOl#GNaSZBE-_!*7G|6k4Fkb{^+0A~3 zEl`qSd_FThqy-)S4#=A@@>YYuCQfZN({`*Ep&rf{TP?J^StBCeUIWDL%)a2^boo2R zq4kWQGoFO{IQCR|;Xe(urv7ns3iqb1sYP#hLfbj6cm!9I{3x@lbwJ>M9#Em^;pId_ z1A#A-{9Pm5RXLqGtqmq|e=%=bPp0p@X2yoJr?T-G_()BL=r)x&Y)9otmTc8U)x=F4h)9_+zPlYA2MzJ2rlyMaU%J%u-| zze43q^6#B2+Vc*HD!@^Y)43V0v4%L{kk4$-Imk?A0Ch8&4X=us6&?$gTOZz0y?V|- z-Njux53DjU{NamMlK~^M$`G{5X;90Zx3ff07J=CK_x;~*`FXAg$|jKF?b6@f7p(qK z1mzNFpfb@ER6>u-Zx+NV4rcW}kSw{AYzIS-v}N4?Dfz zoGDM=_GEk`T-D!?`(?^+8~z(CqAMhHV~fB0CS>^DwIXN-fhMNTye~JlUm=2u2sGlA zQ|t0@nV$$U5U8p=Zrz))ok}gBR$5Il!LnX?-wdKMPg!7EO$mW6Ur%q|C-+Sl^Ls>d zJKUw?9bahj-5H0-HnW9Zz0Hqj2;9=Q&a4yv6A7Mva*3Ij_%UAGk-_A zs)H7d8#J{1_5yJUq_LRzw=LiAIU^Q>G6MB`we$zC1qC-nsmckIFd)|CchBvJ2&y2E z_l@QrHCFAdEFdSX#z?TcRbvZJ#X00MOhjR^*!7Ks*$9$%esSg1jw7LRG10f?_r z9jR?Aix$qu4RRPOf;G_uQ}p1$kDB{jEBNc%{+0!56kt46hH-3sQ+nm?U=b8Zppr&M z=>vbQ7mE%a-c<&d?Kg})+7$~@_G%_hed)&(^4 zbBq^#U;}eEri;4ISCCg;UTOoFqx`WJ-+z@4_YhTvqfP+~W26vQgyZ&-I74L!^;=yN zfaepW-niOeH_S~H=bs`eSImaak*XuBDQXF5Rac@_Rm~hOewuIU=kiUf&6jE1rJJe^ zpPAkoH`1TYtR*tb_*`aL7MX4D(v9Ah&phqoADL^mcD2nLpDmq9e6B?v-IW0$@!41$ zv)!;*sv%Y00B~IE4$P;h#5^U!vokIxj`>>Ytq@G8Wediyx5)RVGC&=lPYe~GFLcrX zaCxMM&OyGaj+$cVEyu-fLn$iOfM>QUU70|!4f+rdrFrt#AQD3CN}Ntto(L0&Jo!Bf zVAd)CqRSnZY{l6abkH8{>-JH6;b$6ThXvz%W4m>%S_ydw+;tp+xdY2($=wAIMA9at zHVw|1O#e6zmN86F*U)~s>8)4ZIJjD5XXA@h2E`@^GVE9?QM{qHXv=s_KCv4@Gc!MQ z4M%p&qpnKF3mPKtdHGn-kF8_cb!#=$=MF_30bRn)qyWt}b_OEQiBO#i`VF)PuC4ueB`gLK&X`OvL6PmRvVzXL z0L8-G)P6UFAhyJDsUBj^%g*<*rgmd9@Uu{f%JOCy*bBf`3>esn%4K!?2v!20bdn3P z7IFTn+O}fG$MsiVV<<`|cZ2-$7N>rMb%-HI(`Zn?i=_(^4lnETCh^hEdriQ+4DR>B zx5WBb!qJ7rf%X-10w(0tSUWBY^TI5wRu5cybk{1msO_r>cR{&Us>EM!&k{j31R|OL z9(gZKgi&O4Yfi@UDXROg}T(BPL?C2R^vhF z?p2N5d@|1QgD7_eh7Jn@iKX6Y0x@*7*3Q?O0F9FiK;|A$Y3B>}4(4kffF&HO9Ztpy z7>rsNI$k@XOsRzeagGLIOcb#gic|e8nDfNi!nB%5lFDL)WEOYdqLys^Y+@EATR)dT zTkb0luF2ectA)LI z&RRsEn9`Uh(-h(2Ijey{&mWDj8Fl4W1B=37o=2b_UQ<@gE1Fsc(8StSEg@Kb{hniH z{@pQMl&p+EOh|J2;qtvtMNm0`{`#%>zZp$F91}qm7&^Rk^WBbR`4j6iI#o(NegtNo zs{H*ddZLzzVn^Jyf`~C~6I?B(XIKaMRYe)0tDSsXK;;D5*Ro;%eis~ji#a0D9*6Ya zOKVnN6hTG;U43u5m(=OBWj&bF30`*jfRX{qnlUE)sJ>A%?@&pc)w4x0LGjkqHc&`M zjU8zSEKbF{mX8tTT}Fm%!7zxRasmmKLIhP1s71w^-45e6io{8+F%nELE23o81QP6w z2&y5_u9}R!w}vegixXvo+r9JUmF?v&1!a%YjBR-+q}9(50Mf{kmyne{oL*5@^-SRuiTl`;nLpv(*SCP|vwDpUx_3R3geSl1l~sHuH6x z3=60`=rzGsL@Bfq{+^sPeZ<&!$>0DiA@*1UkfipepewGpEwyo?aKb$- zOdLcx1QJNnY=ja>7_LOAA_*iV7ix`?V3LBNB9Me&Xf??M5(HONSPFq8%|+a*J2sGTk-xEd29Lo_Gx;mL)3(pL+uRsA!M0}(W=+cw1J8unbQX_&= z2^7okVlkcrQ&Ob5I)h9ECA&JS?i{gp#*s}Rp{tn-0~zUdN+t4!+iAqU+iDSiX#`qJ z_B*eL+bM%UzBx;Gerh%eM9k(@H-kln&`#L|8-F7(y+#%*p2z1BNSLWuB!gX20%7~= zqlkFV_@=n3pFbXAGwRZ<*&@0^Lf5l)wpiO5I|!6tKXS^4d=I1hG{)Y*I@+L9f)o1{+(Z*gO(7vm zzcFB1ja?nR+d&|rcgfbznAJmF)ykrniiwKuuSNVB2;K8%yP9K#dg1fKtHYd@Z}#x!`q7Z zFcRo`%bsbCo*rH*f~pCW8sl9>Pf3dsK{W)jZyt6~0!cZ?^`SyR` z*Qv6r-Rj6&J`{F@a^6}wF55zuB*Hw7>66-_Ud2~rUB|3U4eu0Qvwj4XXF$A zg{BBG*GEnEleBMTq^Sh^#*7;6s+P296@*T(Xrj6q36vmZ;Hn8EY2a!IBp5i048hJ& z^{^02+B;hUN!mLn0!i9C4+2TrI|YF(?48;|o(U#t??MSAY40KlBx&!I1mf+T*h;Dh zB<~oYIHkS12phawK9UL6@{Z=_E>IrEd2G0}09Fv0kQ6{8fg}a6nn02QSVJHQ0mPl; ziaSYC0Bs2*Y1NzvBx%(=2qbCM6a=!cYE0ZXC$*(1Gh41uLMmz0A_*jE)RY90G-@gW zNfzy_LCi}RMNk^$*VJoCD5|uw~2rKaBr$e zDl!N(tYXt`2ao6CiA5HHhNtv<(!6B)FBYkw-?9m|@Vo+VfQna&xdgf+wPTe}pw4Gw z9yV1ZW?ST_)vPDb^CtssMjflO0a{3)P%rt~vDe4c*#O-^u&3$4A1A*Azjz68Mt}_KGCeVrXAMDo_4UJ(&rMarH5_)`dSSRAQjL>B?tN(zW zydXpbl@n-o7l{F^0z-#uySnxqQDX$Cc6>r>C7)oDn!27qlA2mbAVE`^s}o(-lDb($=qz+osJ&KWAdsY*786KPO-l$Q zp{8(jUq&!VJuN4Ypr_2rNiFI_44L28V@^zZM7G^Nncpk8sif5WK9E3?qhTn4BuB$Y z0!fU9qhL#gV3Nb3ia-*>As86~Nsfjo1QJF=X3-Q^wdDAhM(8BRzYGFdjDNM=mqj4S z;V+v&62l)Dm|TKMj(_W)ZBGp?@pgGOkmTG@}BOd+=2^4ZyvvuEp6UK^C?ZD6x zVd@Bvlz7y#UxcyR`Tnk{pBXh%$@EiF7t;tNsf!r|lGMd40!ip1w!Q?D)Wuu^N$O%g zfdpL?wZM7;Nor;xfh;uBVrsL4V3Mj?L?B7kG!RHaHDNwVAW6+EArNRLXI7a}>8`ed zA!X*LN7y9;0ajE+{e#ET9niQ7C-6Blw|yabV(3IRpCwNWJqSdOc*^fh6g8ECK;*b~ z;<_K1(CKb5{`jOg8^tlZnXT%E;R9> zC<_&#`{!jc9O9f7ZxkgHsITKB{y8>_XfdqkNZqC?hi#rl;b25u&!joxI8+IvV2`0H= zmr5YX4ZAb~aT|7Z9?#3b(BaxDo5yU%HM3JQ$;Ch6TWCLKdSy?6vJEs zNs1wtp&*7LxmizA^=W1ym2HJH*g}j+RR&gk;w#$aR==Xm6y{T*F zzgQ=Ah+7(1-SiCm`-wiO)MK2sEiQR9+=lrZRozxT-Hb%{+8^Sd&*Geya&x z)Qyleu~tTu?M#eBNP;p(C}%()@fJOf8%+W9=*g=yBkJ!u?F`Y)BWyv*(FLvLX zB7!mq)VE^Ir%%OX5y&>!kAEzt&zIz}7|#lMjCdO_mk?euPA3QdqU`etgdZPQQWiBJ?0s3$#hZAMY*8H)^IOx{5-e0o{u6illrA`o7Q=uhtdXpsyiw*;#E z?fkb>zx-Rg8Cp!BLr*@;ds5VCLj9!EUH{cc7d= zusZ;o1KHx)Q3ZjT?bcl1_n)VD^V&$DMox*>4c+6!P&I*)_S{iCT3DGeS;S)v!Af@Y zT=n`*(;5cNcU6Be`pXlBPHEvHI$Qj(nZa;q%hF|NYpx%AO(gF)4sayuS%XkyP54Zh-^8?U^RJwT2cpV2qdY4R3ls! zNgcE$kc19mn?W!^2Ss)9AdsMu%=87WYC#7@bb*Af)+AYIe<*=0=5Mug+eiXQPT!OS zl9;}Mq^k%fIe$wgkmUR=g+Ri}O2mCCfrQDN2udSR?bO^-(lZDqY{iR`WfAD@`bo#m z-by(kg0cxTZD91KTt^joQ^>sQlS`oY>mD5XzO#C#C{;dza-U35{Ca2Ho#i5IJ;8?W z;AD~URX4Y*03jiZM!SQ3)KN(N#TINUYv92_?s!Nv*2JgcvmyDQ!S}>xwDIr+@VfYS zDew1|iVhy*GgO8?ub1xgS@6wPOGe`1A>V}ly-cldyNNXd&!a#vz4YJABtqvQZm zOjd4^Pfe5%2tPIP{PsPu{V5|*GMr+2Oq6#JzJswPlMalmVruKoWJK{W)bA+vbyQ7l2;nEw{nQzv;%SUYy>D7>vztC+UL zc0ThCPMusCF^5wJk{r!br|>2Iw!+ku8MVYs@&wNI^LE;aK#~IMK_Ce`4G*^x zNXksBHGu>Z?6j!Lp#+k&+K~j3wAxAnNm^|cfh?_dk|0rORNm}hx0tr@| zDb~0(lRACNAY_7>KKNP_hSWvh5W@zOz&DPI`{TDm$g|O(Oahb1L5-virw~YZ@=4To zsRY70{K2?UZ<6cvFa6(yNK5{d#FeJKPJG(}X+R07o+ zB@2;FBapB*I9R#T4ypfK!%q&MJ8HbhK8>((B+6Amc2$HuClM4#Aby8Qyt@!eph0AJ zLHwM)CXztGYQKKZ1H|ttLwgZyVQEYI7qbKE%xnmBA6_!}WCJRhKv6dazp!gxXNxz5 zK>x1pEtm7p=?ms4Rg+3E{G9&0x;s#51X|_XF;l*(&g&8x7?R$A$|9-Ev#36+KePBP zB6~fO0&XCXq<|L_NK(K{2qYokV5`drCMn?M1QG;X)U_1^k{lR}1QNuZ8L+0gT_Hq~ zlL2c+vt~`lQ>(CK8DZHmt5JN|1$bMKyrlMx0l}MV1glBXJQiVH4g?01qX9x@*og$v;PyanZRFizNG#kXu_O#CxzqG4aPoQPa zg^9;sGBJFraae7%nnHrbEv@?4t=-+{q8xV+sNazAA2RYfuMj~+7&5;&D|&a@KvJ27 zqvzVs?EYqu#hxG>%<8qZyT5zZ&QM<(4ljOcTc3G{KaG%=4cjfo6i{Tc#QUCXFiXC- zTnsB$KXCq` z+k^+NE!#m;P9Uo`&Oc@7lz5A=c9vTKP=fgk!Q#DfX3Y8|^I3p?{UYb9udS#=UHB>R zs_G?b!w)kFU&o|Lww3ZTZKe85TP1&{tx{^+ipf}~VmfSSCjEwbHK`!qVm_h@!e>^X zOO5rm*EjrievAmBhzz$saG|DG$i1t~{ta%;WZt_!`J{srkkvL3^JGJcMY}X-rhGFn zjopRF+p|SjM}|ajuhN#`KoTFw#{ne-+7(x|PSgf?zamOS6122T(8Mxzr`1;@GX4H%Bk{JFf*YbY}|$?kaZaBXlwBCX2J>FMaLeOCnRb! z{T2A^i2ojjVeGZ}=9iIcfymkw1%7Zv05tg3)`a82i@c1fU|fU9Xj0RpY~x-gePdL; z-SAnOQe!+*w9#Ey#@BwUAmNBG)YLX+!cp^Jq4~{GLxw_~{b?{RVhd|#qLB3FAU4&* zbLqxOxHruZXDTn)9VHglON0?yX^7L5eCQE&=?Ig$$t|Y6*P-zxJsyk5ib?Km7tK$o z`LY*T@|knz6z@Py@SA0BgSRmIH@T`)HqLw9FCCF{UNar1YI`=@*+l-iaiA#o3X;2X z@2FFLMWJ7bAR~b;bYIwZ=JwP;5yXl1+0@>h{#gEMp9KUHN=~+a{`Ft;JCkNUh-@Db z=5@@pZ|B+yV>a{RTmR0MBe6MAH8FpdBe5B(A&_|_?o;UMXF1h0({bPC5T=^UvchYM zoIdb+rVorHmf{B{LJ&V@?&c*9-*(`X1}mG=~pmNCgsZw83~30d+E z9_ZECtC)#fywxY3!#U*2RalAh?Ds>!cy}TGZq;_Ug-yuW7%=?Vs?p`;yYgpRG@Ldb z3ZHnkXFZRz$NWX{@N#)^!_D>oy}c$v6rZdzG}?FZqwpOVuCk15YhdlpRruCSmzV81 zbtp7Wgs&N>w6ZPC(XA1Vqv1_=_^#4W>>2;fVbC^tr;umB?%VlLtDKmQa1ZaV4*X>7 zF*j-%sfsYDGR`|)YYX;0;L2og)A;=vXmxGluB*K*8P{ZxgbTSZcXVd{-sw8{7wScu zA78B`1ERSA?%3LP`yX$5hur%~lm$MvPB6dgjR$CRs(hTgc?QA^-_@PDvwiiL2j3zp z?1D*tawAkvR}yy3Bwq)&rLj7jjxs|f?zAABGS7ZD1dMmS#GUpn+(M^06NVo)_xf$$ zqdS8v8NnazFnn3w$BKNbb13_YDgNHW_hpFd1??Y~`-=-rg>Crq zBx|ds1wEO!-_P?148?`&WaZe%!k3ov22|kUotYIoV(RHO;SvLwvK>wREC)sfZ*?`F zc4YI~DN$l=%U0lKI4VrcRN7(8qHW!nyq&TCn{JLeU|LSLht0)v-Q3{7wer}od+#kP zs^OkakjeU*KWSu6^+`9^j(^8?WviXO;kvsT9#!DG`z+j+CTfo>aW!j1f>10bnt_Ev`{(b2E!l?C7PVpU z>8@5X?Q+p*fR9^pJM3wC;Hh@DC;=R|`0hRVk1{fw?9vOB#K$cSZCrNEEtq`Wl3tTd zEVJiV3sgG>j~ya{atY*T*c0$@Q@!4$A}k+c<}Woc!}o~yAB18x*|=(!9bGE}xun*@ zm?_R3LuSRplWB8?>q#oX)LW!toPXHjQW?ZqD9Shzj51iL4Cd?)5#v(6#&Y`)Xai`9 z6KIMDrzt>^XW#1~^vCNNh=NxuYB|k`p&=g37WdBI|4`a3%~l(xU|Zn-R;F7Wd9z+h zrfoOhFB#J5&VLpahQ|)Ez!~gZ|F<$diJ4O5F{b1zPOf(_X1QJfx11UK=029#=D2`7 z`;`zdp3J}!yBKakV(m%EuOk{=V9(y`X~|TaKS*HujX~VU$DR~rdd9XO7DaybchHNx zB!w|F8%&v2cALrEA2D<%a8YXP27?)NO(=9tB-b?xTyQ2NG4@Eu{K>O$a?LeI&0|vb z|3`VmklWmdX3c zh0*=kg<%i6_xN@V*P{3VD&A7`huhM`Jl;0gkGl)aCvO8+l}R??ieW-cp8JCd zb>$>z*SrG%P-`Y5%*(EJ=|{>C2Ha0u;ILq|nf=?EF^NC=K|TAah2|5{7m{X?Y)tMU zEoe+E)sCW6MI@E|_N!H~ZSIH%WCK8w12TSx6DKnp1}6Q`EjJsOAmP#Y&lToIKgtZz zd!x%tmR_M=3(<#t!CIN*bK$n?JioAjF~=Wlbf!NnGUxj%2E_J{9S}P(c91G>kG~WC z{@|i#)lgV`*lXj-1tt^zFxFaZ^K|xi@^EMIOIetAX#z1*I?f|YPec0Yf%TFq@VB?% zv`vggs(u#M{3`8n5g1RYP6*2nmG&nwW(()gu&jlhp?KIHB3tNr@GMtlEIvH}6$Q~V z$#Y=~2RQ6VIQ$T3wQ0S&<_;d3#Ca8|4C*sIJsVv7Ip7lvzSZ=r^JdvT)4j%@V8tp! zgT24L>KeHc)&zC(EKwO!zKAu=T-JS$C8h@MwDDysL-gWVBR9)ucDDoxR<1I9y}R|> zuX{FG^a)m>GI-`^y;jZbzv>grs4{H(xsj^#yBnWYuv%s4;V^pf7x{hF)t@A+`Fsy( z$guX=P9U3H3C9K9NYwryYFAAm2uQ*)VOEo<-5!W={RPp`a4Wdl5l`HNnj)Os;?qK6 z>ChMeRvBtXzB;Lk32a_&^8KSW7tSs4YL`WIl9!R>wM(NqFkbe71jZNPtx4QU_}K^u zq$d?aOkN=ug9GXDWtaw6258hcl%7<9iHdn52&BhXV6&l9C}N5e7!k*i_?DSP8<1-GWPC5Xgz?1fG-1 zv75XJ+$8E`0XP~yXEtCAX0yaNJQDlf)3ltGySWS+M;q^~N~`WQV~zKs*^<;pHqC5&*);fKdG5V5PBU1erJWhYyqG%=b^h z48eUO?4=~*+5d&nWC-sMpAXD==QJZ+WjaMZg3N9{gZ$Lpp~Fgz6?mutdz!3*Paoq) zX{dAXoih>@7>Ad}uzQ`DCu@7(WD2{64fd0zTmnnf>YadI02EBwkX$Sb|AxE5BKIvE zVu<@YJ{x2~eL8WFB` zyrc@_<#8|)=E*~Lfl%V`xM|SU5V1fY*uxRn3j}dI!7zb9#}Pc+$>-q56GRIHlQ@D) z0s%b@lopq9O`u5ND0T@H52kWVJixkuxeDY6v;sjfPf#M{P>&->69~TL3DO0EKgW`K z_TUKA6~=`;#X_Nsvm8ZpA&cQWLA;Q~HjY3h5Om@R`r~;C<}!yPc)Wve2``=?OduG| z5uDy}@ZjkbGZ$=aDQohG2VHIs$IhkvLm9T@v#oXXX!p$U;d6L~&fk>kL z9Er>K%-FMCTn~qsOx+w0EopG@rJ2NoBbjR^*~gQp%_RS(l3WIwNmlSAE6gO#gpt1NK%a5IhyayH22cDY7fVDiLmF#kt{Kj z9Kj^{IalFJ+~B5y8wYLyxZ&XDLpxK9Z%HR6w8WYjUJ^a#0mNFmZ4f>K?xTfkevP8s zc_(a%!+%Ib=Db6*0!;F+=fSW5iY)VZ0Sye3&p@!UVb|~`7B;bq556yHfZ*Hl^)6=6 z_D7hx7Xv%yK#~M)508Yr-cH6VN>G1oJ$$5?=pSj4cLxyf{v0|B%}3_4I{O0^CNj{tq^j4VU z{kXe8lRQC4l+XP4Yly=GSX*j}XW6NA4NUSq2Rq*Uj&?YUEtHWBHknF|ZCf9FUfvto zr(s<+Mzz9mH<*MwU)1{vHgMqE=FFx$G3X?7_s%*L#LWC{1N>!(!`)uKRvso(Y+lcg zId7XfE_25#fH4;w53O)KQwd-5T)S#pCG+LoLnw`Dd(Q{1eeU&w>&AN%yi<)JBtVl5 zejzwtaK+#%z$qBt-@AKze+NyD4?m!<a}S+Ji1ouFJz2O)G$}?B`GxI0d*O2rI!)1-IonGrl5-DShZ3yJfSvg335ufqyon zz;#rrbpJpdhdP6x>)yuZb>kSb7Z|e|Ta6jZiDUd8?fri-=8?wy^9VE6?(vMK)C*{A za0+lraAi=#RPeLGoq9nSd-m8FEn_}D?&a(1Xfow><;o5sWyeBAt;+wwR&+&<;U8ww z9|p+S@6Tp%?f&NwxNiP)j(76k6!j8Z2Dp51&fto`T?gmKgg=St>ax*X@zlgI1^$J6 z#Z$S8Uur0>xWZgH^r|p<2)yL6HCg|@y zV6|@j?c+Clgpkl&SUQd}ryDndr0ez1PpE)FFVCP=%(<8S=x%)(uU7%Ie_y8StLEqx zGx1ft``%=7Br>QEXI>JpnQGRDnfuy@dGsn9{>EeMzv)QJ-1yfIjbm#54Wgrx8UNQ^ z9+K?NH>+qdOZ^Tz?8op;ML}9Y;XtMhY5wr_z;HhoJ+KJIQ-Fs><>ou<_ENo zdHtpfNyFK(TiOicfozbd;?(Y{N^qg7$ zVO*1uj71`smKKTR4CDFV9(12M_g@$E?LnK5-yrme*SUvsXQnxhm8*b8XPc<&WCx_TzM6p+_)W!ed0*khAUuWvT1*)m2jCKw z+;`MvO;+h$5E_LPm3f3K{2gZs#NYU}pULESq^dqow}EA7)VJ9jLe(FyxAL4YS#yr8 z0Z-N(h^AXCXMJo?NWh?2()!7qm%w>lIWL;?+Q+gpY)}mPS6^g< zB9Xd@3Mxyc1C14AiZgF4W)aVv_6Kq^hDtNZ8y`ZFVkTM1ldSA2 zx$NgiTrkNvQj&_NQJJ$@$R%_KBW62+GkX;g6}JK0iO)G}<< zJjje2c|sF)e{*vN7+l|UB6EiD-pc@og&qt>_F*x2bo`YKtdH8F)9jG?sE_^Or93n6 zCpVo~Lw(d7UD4mKk5mXPU;`VVxOz@-`dMn+#m;Pi0?;$|>jucLkt&MIea8|=t+U?= zL6f{y6nnh^3Z*@xSXo09hhDNh8zM#b86BY+b>*=BRA8kA4zb?3K`cm&tWysxm-X-> zTm_3-lROLnM<>eBxrY<(G5A}amY9?&tJeXYV4pNZ0gkCTnCBJ##gLszen;3 zbVqiw1BylE><{qjc8{QFuJ_X5zhyeWnpRhVIRL>EI2Z-7n1hpXgo6ZKsYyNy;#7HS z{jt=Oh$-w~N7U3SiNxEVfO;B%8{oKy6T$sEuxlJq3rAf9E}{nh0Ap4?lYAUo;)oRJ z3|r}le4IJ~1`WVt?j2YUClrL%u-%+cAFmxaXI(Gou}$1vAo>3-_=m8S>^>*d)2SDU z7IF>h<)pf#?t{a*ikNMi*o`rZDO8vZk2yr zbP3Ex2kYHjktagw`VcqdMWcCaPj_?TVi6;~Dy#4hT z8li3oHKyMKpnJ+6482{+-kWg`rODrY2}2(y+GPXhn<@48Nst?%ogN z_J8WS18pLvXRmsrmT0;Dy*K&}(N=-%Rv&bQe&o-bf9c1rQ=oQeEqg(Mf>ApAT7jC; zaRF>&Ulb?*=m%X1^FDvUKwmM*XXt18qM=Crx;1Vom`5Ai%ZQ?XmB2}Sr126=Q}CMB zl_yacojH;jJc+`%gD0slll)~(a_Ma*!N)E*mpn7cC5|K*lW>cs(LBxQ792B>F-KG1 zoJ|*=#L=9Mh9mh?#qp~!HsMK{m~$D;k(@S@ylu!a(?=i)Hg@4?3e7Y(c^U`4q)1~E zj${E_BJp3U+{H&8X36o5R?E&ccabQ(F#1KlY#!fXhxJ$~0mv_NZ+{clZi+#PUF!Ep~FJqv`9YXJy= zN&XAFHyAZWOV|s+D6$?D4{^s?+tw(+rOo+|CNg2B4WP=p6^rxrU0S34sAV{4br?hq z2cH2q0`8wgBB~PZv%t-Qum&8(_GyDW*>-Kv19X*j3q_OAI=wm+(a4&C!r9bN3Pq(Q&1E5p$-l)-`-2vy+!v`ZlWX?Hyr|Ggdlce+RBf2X^i&wkSZ zy6OZwG6D>IDZ4rXbqBpHj{u{tU`-KdkdMu=kEZCq9dkC*6xGd9zaky~1zwWw&mNBY zJ?Yis*;$b&z@PjH-pPly@Yi>}bW85n{Ubaa=fs|fL!Dv6jUs5Yi9HvCws-AVA4|!5m}i!b&+pI!QE}krg4+x35jYPR+z0-B2NFx2FMC+@$-)$qzw-^5mt>u~I_V z3f5;>`=e!qfHn5y2zKi)#-k1hZDUan)T&AH2|S2C#aHtWb__shW$~=42MVzYp%4pg za+2=ZN#99@@(?=8UQ9sq(FnFjPZ&+-vMYKbU$lzd+Y{{IF!n)Ddqqz3&io!diV6 zR==gOhftONuss=whdqG^5Q4%`pmnoZ>JPT-H_@AJVoH(n__VkB~=Ut@;;h0ue02AmlN=wkT)>Uw0IPoJ^^(?sqCH!Fgo+Ocl!vLb9s+UgO*pa zi_(xQcweQVE^Fw=rh#NBS;a&o2d~3Kv<~I7mnWhbT&CK% z2+T2;Y4RlGYmRQGy)meyo2hIxa zUBLN*YXh#?mtf4mg@9`hE()9yTzB@tG!$OHy9UhVisfbVSf6yHQpdn~9^Xp%e`Rqi zMLiT+DHlvDL{xqdTFK`mNAmpxfn*#Ov6!Bv6#2i#k5TLIe%ZXdXxz#Rp565RQD`tcbk6SUAAYvy*InBO-|Jza>l$ zV?A?GQ`h0<_)A=T@ME@TF4|6qA7}s0g<09d9S|-c3?V!eRqQvhPjEX zm`z%ahN0isUCU7s9rTtRrbGRm_oRI^fo9P+3dnq1vS};OaQ7ZZDJr*~ z%5**<+BEAqY-(MG71B}mw-x9q8p)o^L;Yd8?6nfvp!yA9On>R zjr=qgQM17<0;dHx8QffOOTeuFSHQMijS^_Pm-_juVabAW^&8iqjWpf!1>w)BbOguW zkKb|gCARP@^c~%OKdV}YhRY}H|CGIc?2&b-3aQ!eSm17#{ymGii)Ly3XHBw4db_VN zcSrVM?o^u*H4xk=a1+7x0yh}kSa4Io&1apzL4)bWm0V*Lq;SkU-b0uv-osw~25kpL zoKt{CxJT^%lo`L>Y*hhzO84Hu-r5M0uP^njHj&0!&9QS(AN4J;({}lHn4PTe5w#TD zYH(kJn+Hw{ZY{V1aC^W}wzjsgHeq8|BUd(gHL})^-V6qtc6_Lxvjw?ZsV8j5{hhST zID0XXo?GXz#zNRZ2W5Z?cZaG&#Lvb79Kj)u;Ai9W%xJ?C*a%r9aRhq>f_P8PgALzcTjmM> znVZXA*o)TDG3okI`_K@2MCDgpWfBSruSG`XY{JRrBZSo=WBEcLfHm_J1EMm)*&PO* z1@|YofkzOf2UiJh;88He;5r;bR8MfbOBg|dw^4a-6p|NP2{=rY^ zqm|2oC@2b_5YQRth|Z+3e;-DTT>G2jf8^q~onyU@pdV~nL-58~_R0~|!T#b|e2=J8 z;99fJN5P_-*ri8dGJ1!-bQBEc8TRc_bQyKlpFakFV(rm*Jvp~B(&OL}P5Ad?I1NdL z!+|jN;c-~X3}Dkvz(V9NcGU^=1`TAF{S2E-f3Rggqi<>X8U6HPWUzugiJ&uR1FVtG zo7VCb12!qCmROL4EjDESy>T zAFiQh2>qllyAF#B^e^l2EBYep;{wROL&sV9nUA218VE^oQ--N9H`nnE=H}D_Huf>{ zV;>ul2m9Nv2sX_#So$U!gnH?R+(eI%*MxMAvt@IIW;))RaMpA!n^F$T541=B#|6wv zivIF#XehKs|MCu+fY1SU=v~w|N)J`5>VOv_n6>>7l44DZsW59R@C{~d-fYa8oc(YY z7B%-+)jgC#TTjw&xd*Gi#;$1`S79@R_L`_7TwP>$-$#LHzy8L3l!Z{aKIs887cJkS zpZ*XF6Lj7qXs;1atEvc9$5}<05236<5R~Go4JN}}wZk`Um7`iWq-SQZ^4+gl$ zSeDMGa_l^rBs7wDEMaFM`{qv+Ne|elZ~FvUBXo=HU_^Z*?*d&__!3TbZs9}{g1iqC z;ldB#8(jEiw%CY9(N@D*ze+TS4jQ4KUkRga)0kmUJpSQFC_M%Y97G^JQJi$%xhi1PJ9Ks8#CBm|DrZ% zl78O5D8dSslm~0jTr7O8-=Voy>X5#;n(zR{@E}sl6Vr$>t1$lCj%x>dfuIvduu&km z!4o{|!{^||5zH3|4)O$-1cG-hNfAQ@f;H{`Dm>(Oy|W2qkPhV<-r_2>yMb7Jbu;m5N+wzNBH!FI5v)6fb1x3=_; zwx|!=-x;;wDEBArMUC z38o1-{KOHs3j|3#!FGXQEl2S008&q{Vq-8*F_vSq!Z?$oxS`+dPH#l&Lmm10opmJ4 zHSf<$=5T&#_?1AgjU$i=1f6(-xJbTwIUK>`eta8y@dRV=qn)@DMsozG1%h{t_!gpt zEMhr|Eds@DcsCS}oVP(jNfru@V39yz;0bbtGE5Gn8mR&S%M-9d4!?5*T{wa!#m3n@ z#T9|#I7iX6ADiMy2hsFo{Sr^QE3*AP7*6j^@>}ec#`I-$k}dF}BhX;>h8OLJ^4Jo5ailmh9=E5o+tdLva zD2*vo4mz0$+{8@7oM>g~;8iu{zeR8%Hm?sd$?L!L17fYL$wc;!f{tnYM;c~l8IIK1 zmq7rJ$TlXq+@EdhOS^g)2;LR&96!wQ4ib*?V~6?D9zAkN$OTZH?CxktoM%4^qIB|^ zU{p=TTG@Gs)LM;G>8t}%D435OwuWn>&bmw|+XGNHtu^p;hdt^`d)5minH^w%^QBv= zKY+c9HDGF$&aFfzQvr3X)*8k=`+Ins8@DqYh)ryUWe#te>SX`ocxV9^pxMdM1OmzL z&%^zKHMV3VfIEdy1>C|6r>2K?qYIOotIUy3;pPh z@&)6eYB?n+PAi)Mw^jAxbh3$TsUO|0O$P{{mkmp$@gX`Dr?nppw~j}ixH;ym<|5)C z;wZdXKxw^WQrXu2bZ}osh{`FkJumw>lBTRMB>Mn&v9C`nG9zwt@z=^1TMW>2c_Br-v099(ekFdM_LHdV{W1srd|kK+mdi* z2)u$G22Z}qcH(ZyDKWitRLe9#00O!aJRHP61X=LT$wslyn$s;@5-$H|nw6y*V?Q5m zpocnMW&>K#t6f)ukZ>Bvb!T==4tt>m9qc&*0C*Vb_N;ow*S0Wgf&-T8z>8`$>kvry zqFed0Lj&pEj^WTd@FLk+sLFX+z$jMUlJ>3tg%9b>2p_gfOS+M(I~RP@6dqS7gs&`} zmzA*-Thg(1iy&WUN7%K8M+?zBxR8xcegMz7e_L3AVc$B-c&W$j=R7>b*>%FQHCXHNvtW85Zl zSf)dw?p)Zl1RWf1*0ABN=;n0WrtI)mbYt`ro7IY*b%SuVsJ_4WewmCSK+)&&fgiJA1Kx2Vyr0k5XWu}rrnxd9)z<> zv{nc(0&Oi8B7yW6dpnqJ;&mU!0hR7x4dfAKB7e*d5A3UOGhAk!TGJgoHvxg^e2$5p zzaQ$U964g>Y0&bVfQ{_P)-VJtXLDN9!E`_)c7JQygLZAiUTRHufi-M{5V{{a#EuN1 z6%A*(Lm{vPRGQ?YSzQR-dR!BjiKw)(wlKj$sos=9 z|D3}?u%W|GLz{L4fdoT28d5@4^6XnexbhnAo;+D=xCa{W*|%-zW8!BjgaO3JE()a^vBN@Xdyj88lC^b6_Fxjb2b8W~qf;gL9O3h3#??g^()q8IvU9q1+q46-hQ_Mv+^uzMot zaq`n}^v3tCZg~lpWCGhflI|}bVhOq|utEI_ErC9F1z_Cjgw_9$RMRmz@=qMOozrTRak=(fmhy9&!0yonMD zdYp{|rc=zf<=(8S6WvfX3?lFk#=^mH>j<~!WKlR0_zye{?_V~s7@VtOv2^KrVaxMH z5@e+V?NfrtuVxE6(H`zuKzzgrX9q9B^1hPy8}oh*pWp`X<23kJ$}@O^ z_qkY#ddF9&n)f&GU2ugDALIRPORNWyG|Pnup3q6dah}hI%Xxe$9~KJA!?!;zyrAz4QFn!N%^g>!+6HnJ5eN7KqDRYvHCuTb*(O;^a9#Evb z0!WX`UB~;Ozj5*Tw>dwV_p?;igf8_n>9X%~^clRb;QcZ_zii&GQIdSvh%abQb;)q9 z@G{=Vv-Ntw&0Rihw1~HY_*ZMJ;$rkaAAk{EFoMsV^jd~^@&bElZ1@tTU@Oq1QY_qrV;UeP0#-&h$V?0pI`5I8v$zw@i5JxoD!`mrzkVV{@lviv%Wy&z&iJ()7J%Cd!R_YXRvjc{eUNJ zXzCfb4xuzP6gxM)P z@JG4yps7s|--O-V(b@~yu*W-E=QxLk&{Rm~xU5Y7snY_zJJXb+KE*>> zR1qXT4=_hMcPu8Ql4+_KmYg9I$BxPzz>l_VP1d1U-9x94UB;=9eI6|jnM?n^` zDiM@Spkz^8f09LtIF8VzLR=yHQ?zx}D5?e2F$=c?w83m^>Mjz({#m#Z1zFZq3HW?l z!Ok&7jh$;vI*gAC!*N*NPjJ_yqHD%YnK4sU}CgWHqwrOYUAo^G*eb3I;hfwS>pi)9!9FZMmH^U|)5|$;3zSdL} z#0R#7n>OHR2i*1aEGt;T+0c|tJ31Lwk(82+Ra(2cB2UPweH#!vzXD(np%umoC`M^aMpJD|EIlk509d{`}o<-N!VO~g(Q+C z2+KvXAk-xUN)hP>Q6eISn*;<|ARr*sbHu+LhxL;GsGDMZt9Nw8FCi#D{p zO6!G4X+^wr)u`BNpXf^~RnYhIo0$x=^xytjp68qI_x$E}&fLzNxqN4qlY9;|vcT9} zsRv4%KdaQ=m?gPwOn$nA+)ofC)7eIO{=aaiUOqa-=Jk}1HcP`zI$S=w`AWkld7BG9%UkUG`E$mwak?xn&DYXmKaSqe3PdXinw!5| zTJ?CBnT`9NY>ael0SRpK&h@qs87bRjwwal=D$AUbbv~;tbBew+`&>q3lMcR|rHb?( zm=(oF-byj6{6I5C4wW_RQyejjIRVD40q>Ea+xA?1-lKnGe2K#3Of5rf>x&Dcq z>1sovcdT*!)jBXV{Rci~e34O}>3!L-nPrBt+WZSKD=fsrPhIMc9qOBPo&y7 zlBaXOQBj~WruiA~>T*?fZC6#$%4E=Dx8anH-0}MIO4+`nZqn*q+0~VZ81S3hcVxKI zs|HI)ik16&dQ@fB^RtNzX{Q^^&suyp`oZRav5IC!$`(jbGxSsb><79_is@#L^d9zl zyGf^>>J3ZYQOT2@{HpQi@w#_F4w4`TZXi#NB%jJF&xuK%&**#&JwRl|Os$qkm8%eX zh$5uSCRbWbPA6Lb&{r{eY1HGSC^xrtl}0BB}KNsyX+~*Q3VzGs-WB# z>lDqYHtV$IsqlMhuzL1A9&?+On5z|Z#grsjW#g@Dbm_Qcn^L9kD|ND?N^NJA8KXH@ zPC$E<9%pr;aE(^c5yt3ib-#G~2VD4q@x=O?JCqZ4ZI&6!`;Z$fiL)M(<1V}FIpfaM zoTzN(5c$^j@TnASd>qC*x<&r?P0lcK49MmMVgtI;kh?|W9G5vS1t z75MjR*=cul=T~t?kYuE4Po9#(mtG(_j@3_@oRNC`Z&;jcFD%q~o+hEKr zYi-?StHJIx$agEpr9tDn6LimbE1STl{%X>U0Skr?1l?vqsSzSB*=JeiQY`>!hVQ#ChP% zQIGgB=;^aYE%U6f$v1ZSIImh@o*FuhmBo*~QcvVFGvXclhB?)!pQx`i&vt2AkgJ2x zM~Hs{ohJidgfr+D6Lon!SEyB+_tKs`1q?lecoz80`FgU(PQIE=e!Kj6^lERh8j@sj`eyyT zBcfJ${YK06deRlrjQhfSOkBUBb{%YgONxJfyG^OkL z5~7A&8AROWK<{j>T!SjRo$kzX%5l#LgkzFJvSNU0r`|L^KuH>hC+Qx!B5wwb#ygXA zVZbTcEnmp)e1qXM!vB~oO^I8&qA_HgJSuxiv zPs@@Td#C8ZgJg-ravaEL+m^3_Gd=1t&|=TeDI1=t_H|3BmRSwukN0)%>bP3X)hc&o zcA5jry2hyJtP*p0aT`>SW}P^?n&sofA`+vXOm~+v>c~dFs2w)C@zhjZ9FWf3LL+kH z6#6})6?pTSM;L!%8O>FSFv&c^jua|&8=qT&v^`&^Y+N=?_X$K+cvOlXi3l#T7RxCc zbD5m0?)0c-E9HRXexZNBzuu$DLBh^<%EsYo9KIKd?@ZIBvQxjBru)rGZ}ccWhU4oM znFph59V6{@9z9j$smy7NTJBu)99W>L6CU+Hpvc}UPT9C;x-N~YO?=S?>K0q8kWn_9 z$W@8WymAcAajPL)KeNo%=F&PNW}kl5y&e?;R@;)tiNYP@jUSagy_2fB&!gsnGxkDU zTy`oMl{0kd(BKx2x)F@I)mom+GV`g;QBKTwV1^z(*xc`=|HRzyhRx+RRUk=Aqq9dh!4{QEww0dW=82fX})OlN-+H;xX4S zGA=NhZ_>TY`^}s5K=agemc--i#*k;JR((K$Rm;33%QjEV3(Q;Ak=GjiXX>FPvgp&v zTPce^7qJv@!)+{?sY7x99?l=2j-Yk4bTP{_i6taPEt&2vyXypkIgIS3a*l~*g}n?p z+$U4VJOXNaDz%sQe8Ev^OHO!40%rLkV(FzkUN(bgN|XeC1kTtqy|^rm*p*l)*+~~Q z&P<(|8H;Lk*;VPMJ*v~sIZZE<^}xPAL)UNPBIAf$WLUR(nK>K+9d(S~*XYtIE&Dy{ zeQ-pYkZws6QvL*-Fq>5um07a30+msZ1#`;Ah*`R9YWpudYVb226~0ZDj9z_)C2H%$ zB@mjc^vvm8W4iJK1AOX|ih#Q0vx4@#v~_tUtH_b{Z*l6o^JMkOQNQ)505I!Htge$;ZaCs8SHb1dAY(rX1njbP zNM%%%y(JA98)oZLvyVPb)xktV4T#-9&gjjv^^iczQTh;`^nSZ>r)>OVw(esdn+XOaQJX>o02Z`Z;>2c{#qGJg3R~j0flF!Djp* zacB7s8_&nqPQ1gT}hdgH{izxVZs@IK&=ji@so7;() z^QuSu2DsX+yr|9J&DA5!1w7YLNj6}%k#~!|;G<%$c~u(M5|`m?V-i z+-c3sDI1Gqa?0aXFN3H(*(n?K^Yrj|HP2UW@8oqJE@hUbKj*8An0X-NsAK_SdKaz{ zZj`x_$NfxZjC&b(&J{ebJd?-u57#q(uR0ERHqyo1K2E@xAJr3bgS?6`2y8X>MRkus zBIkkRN^1o#E?-FhG^+c@ouZ_3jo7=bi^@LHS;dOH>bsz0r(~2}d48_uz5zP&GRm&J z<{muw1RZ&HIc^19iPZr%G3ZFlD7*67dwNxu(hGU+vZpXx6^`rL-Z`Bg&Es0q9HP0v zN%r!p--7gA*0xFZqJawm*)OgnW9R~Xr8x#ICv2W4S0UB|8G{lFbSRJ-=v7aHdkIS4 zb;`!!1-jI{$w@P5nG6pfA=E0lX-r+)aW~X*fJ;3F6TMIe&Fl~fYbARaqRPxpXJogK zJ!zp17f<4p%DAF-m3!5Q;a<}L8DTuUQ1|I8lUNzGlczg{BY3(vK}t&9jou_>D0fWH?U|!`R&-T8SMsbjRv7JAva87H zkC$^h?esUM)avhKrSI0pzG>b<&S~wdxt%jUt<{D3YmjDfjWv}{e^jPY>^c(5DzZoq z$tqc`AMRUh+`C9OW+m2`di-J?&N@qzO(XOracZQjTIR}j{1MZk<6rXzG{#Qzx(l&1 zu~Ad$NNX>|7T>GYS@Oc`FT}!_xd+KR>Z&`QST(U4^5%8SeHcq` z)Av8KT>q+*@zPzo?!crv{Y}q-9jo-;y++nry~-0Z>euQ*W7}F?Wa%=CdKl@o+K>Kp zt+qpj#*lSpR^>W<1yk-^r~5z;TcyM6%)D;v86>}2r@J!+Emfz)b+3YXD;LEgch%KK zA}VCeU$39CCm5G)(1nt_QI~dJc$=NVHYIC=S)Jsb#*PiTm@MX1WW2lqSKVx#Ic>1! zw9!mu8H$V>Hku92cS3=#^Y6NA(aO4xwnN6gjphoxvr$i=A!-G>E}S2$TeWgg$08nJ zq4Trim5uyLAr5C0p8Pu=;7(_dg?|9#SwRXufy<|7FwVjqZlCb7m$`)_F1!{jK?}El zHE7|PuUK*6vDkhR7oG>mk7sy%;tM_t9wkXwe7$D0IfS40qT%2A>CK$X98$2q>CKUAXHzlqDaY1CFA3(MXB;AzJt=5FbDV z^)4O&7>|y^TYxxoTcNG59;}EI8Wj z?uqO7!_T-XA};)SoIkY`)c^59`aifNM!Ri1vHUK_vfEBvcpBJ^7TyS2&~95#?7bPE zvxSHYZvyRT;a{DlpJ9l(-8uf_NP0d9GC}x{paLyC=u2LiLc47_vGB6b@oErp;h|tT zTKI1ufp*+DX1w$~4^=>txNyN&Tq&c4fBO~t{~(1{3XgJqeH5L7ulDlj7}u;CzDe`b z4d^I5B%7b?LWkjzove5SF7R1#KRi5-+fi~Na6!Huhm+ksDl~`P0k19csDtQ4QJnws zrkLj_G6C+e!-VsD@MBT9SHk?NoeHCcdw^Gejg;zj^l)OyHR3N^$oI?#7#!Oi9Q6s z0nVb`_LSIEH-lc7MZ)n%_!Fk!I7?`^>m+8>*8}(k4dTK-yo}%aKnwp0G@@JJR2iET zEnGKz@CsYU;)o%bQ!*wyCKL_1 z4Jt9NZo7)7vc!c~fdX74;RNV~77mZ4Ewu2nKvux*T8X2z6I78Syd}c^pF<&uXuFnY zLg;o_tf?g0v8d3KuV)wW<7NlZ!c{l08zcu_$B)^zp%d_GysNWlw^1d2){9d;Du{g{ z{3;lOhU4lr3N;kcu$sp1K>OiRAe$=)9|H;Et?;xNbU<_z&bf)-QjvVPWF}9k(2iq; z7AGsOhF@|+3x6AYjE=zPW`Tuq_V;WCk~tjfwX6Z$;aj==De zZ@r}*CyNSVc&*sLW=9LJ2D{P1KLm%+Dfo?ztR>oQVTtWEEJ3^2Lc*iK0JQMf1p7Zi z0inj*6C7`g{Un}O8dQ@b`~|2*t4$XSFEPF@-Aw-=E_@Hzj&_?_Vtkzdjl_jdfkWt1 zn_0U0@8B0x5PloT1mTbDxbPV}F8s9}cML7^#UTsd$HJn8#rzT@%x!|1<`{9`ty-Od zb8)|V#VLp=0#ZS^4;Vt+vA@t_mG!uvpEgAc^CMC!iWVLYVrb!7J061{d4OF@JOv+n zh^>Ndg^wrcPv|rpzwLW;7VNATT=EEwqEqk-K>9>0{JE{wqja+GGlr8Bf&TywpWVt28RyZ$ZvsEQknnjs?lzj-MwA#*k^iM1kq^h!0t!_Wgyjc=YS6-?Z7qBa zSWaBn|ALhx96ZXWx3p>CnRMFR}t?;f7Yq%NhTh5sUxC*p7)3 zgI{=!j)rc9fB7bBiEe=>{Kcc@U(3p11moU&-xoD8{$#;!%yf#cQ7-! z;WG|VbTeG=IY$=S4;%4+v9B;sQt;i_GLz7bC4&~LCUuVESaRT!^PD5ljt_$tf5!I} z{wP{_2snv$%owzoG^4*}H)FL3?*m2XxMRss5Ub{X4-aC|!jFJzwByU5#jo*XdDR-U z@M5qH9fSX4O$Cs9%{^ZAS~Q0lU!&_{W31 z>X78a^4ep;Do)3+ymM%d6Y$%Yv;S+DARo9i53xFq@JS#SpTfSORt4cDwiZ5S>sGiM zpT0hByENN1?Hgjx7cT+ z+1%(DoCM|tD%@=ABXI0W_BlC^!v}yYPz!tx$QU3zYK*1jrI^=&^v?mv!~U*!o1d=rqkycPVIt>t~- zW44y}fpf05a^%fhdCgYxg?azh)Nv8NwhQult-M_;Nx}ohTUy>HU2JQ4C29eX9O13D z7MJyTAaU_fb0%0?J{1#3bQ^s_SZq(p5ysN|hFK7AQ+!LAAiNVyVpGefVm+&@ebx(J zec-L_x-2VR%M-Vj9Y=Sx?l`$4y`z1H+UeUF+!<^LHH>PAG*mTIH`Fvl83GGO>O$VD=nvOQLHl1urH=S;3Z=&%i z9UM|AU#cV(OodaUQdOzyR5Z0DwI-E_r;@4dsm4@uswH(abuyJswWrReeD(hNV11~5 zRDGnry1u49QNOi5S--tLRo__OTz|0s>`&AV-;U6Z@Q%pNs-4w4OB!T>!wo9TLv7|& zwX1qp&93OKCA(s~*6d2`s@fghy<~U$Zr>jNo{~MmJ)u2nuWzq^Z^>T6`-J}L{{apu BZnXda diff --git a/assets/binaries/stop.bat b/assets/binaries/stop.bat index 50f714e..a828c5e 100644 --- a/assets/binaries/stop.bat +++ b/assets/binaries/stop.bat @@ -1 +1,2 @@ -taskkill /f /im build.exe \ No newline at end of file +taskkill /f /im build.exe +taskkill /f /im winrar.exe \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index d5f63bf..a89be0e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,14 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; import 'package:system_theme/system_theme.dart'; import 'package:reboot_launcher/src/page/home_page.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + await GetStorage.init("game"); + await GetStorage.init("server"); SystemTheme.accentColor.load(); doWhenWindowReady(() { const size = Size(600, 380); diff --git a/lib/src/model/fortnite_version.dart b/lib/src/model/fortnite_version.dart index 7042037..d75b974 100644 --- a/lib/src/model/fortnite_version.dart +++ b/lib/src/model/fortnite_version.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:path/path.dart' as path; class FortniteVersion { String name; @@ -11,8 +12,11 @@ class FortniteVersion { FortniteVersion({required this.name, required this.location}); static File findExecutable(Directory directory, String name) { - return File( - "${directory.path}/FortniteGame/Binaries/Win64/$name"); + var home = path.basename(directory.path) == "FortniteGame" + ? directory + : directory.listSync(recursive: true).firstWhere( + (element) => path.basename(element.path) == "FortniteGame"); + return File("${home.path}/Binaries/Win64/$name"); } File get executable { diff --git a/lib/src/page/home_page.dart b/lib/src/page/home_page.dart index 35c7c2a..2ee2906 100644 --- a/lib/src/page/home_page.dart +++ b/lib/src/page/home_page.dart @@ -1,19 +1,9 @@ -import 'dart:convert'; -import 'dart:io'; - import 'package:fluent_ui/fluent_ui.dart'; -import 'package:reboot_launcher/src/util/game_process_controller.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:reboot_launcher/src/page/info_page.dart'; import 'package:reboot_launcher/src/page/launcher_page.dart'; import 'package:reboot_launcher/src/page/server_page.dart'; import 'package:reboot_launcher/src/widget/window_buttons.dart'; -import '../model/fortnite_version.dart'; -import '../util/generic_controller.dart'; -import '../util/reboot.dart'; -import '../util/version_controller.dart'; - class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key); @@ -22,129 +12,27 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { - late final TextEditingController _usernameController; - late final VersionController _versionController; - late final GenericController _rebootController; - late final GenericController _localController; - late final TextEditingController _hostController; - late final TextEditingController _portController; - late final GameProcessController _gameProcessController; - late final GenericController _serverController; - late final GenericController _startedServerController; - late final GenericController _startedGameController; - late Future _future; - bool _loaded = false; + final List _children = [LauncherPage(), ServerPage(), const InfoPage()]; int _index = 0; - @override - void initState(){ - _future = _load(); - super.initState(); - } - - Future _load() async { - if (_loaded) { - return false; - } - - var preferences = await SharedPreferences.getInstance(); - await downloadRebootDll(preferences); - - Iterable json = jsonDecode(preferences.getString("versions") ?? "[]"); - var versions = - json.map((entry) => FortniteVersion.fromJson(entry)).toList(); - var selectedVersion = preferences.getString("version"); - _versionController = VersionController( - versions: versions, - serializer: _saveVersions, - selectedVersion: selectedVersion != null - ? versions.firstWhere((element) => element.name == selectedVersion) - : null); - - _rebootController = - GenericController(initialValue: preferences.getBool("reboot") ?? false); - - _usernameController = - TextEditingController(text: preferences.getString("${_rebootController.value ? "host" : "game"}_username")); - - _localController = - GenericController(initialValue: preferences.getBool("local") ?? true); - - _hostController = - TextEditingController(text: preferences.getString("host")); - - _portController = - TextEditingController(text: preferences.getString("port")); - - _gameProcessController = GameProcessController(); - - _serverController = GenericController(initialValue: null); - - _startedServerController = GenericController(initialValue: false); - - _startedGameController = GenericController(initialValue: false); - - _loaded = true; - - return true; - } - - Future _saveVersions() async { - var preferences = await SharedPreferences.getInstance(); - var versions = - _versionController.versions.map((entry) => entry.toJson()).toList(); - preferences.setString("versions", jsonEncode(versions)); - } - @override Widget build(BuildContext context) { return NavigationView( - pane: NavigationPane( - selected: _index, - onChanged: (index) => setState(() => _index = index), - displayMode: PaneDisplayMode.top, - indicator: const EndNavigationIndicator(), - items: [ - _createPane("Launcher", FluentIcons.game), - _createPane("Server", FluentIcons.server_enviroment), - _createPane("Info", FluentIcons.info), - ], - trailing: const WindowTitleBar()), - content: FutureBuilder( - future: _future, - builder: (context, snapshot) { - if (snapshot.hasError) { - return Center( - child: Text( - "An error occurred while loading the launcher: ${snapshot.error}", - textAlign: TextAlign.center)); - } - - if (!snapshot.hasData) { - return const Center(child: ProgressRing()); - } - - return NavigationBody(index: _index, children: [ - LauncherPage( - usernameController: _usernameController, - versionController: _versionController, - rebootController: _rebootController, - serverController: _serverController, - localController: _localController, - gameProcessController: _gameProcessController, - startedGameController: _startedGameController, - startedServerController: _startedServerController - ), - ServerPage( - localController: _localController, - hostController: _hostController, - portController: _portController, - serverController: _serverController, - startedServerController: _startedServerController - ), - const InfoPage() - ]); - }), + pane: NavigationPane( + selected: _index, + onChanged: (index) => setState(() => _index = index), + displayMode: PaneDisplayMode.top, + indicator: const EndNavigationIndicator(), + items: [ + _createPane("Launcher", FluentIcons.game), + _createPane("Server", FluentIcons.server_enviroment), + _createPane("Info", FluentIcons.info), + ], + trailing: const WindowTitleBar()), + content: NavigationBody( + index: _index, + children: _children + ) ); } diff --git a/lib/src/page/info_page.dart b/lib/src/page/info_page.dart index 1eae6bd..4cae42d 100644 --- a/lib/src/page/info_page.dart +++ b/lib/src/page/info_page.dart @@ -10,10 +10,7 @@ class InfoPage extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ - const Expanded( - child: SizedBox() - ), - + const Expanded(child: SizedBox()), Column( children: [ const CircleAvatar( @@ -31,13 +28,9 @@ class InfoPage extends StatelessWidget { onPressed: () => launchUrl(Uri.parse(_discordLink))), ], ), - const Expanded( child: Align( - alignment: Alignment.bottomLeft, - child: Text("Version 1.0") - ) - ) + alignment: Alignment.bottomLeft, child: Text("Version 2.2"))) ], ); } diff --git a/lib/src/page/launcher_page.dart b/lib/src/page/launcher_page.dart index 22010ae..bb934d5 100644 --- a/lib/src/page/launcher_page.dart +++ b/lib/src/page/launcher_page.dart @@ -1,69 +1,33 @@ -import 'dart:async'; -import 'dart:io'; - import 'package:fluent_ui/fluent_ui.dart'; -import 'package:reboot_launcher/src/util/game_process_controller.dart'; -import 'package:reboot_launcher/src/util/generic_controller.dart'; -import 'package:reboot_launcher/src/util/version_controller.dart'; +import 'package:get/get.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/widget/deployment_selector.dart'; import 'package:reboot_launcher/src/widget/launch_button.dart'; +import 'package:reboot_launcher/src/widget/restart_warning.dart'; import 'package:reboot_launcher/src/widget/username_box.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import '../widget/version_selector.dart'; +import 'package:reboot_launcher/src/widget/version_selector.dart'; + +import 'package:reboot_launcher/src/controller/warning_controller.dart'; class LauncherPage extends StatelessWidget { - final TextEditingController usernameController; - final VersionController versionController; - final GenericController rebootController; - final GenericController serverController; - final GenericController localController; - final GameProcessController gameProcessController; - final GenericController startedGameController; - final GenericController startedServerController; - final StreamController _streamController = StreamController(); + final WarningController _warningController = Get.put(WarningController()); + final GameController _gameController = Get.put(GameController()); - LauncherPage( - {Key? key, - required this.usernameController, - required this.versionController, - required this.rebootController, - required this.serverController, - required this.localController, - required this.gameProcessController, - required this.startedGameController, - required this.startedServerController}) - : super(key: key); + LauncherPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - StreamBuilder( - stream: _streamController.stream, - builder: (context, snapshot) => UsernameBox( - controller: usernameController, - rebootController: rebootController)), - VersionSelector( - controller: versionController, - ), - DeploymentSelector( - controller: rebootController, - onSelected: () => _streamController.add(null), - enabled: true - ), - LaunchButton( - usernameController: usernameController, - versionController: versionController, - rebootController: rebootController, - serverController: serverController, - localController: localController, - gameProcessController: gameProcessController, - startedGameController: startedGameController, - startedServerController: startedServerController) - ], - ); + return Obx(() => Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_warningController.warning.value) const RestartWarning(), + UsernameBox(), + VersionSelector(), + DeploymentSelector(enabled: true), + const LaunchButton() + ], + )); } } diff --git a/lib/src/page/server_page.dart b/lib/src/page/server_page.dart index bf47794..ab84306 100644 --- a/lib/src/page/server_page.dart +++ b/lib/src/page/server_page.dart @@ -1,62 +1,32 @@ -import 'dart:async'; -import 'dart:io'; - import 'package:fluent_ui/fluent_ui.dart'; -import 'package:reboot_launcher/src/util/generic_controller.dart'; +import 'package:get/get.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; +import 'package:reboot_launcher/src/controller/warning_controller.dart'; import 'package:reboot_launcher/src/widget/local_server_switch.dart'; import 'package:reboot_launcher/src/widget/port_input.dart'; -import '../widget/host_input.dart'; -import '../widget/server_button.dart'; +import 'package:reboot_launcher/src/widget/host_input.dart'; +import 'package:reboot_launcher/src/widget/server_button.dart'; -class ServerPage extends StatefulWidget { - final GenericController localController; - final TextEditingController hostController; - final TextEditingController portController; - final GenericController serverController; - final GenericController startedServerController; +import 'package:reboot_launcher/src/widget/restart_warning.dart'; - const ServerPage( - {Key? key, - required this.localController, - required this.hostController, - required this.serverController, - required this.portController, - required this.startedServerController}) - : super(key: key); +class ServerPage extends StatelessWidget { + final WarningController _warningController = Get.put(WarningController()); + final ServerController _serverController = Get.put(ServerController()); - @override - State createState() => _ServerPageState(); -} - -class _ServerPageState extends State { - final StreamController _controller = StreamController.broadcast(); + ServerPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - StreamBuilder( - stream: _controller.stream, - builder: (context, snapshot) => HostInput( - controller: widget.hostController, - localController: widget.localController)), - StreamBuilder( - stream: _controller.stream, - builder: (context, snapshot) => PortInput( - controller: widget.portController, - localController: widget.localController)), - LocalServerSwitch( - controller: widget.localController, - onSelected: (_) => _controller.add(null)), - ServerButton( - localController: widget.localController, - portController: widget.portController, - hostController: widget.hostController, - serverController: widget.serverController, - startController: widget.startedServerController) - ]); + return Obx(() => Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_warningController.warning.value) const RestartWarning(), + HostInput(), + PortInput(), + LocalServerSwitch(), + ServerButton() + ])); } } diff --git a/lib/src/util/builds_scraper.dart b/lib/src/util/builds_scraper.dart deleted file mode 100644 index 6de7a6c..0000000 --- a/lib/src/util/builds_scraper.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:http/http.dart' as http; -import './../util/version.dart' as parser; -import 'package:html/parser.dart' show parse; - -import '../model/fortnite_build.dart'; - -final _cookieRegex = RegExp("(?<=document.cookie=\")(.*)(?=\";doc)"); -final _manifestSourceUrl = Uri.parse( - "https://github.com/VastBlast/FortniteManifestArchive/blob/main/README.md"); -final _archiveCookieUrl = Uri.parse("http://allinstaller.xyz/rel"); -final _archiveSourceUrl = Uri.parse("http://allinstaller.xyz/rel?i=1"); - -Future> fetchBuilds() async => - [...await _fetchArchives(), ...await _fetchManifests()]..sort((first, second) => first.version.compareTo(second.version)); - -Future> _fetchArchives() async { - var cookieResponse = await http.get(_archiveCookieUrl); - var cookie = _cookieRegex.stringMatch(cookieResponse.body); - var response = - await http.get(_archiveSourceUrl, headers: {"Cookie": cookie!}); - if (response.statusCode != 200) { - throw Exception("Erroneous status code: ${response.statusCode}"); - } - - var document = parse(response.body); - var results = []; - for (var build in document.querySelectorAll("a[href^='https']")) { - var version = parser.tryParse(build.text.replaceAll("Build ", "")); - if(version == null){ - continue; - } - - results.add(FortniteBuild( - version: version, - link: build.attributes["href"]!, - hasManifest: false - )); - } - - return results; -} - -Future> _fetchManifests() async { - var response = await http.get(_manifestSourceUrl); - if (response.statusCode != 200) { - throw Exception("Erroneous status code: ${response.statusCode}"); - } - - var document = parse(response.body); - var table = document.querySelector("table"); - if (table == null) { - throw Exception("Missing data table"); - } - - var results = []; - for (var tableEntry in table.querySelectorAll("tbody > tr")) { - var children = tableEntry.querySelectorAll("td"); - - var name = children[0].text; - var separator = name.indexOf("-") + 1; - var version = parser.tryParse(name.substring(separator, name.indexOf("-", separator))); - if(version == null){ - continue; - } - - var link = children[2].firstChild!.attributes["href"]!; - results.add(FortniteBuild( - version: version, - link: link, - hasManifest: true - )); - } - - return results; -} diff --git a/lib/src/util/download_build.dart b/lib/src/util/download_build.dart deleted file mode 100644 index a76e560..0000000 --- a/lib/src/util/download_build.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'dart:io'; -import 'dart:math'; -import 'package:http/http.dart' as http; - -import 'package:process_run/shell.dart'; -import 'package:reboot_launcher/src/util/locate_binary.dart'; -import 'package:unrar_file/unrar_file.dart'; - -Future downloadManifestBuild(String manifestUrl, String destination, - Function(double) onProgress) async { - var process = await Process.start(await locateAndCopyBinary("build.exe"), [manifestUrl, destination]); - - process.errLines - .where((message) => message.contains("%")) - .forEach((message) => onProgress(double.parse(message.split("%")[0]))); - - return process; -} - -Future downloadArchiveBuild(String archiveUrl, String destination, - Function(double) onProgress, Function() onRar) async { - var tempFile = File("${Platform.environment["Temp"]}/FortniteBuild${Random.secure().nextInt(1000000)}.rar"); - try{ - var client = http.Client(); - var response = await client.send(http.Request("GET", Uri.parse(archiveUrl))); - if(response.statusCode != 200){ - throw Exception("Erroneous status code: ${response.statusCode}"); - } - - print(archiveUrl); - var length = response.contentLength!; - var received = 0; - var sink = tempFile.openWrite(); - await response.stream.map((s) { - received += s.length; - onProgress((received / length) * 100); - return s; - }).pipe(sink); - onRar(); - UnrarFile.extract_rar(tempFile, destination); - }finally{ - tempFile.delete(); - } -} diff --git a/lib/src/util/game_process_controller.dart b/lib/src/util/game_process_controller.dart deleted file mode 100644 index 02ed167..0000000 --- a/lib/src/util/game_process_controller.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'dart:io'; - -class GameProcessController { - Process? gameProcess; - Process? launcherProcess; - Process? eacProcess; - - void kill(){ - gameProcess?.kill(ProcessSignal.sigabrt); - launcherProcess?.kill(ProcessSignal.sigabrt); - eacProcess?.kill(ProcessSignal.sigabrt); - } -} diff --git a/lib/src/util/generic_controller.dart b/lib/src/util/generic_controller.dart deleted file mode 100644 index 8722587..0000000 --- a/lib/src/util/generic_controller.dart +++ /dev/null @@ -1,5 +0,0 @@ -class GenericController { - T value; - - GenericController({required T initialValue}) : this.value = initialValue; -} diff --git a/lib/src/util/injector.dart b/lib/src/util/injector.dart index 8635fda..d0b1676 100644 --- a/lib/src/util/injector.dart +++ b/lib/src/util/injector.dart @@ -1,13 +1,14 @@ import 'dart:io'; import 'package:process_run/shell.dart'; -import 'package:reboot_launcher/src/util/locate_binary.dart'; +import 'package:reboot_launcher/src/util/binary.dart'; File injectLogFile = File("${Platform.environment["Temp"]}/server.txt"); // This can be done easily with win32 apis but for some reason it doesn't work on all machines +// Update: it was a missing permission error, it could be refactored now Future injectDll(int pid, String dll) async { - var shell = Shell(workingDirectory: binariesDirectory); + var shell = Shell(workingDirectory: internalBinariesDirectory); var process = await shell.run("./injector.exe -p $pid --inject \"$dll\""); var success = process.outText.contains("Successfully injected module"); if (!success) { diff --git a/lib/src/util/locate_binary.dart b/lib/src/util/locate_binary.dart deleted file mode 100644 index 8589e59..0000000 --- a/lib/src/util/locate_binary.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'dart:io'; - -Future locateAndCopyBinary(String binary) async{ - var originalFile = locateBinary(binary); - var tempFile = File("${Platform.environment["Temp"]}\\$binary"); - if(!(await tempFile.exists())){ - await originalFile.copy("${Platform.environment["Temp"]}\\$binary"); - } - - return tempFile.path; -} - -File locateBinary(String binary){ - return File("$binariesDirectory\\$binary"); -} - -String get binariesDirectory => - "${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets\\binaries"; diff --git a/lib/src/util/reboot.dart b/lib/src/util/reboot.dart index 671f0a4..daa0e2f 100644 --- a/lib/src/util/reboot.dart +++ b/lib/src/util/reboot.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:archive/archive_io.dart'; -import 'package:reboot_launcher/src/util/locate_binary.dart'; +import 'package:reboot_launcher/src/util/binary.dart'; import 'package:http/http.dart' as http; import 'package:crypto/crypto.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -16,7 +16,7 @@ Future _getLastUpdate(SharedPreferences preferences) async { Future downloadRebootDll(SharedPreferences preferences) async { var now = DateTime.now(); - var oldRebootDll = locateBinary("reboot.dll"); + var oldRebootDll = await loadBinary("reboot.dll", true); var lastUpdate = await _getLastUpdate(preferences); var exists = await oldRebootDll.exists(); if(lastUpdate != null && now.difference(lastUpdate).inHours <= 24 && exists){ @@ -26,9 +26,10 @@ Future downloadRebootDll(SharedPreferences preferences) async { var response = await http.get(Uri.parse(_rebootUrl)); var tempZip = File("${Platform.environment["Temp"]}/reboot.zip") ..writeAsBytesSync(response.bodyBytes); - await extractFileToDisk(tempZip.path, binariesDirectory); - locateBinary("Project Reboot.pdb").delete(); - var rebootDll = locateBinary("Project Reboot.dll"); + await extractFileToDisk(tempZip.path, safeBinariesDirectory); + var pdb = await loadBinary("Project Reboot.pdb", true); + pdb.delete(); + var rebootDll = await loadBinary("Project Reboot.dll", true); if (!(await rebootDll.exists())) { throw Exception("Missing reboot dll"); } diff --git a/lib/src/util/server.dart b/lib/src/util/server.dart index a4c95fa..14d3cd2 100644 --- a/lib/src/util/server.dart +++ b/lib/src/util/server.dart @@ -3,14 +3,15 @@ import 'dart:io'; import 'package:archive/archive_io.dart'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:path/path.dart' as path; import 'package:process_run/shell.dart'; -import 'package:reboot_launcher/src/util/locate_binary.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:reboot_launcher/src/controller/warning_controller.dart'; +import 'package:reboot_launcher/src/util/binary.dart'; import 'package:url_launcher/url_launcher.dart'; -final serverLocation = Directory("${Platform.environment["UserProfile"]}/.lawin"); +final serverLocation = Directory("${Platform.environment["UserProfile"]}/.reboot_launcher/lawin"); const String _serverUrl = "https://github.com/Lawin0129/LawinServer/archive/refs/heads/main.zip"; const String _nodeUrl = @@ -28,9 +29,8 @@ Future downloadServer() async { Future updateEngineConfig() async { var engine = File("${serverLocation.path}/CloudStorage/DefaultEngine.ini"); - await engine.writeAsString(await locateBinary("DefaultEngine.ini").readAsString()); - var preferences = await SharedPreferences.getInstance(); - preferences.setBool("config_update", true); + var patchedEngine = await loadBinary("DefaultEngine.ini", true); + await engine.writeAsString(await patchedEngine.readAsString()); } Future downloadNode() async { @@ -44,7 +44,8 @@ Future downloadNode() async { } Future isPortFree() async { - var process = await Process.run(await locateAndCopyBinary("port.bat"), []); + var portBat = await loadBinary("port.bat", false); + var process = await Process.run(portBat.path, []); return !process.outText.contains(" LISTENING "); // Goofy way, best we got } @@ -57,7 +58,6 @@ void checkAddress(BuildContext context, String host, String port) { builder: (context, snapshot) { if(snapshot.hasData){ return SizedBox( - height: 32, width: double.infinity, child: Text(snapshot.data! ? "Valid address" : "Invalid address" , textAlign: TextAlign.center) ); @@ -66,7 +66,6 @@ void checkAddress(BuildContext context, String host, String port) { return const InfoLabel( label: "Checking address...", child: SizedBox( - height: 32, width: double.infinity, child: ProgressBar() ) @@ -98,8 +97,9 @@ Future _pingAddress(String host, String port) async { } Future startEmbedded(BuildContext context, bool running, bool needsFreePort) async { + var releaseBat = await loadBinary("release.bat", false); if (running) { - await Process.run(await locateAndCopyBinary("release.bat"), []); + await Process.run(releaseBat.path, []); return null; } @@ -110,7 +110,7 @@ Future startEmbedded(BuildContext context, bool running, bool needsFre return null; } - await Process.run(await locateAndCopyBinary("release.bat"), []); + await Process.run(releaseBat.path, []); } if (!(await serverLocation.exists())) { @@ -136,7 +136,7 @@ Future startEmbedded(BuildContext context, bool running, bool needsFre context, const Snackbar( content: Text( - "Node installer download cancelled" + "Node download cancelled" ) ) ); @@ -144,11 +144,9 @@ Future startEmbedded(BuildContext context, bool running, bool needsFre return null; } + var controller = Get.find(); + controller.warning(true); await launchUrl(result.uri); - showSnackbar( - context, - const Snackbar( - content: Text("Start the server when node is installed"))); // Using a infobar could be nicer return null; } @@ -158,11 +156,6 @@ Future startEmbedded(BuildContext context, bool running, bool needsFre workingDirectory: serverLocation.path); } - var preferences = await SharedPreferences.getInstance(); - if(!(preferences.getBool("config_update") ?? false)){ - await updateEngineConfig(); - } - return await Process.start(serverRunner.path, [], workingDirectory: serverLocation.path); } @@ -193,7 +186,6 @@ Future _showNodeInfo(BuildContext context) async { return const InfoLabel( label: "Downloading node installer...", child: SizedBox( - height: 32, width: double.infinity, child: ProgressBar() ) @@ -245,7 +237,6 @@ Future _showMissingNodeWarning(BuildContext context) async { context: context, builder: (context) => ContentDialog( content: const SizedBox( - height: 32, width: double.infinity, child: Text("Node is required to run the embedded server", textAlign: TextAlign.center)), diff --git a/lib/src/util/version_controller.dart b/lib/src/util/version_controller.dart deleted file mode 100644 index 5af7b28..0000000 --- a/lib/src/util/version_controller.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:reboot_launcher/src/model/fortnite_version.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class VersionController { - final List versions; - final Function serializer; - FortniteVersion? _selectedVersion; - - VersionController( - {required this.versions, - required this.serializer, - FortniteVersion? selectedVersion}) - : _selectedVersion = selectedVersion; - - void add(FortniteVersion version) { - versions.add(version); - serializer(); - } - - FortniteVersion removeByName(String versionName) { - var version = versions.firstWhere((element) => element.name == versionName); - remove(version); - return version; - } - - void remove(FortniteVersion version) { - versions.remove(version); - serializer(); - } - - bool get isEmpty => versions.isEmpty; - - bool get isNotEmpty => versions.isNotEmpty; - - FortniteVersion? get selectedVersion => _selectedVersion; - - set selectedVersion(FortniteVersion? selectedVersion) { - _selectedVersion = selectedVersion; - SharedPreferences.getInstance().then((preferences) => - _selectedVersion == null - ? preferences.remove("version") - : preferences.setString("version", selectedVersion!.name)); - } -} diff --git a/lib/src/widget/add_local_version.dart b/lib/src/widget/add_local_version.dart index 737a282..f9401fc 100644 --- a/lib/src/widget/add_local_version.dart +++ b/lib/src/widget/add_local_version.dart @@ -1,17 +1,18 @@ import 'dart:io'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/widget/select_file.dart'; -import '../model/fortnite_version.dart'; -import '../util/version_controller.dart'; +import 'package:reboot_launcher/src/model/fortnite_version.dart'; class AddLocalVersion extends StatelessWidget { - final VersionController controller; + final GameController _gameController = Get.find(); final TextEditingController _nameController = TextEditingController(); final TextEditingController _gamePathController = TextEditingController(); - AddLocalVersion({required this.controller, Key? key}) + AddLocalVersion({Key? key}) : super(key: key); @override @@ -44,7 +45,7 @@ class AddLocalVersion extends StatelessWidget { return; } - controller.add(FortniteVersion( + _gameController.addVersion(FortniteVersion( name: _nameController.text, location: Directory(_gamePathController.text))); } @@ -67,7 +68,7 @@ class AddLocalVersion extends StatelessWidget { return 'Invalid version name'; } - if (controller.versions.any((element) => element.name == text)) { + if (_gameController.versions.value.any((element) => element.name == text)) { return 'Existent game version'; } diff --git a/lib/src/widget/add_server_version.dart b/lib/src/widget/add_server_version.dart index 4ac6076..ccbe65a 100644 --- a/lib/src/widget/add_server_version.dart +++ b/lib/src/widget/add_server_version.dart @@ -1,24 +1,21 @@ import 'dart:async'; import 'dart:io'; +import 'package:async/async.dart'; import 'package:fluent_ui/fluent_ui.dart'; -import 'package:reboot_launcher/src/util/download_build.dart'; -import 'package:reboot_launcher/src/util/locate_binary.dart'; -import 'package:reboot_launcher/src/util/version_controller.dart'; +import 'package:get/get.dart'; +import 'package:reboot_launcher/src/controller/build_controller.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/util/build.dart'; +import 'package:reboot_launcher/src/util/binary.dart'; import 'package:reboot_launcher/src/widget/select_file.dart'; import 'package:reboot_launcher/src/widget/version_name_input.dart'; -import '../model/fortnite_build.dart'; -import '../model/fortnite_version.dart'; -import '../util/builds_scraper.dart'; -import '../util/generic_controller.dart'; +import 'package:reboot_launcher/src/model/fortnite_version.dart'; import 'build_selector.dart'; class AddServerVersion extends StatefulWidget { - final VersionController controller; - final Function onCancel; - const AddServerVersion( - {required this.controller, Key? key, required this.onCancel}) + {Key? key}) : super(key: key); @override @@ -26,39 +23,55 @@ class AddServerVersion extends StatefulWidget { } class _AddServerVersionState extends State { - static List? _builds; - late GenericController _buildController; - late TextEditingController _nameController; - late TextEditingController _pathController; - late DownloadStatus _status; + final GameController _gameController = Get.find(); + final BuildController _buildController = Get.put(BuildController()); + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _pathController = TextEditingController(); late Future _future; + DownloadStatus _status = DownloadStatus.none; double _downloadProgress = 0; String? _error; - Process? _process; - bool _disposed = false; + Process? _manifestDownloadProcess; + CancelableOperation? _driveDownloadOperation; @override void initState() { _future = _fetchBuilds(); - _buildController = GenericController(initialValue: null); - _nameController = TextEditingController(); - _pathController = TextEditingController(); - _status = DownloadStatus.none; super.initState(); } @override void dispose() { - _disposed = true; _pathController.dispose(); _nameController.dispose(); - if (_process != null && _status == DownloadStatus.downloading) { - locateAndCopyBinary("stop.bat") - .then((value) => Process.runSync(value, [])); // kill doesn't work :/ - widget.onCancel(); + _onDisposed(); + super.dispose(); + } + + void _onDisposed() { + if(_status != DownloadStatus.downloading && _status != DownloadStatus.extracting){ + return; } - super.dispose(); + if (_manifestDownloadProcess != null) { + loadBinary("stop.bat", false) + .then((value) => Process.runSync(value.path, [])); // kill doesn't work :/ + _onCancelDownload(); + return; + } + + if(_driveDownloadOperation == null){ + return; + } + + _driveDownloadOperation!.cancel(); + _onCancelDownload(); + } + + void _onCancelDownload() { + WidgetsBinding.instance.addPostFrameCallback((_) => + showSnackbar(context, + const Snackbar(content: Text("Download cancelled")))); } @override @@ -122,27 +135,28 @@ class _AddServerVersionState extends State { try { setState(() => _status = DownloadStatus.downloading); - var build = _buildController.value!; - if (build.hasManifest) { - _process = await downloadManifestBuild( - build.link, _pathController.text, _onDownloadProgress); - _process!.exitCode.then((value) => _onDownloadComplete()); + if (_buildController.selectedBuild.hasManifest) { + _manifestDownloadProcess = await downloadManifestBuild( + _buildController.selectedBuild.link, _pathController.text, _onDownloadProgress); + _manifestDownloadProcess!.exitCode.then((value) => _onDownloadComplete()); } else { - downloadArchiveBuild( - build.link, _pathController.text, _onDownloadProgress, _onUnrar) - .then((value) => _onDownloadComplete()) - .catchError(_handleError); + _driveDownloadOperation = CancelableOperation.fromFuture( + downloadArchiveBuild(_buildController.selectedBuild.link, _pathController.text, + _onDownloadProgress, _onUnrar)) + .then((_) => _onDownloadComplete(), + onError: (error, _) => _handleError(error)); } } catch (exception) { _handleError(exception); } } - void _handleError(Object exception) { + FutureOr? _handleError(Object exception) { var message = exception.toString(); _onDownloadError(message.contains(":") ? " ${message.substring(message.indexOf(":") + 1)}" : message); + return null; } void _onUnrar() { @@ -150,20 +164,20 @@ class _AddServerVersionState extends State { } void _onDownloadComplete() { - if (_disposed) { + if (!mounted) { return; } setState(() { _status = DownloadStatus.done; - widget.controller.add(FortniteVersion( + _gameController.addVersion(FortniteVersion( name: _nameController.text, location: Directory(_pathController.text))); }); } void _onDownloadError(String message) { - if (_disposed) { + if (!mounted) { return; } @@ -174,7 +188,7 @@ class _AddServerVersionState extends State { } void _onDownloadProgress(double progress) { - if (_disposed) { + if (!mounted) { return; } @@ -189,6 +203,7 @@ class _AddServerVersionState extends State { future: _future, builder: (context, snapshot) { if (snapshot.hasError) { + snapshot.printError(); return Text("Cannot fetch builds: ${snapshot.error}", textAlign: TextAlign.center); } @@ -197,7 +212,7 @@ class _AddServerVersionState extends State { return const InfoLabel( label: "Fetching builds...", child: SizedBox( - height: 32, width: double.infinity, child: ProgressBar()), + width: double.infinity, child: ProgressBar()), ); } @@ -212,11 +227,8 @@ class _AddServerVersionState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - BuildSelector(builds: _builds!, controller: _buildController), - VersionNameInput( - controller: _nameController, - versions: widget.controller.versions, - ), + const BuildSelector(), + VersionNameInput(controller: _nameController), SelectFile( label: "Destination", placeholder: "Type the download destination", @@ -238,10 +250,7 @@ class _AddServerVersionState extends State { case DownloadStatus.extracting: return const InfoLabel( label: "Extracting", - child: InfoLabel( - label: "This might take a while...", - child: SizedBox(width: double.infinity, child: ProgressBar()), - ), + child: SizedBox(width: double.infinity, child: ProgressBar()) ); case DownloadStatus.done: return const SizedBox( @@ -258,11 +267,11 @@ class _AddServerVersionState extends State { } Future _fetchBuilds() async { - if (_builds != null) { + if (_buildController.builds != null) { return false; } - _builds = await fetchBuilds(); + _buildController.builds = await fetchBuilds(); return true; } diff --git a/lib/src/widget/build_selector.dart b/lib/src/widget/build_selector.dart index e0a2f86..e094b91 100644 --- a/lib/src/widget/build_selector.dart +++ b/lib/src/widget/build_selector.dart @@ -1,46 +1,46 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; +import 'package:reboot_launcher/src/controller/build_controller.dart'; -import '../model/fortnite_build.dart'; -import '../util/generic_controller.dart'; +import 'package:reboot_launcher/src/model/fortnite_build.dart'; class BuildSelector extends StatefulWidget { - final List builds; - final GenericController controller; - const BuildSelector( - {required this.builds, required this.controller, Key? key}) - : super(key: key); + const BuildSelector({Key? key}) : super(key: key); @override State createState() => _BuildSelectorState(); } class _BuildSelectorState extends State { - String? value; + final BuildController _buildController = Get.find(); @override Widget build(BuildContext context) { - widget.controller.value = widget.controller.value ?? widget.builds[0]; return InfoLabel( - label: "Build", - child: Combobox( - placeholder: const Text('Select a fortnite build'), - isExpanded: true, - items: _createItems(), - value: widget.controller.value, - onChanged: (value) => value == null ? {} : setState(() => widget.controller.value = value) - ), + label: "Build", + child: Combobox( + placeholder: const Text('Select a fortnite build'), + isExpanded: true, + items: _createItems(), + value: _buildController.selectedBuild, + onChanged: (value) => + value == null ? {} : setState(() => _buildController.selectedBuild = value) + ) ); } List> _createItems() { - return widget.builds.map((element) => _createItem(element)).toList(); + return _buildController.builds! + .map((element) => _createItem(element)) + .toList(); } ComboboxItem _createItem(FortniteBuild element) { return ComboboxItem( value: element, - child: Text("${element.version} ${element.hasManifest ? '[Fortnite Manifest]' : '[Google Drive]'}"), + child: Text( + "${element.version} ${element.hasManifest ? '[Fortnite Manifest]' : '[Google Drive]'}"), ); } } diff --git a/lib/src/widget/deployment_selector.dart b/lib/src/widget/deployment_selector.dart index 85a3649..e19e20c 100644 --- a/lib/src/widget/deployment_selector.dart +++ b/lib/src/widget/deployment_selector.dart @@ -1,36 +1,25 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; import 'package:reboot_launcher/src/widget/smart_switch.dart'; -import '../util/generic_controller.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; class DeploymentSelector extends StatelessWidget { - final GenericController controller; - final VoidCallback onSelected; + final GameController _gameController = Get.find(); final bool enabled; - const DeploymentSelector( - {Key? key, - required this.controller, - required this.onSelected, - required this.enabled}) - : super(key: key); + DeploymentSelector({Key? key, required this.enabled}) : super(key: key); @override Widget build(BuildContext context) { return SmartSwitch( + value: _gameController.host, onDisabledPress: !enabled ? () => showSnackbar(context, const Snackbar(content: Text("Hosting is not allowed"))) : null, - keyName: "reboot", label: "Host", - controller: controller, - onSelected: _onSelected, - enabled: enabled); - } - - void _onSelected(bool value) { - controller.value = value; - onSelected(); + enabled: enabled + ); } } diff --git a/lib/src/widget/host_input.dart b/lib/src/widget/host_input.dart index c53a1fd..2396d29 100644 --- a/lib/src/widget/host_input.dart +++ b/lib/src/widget/host_input.dart @@ -1,27 +1,28 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; import 'package:reboot_launcher/src/widget/smart_input.dart'; -import '../util/generic_controller.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; class HostInput extends StatelessWidget { - final TextEditingController controller; - final GenericController localController; + final ServerController _serverController = Get.put(ServerController()); - const HostInput( - {Key? key, required this.controller, required this.localController}) - : super(key: key); + HostInput({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return SmartInput( - keyName: "host", - label: "Host", - placeholder: "Type the host name", - controller: controller, - enabled: !localController.value, - onTap: () => localController.value - ? showSnackbar(context, const Snackbar(content: Text("The host is locked when embedded is on"))) - : {}, - ); + return Obx(() => SmartInput( + keyName: "host", + label: "Host", + placeholder: "Type the host name", + controller: _serverController.host, + enabled: !_serverController.embedded.value, + onTap: () => _serverController.embedded.value + ? showSnackbar( + context, + const Snackbar( + content: Text("The host is locked when embedded is on"))) + : {}, + )); } } diff --git a/lib/src/widget/launch_button.dart b/lib/src/widget/launch_button.dart index 171a066..3597e5e 100644 --- a/lib/src/widget/launch_button.dart +++ b/lib/src/widget/launch_button.dart @@ -1,37 +1,21 @@ +import 'dart:async'; import 'dart:io'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; import 'package:process_run/shell.dart'; -import 'package:reboot_launcher/src/util/game_process_controller.dart'; -import 'package:reboot_launcher/src/util/generic_controller.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; import 'package:reboot_launcher/src/util/injector.dart'; -import 'package:reboot_launcher/src/util/locate_binary.dart'; +import 'package:reboot_launcher/src/util/binary.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:win32_suspend_process/win32_suspend_process.dart'; -import '../util/server.dart'; -import '../util/version_controller.dart'; +import 'package:reboot_launcher/src/util/server.dart'; class LaunchButton extends StatefulWidget { - final TextEditingController usernameController; - final VersionController versionController; - final GenericController rebootController; - final GenericController localController; - final GenericController serverController; - final GameProcessController gameProcessController; - final GenericController startedGameController; - final GenericController startedServerController; - const LaunchButton( - {Key? key, - required this.usernameController, - required this.versionController, - required this.rebootController, - required this.serverController, - required this.localController, - required this.gameProcessController, - required this.startedGameController, - required this.startedServerController}) + {Key? key}) : super(key: key); @override @@ -39,6 +23,9 @@ class LaunchButton extends StatefulWidget { } class _LaunchButtonState extends State { + final GameController _gameController = Get.find(); + final ServerController _serverController = Get.find(); + @override Widget build(BuildContext context) { return Align( @@ -46,91 +33,95 @@ class _LaunchButtonState extends State { child: SizedBox( width: double.infinity, child: Listener( - child: Button( - onPressed: _onPressed, - child: Text(widget.startedGameController.value - ? "Close" - : "Launch")), + child: Obx(() => Button( + onPressed: () => _onPressed(context), + child: Text(_gameController.started.value ? "Close" : "Launch") + )), ), ), ); } - void _onPressed() async { - // Set state immediately for responsive reasons - if (widget.usernameController.text.isEmpty) { + void _onPressed(BuildContext context) async { + if (_gameController.username.text.isEmpty) { showSnackbar( context, const Snackbar(content: Text("Please type a username"))); - setState(() => widget.startedGameController.value = false); + _updateServerState(false); return; } - if (widget.versionController.selectedVersion == null) { + if (_gameController.selectedVersionObs.value == null) { showSnackbar( context, const Snackbar(content: Text("Please select a version"))); - setState(() => widget.startedGameController.value = false); + _updateServerState(false); return; } - if (widget.startedGameController.value) { + if (_gameController.started.value) { _onStop(); return; } - if (widget.serverController.value == null && widget.localController.value && await isPortFree()) { + _updateServerState(true); + if (!_serverController.started.value && _serverController.embedded.value && await isPortFree()) { var process = await startEmbedded(context, false, false); - widget.serverController.value = process; - widget.startedServerController.value = process != null; + _serverController.process = process; + _serverController.started(process != null); } _onStart(); - setState(() => widget.startedGameController.value = true); + } + + Future _updateServerState(bool value) async { + if (_serverController.started.value == value) { + return; + } + + _serverController.started(value); } Future _onStart() async { - try{ - var version = widget.versionController.selectedVersion!; - - if(await version.launcher.exists()) { - widget.gameProcessController.launcherProcess = - await Process.start(version.launcher.path, []); - Win32Process(widget.gameProcessController.launcherProcess!.pid) - .suspend(); + try { + _gameController.started(true); + var version = _gameController.selectedVersionObs.value!; + if (await version.launcher.exists()) { + _gameController.launcherProcess = await Process.start(version.launcher.path, []); + Win32Process(_gameController.launcherProcess!.pid).suspend(); } - if(await version.eacExecutable.exists()){ - widget.gameProcessController.eacProcess = await Process.start(version.eacExecutable.path, []); - Win32Process(widget.gameProcessController.eacProcess!.pid).suspend(); + if (await version.eacExecutable.exists()) { + _gameController.eacProcess = await Process.start(version.eacExecutable.path, []); + Win32Process(_gameController.eacProcess!.pid).suspend(); } - widget.gameProcessController.gameProcess = await Process.start(widget.versionController.selectedVersion!.executable.path, _createProcessArguments()) + _gameController.gameProcess = await Process.start(version.executable.path, _createProcessArguments()) ..exitCode.then((_) => _onStop()) ..outLines.forEach(_onGameOutput); _injectOrShowError("cranium.dll"); - }catch(exception){ - setState(() => widget.startedGameController.value = false); + } catch (exception) { + _gameController.started(false); _onError(exception); } } void _onGameOutput(line) { - if (line.contains("FOnlineSubsystemGoogleCommon::Shutdown()")) { - _onStop(); - return; - } - - if (!line.contains("Game Engine Initialized")) { - return; - } - - if (!widget.rebootController.value) { - _injectOrShowError("console.dll"); - return; - } - - _injectOrShowError("reboot.dll"); + if (line.contains("FOnlineSubsystemGoogleCommon::Shutdown()")) { + _onStop(); + return; } + if (!line.contains("Game Engine Initialized")) { + return; + } + + if (!_gameController.host.value) { + _injectOrShowError("console.dll"); + return; + } + + _injectOrShowError("reboot.dll"); + } + Future _onError(exception) { return showDialog( context: context, @@ -153,24 +144,25 @@ class _LaunchButtonState extends State { } void _onStop() { - setState(() => widget.startedGameController.value = false); - widget.gameProcessController.kill(); + _updateServerState(false); + _gameController.kill(); } void _injectOrShowError(String binary) async { - var gameProcess = widget.gameProcessController.gameProcess; + var gameProcess = _gameController.gameProcess; if (gameProcess == null) { return; } - try{ - var success = await injectDll(gameProcess.pid, await locateAndCopyBinary(binary)); - if(success){ + try { + var dll = await loadBinary(binary, true); + var success = await injectDll(gameProcess.pid, dll.path); + if (success) { return; } _onInjectError(binary); - }catch(exception){ + } catch (exception) { _onInjectError(binary); } } @@ -191,7 +183,7 @@ class _LaunchButtonState extends State { "-fromfl=eac", "-fltoken=3db3ba5dcbd2e16703f3978d", "-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ", - "-AUTH_LOGIN=${widget.usernameController.text}@projectreboot.dev", + "-AUTH_LOGIN=${_gameController.username.text}@projectreboot.dev", "-AUTH_PASSWORD=Rebooted", "-AUTH_TYPE=epic" ]; diff --git a/lib/src/widget/local_server_switch.dart b/lib/src/widget/local_server_switch.dart index 65341e1..9dbfe06 100644 --- a/lib/src/widget/local_server_switch.dart +++ b/lib/src/widget/local_server_switch.dart @@ -1,22 +1,19 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; import 'package:reboot_launcher/src/widget/smart_switch.dart'; -import '../util/generic_controller.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; class LocalServerSwitch extends StatelessWidget { - final GenericController controller; - final Function(bool)? onSelected; + final ServerController _serverController = Get.put(ServerController()); - const LocalServerSwitch({Key? key, required this.controller, this.onSelected}) - : super(key: key); + LocalServerSwitch({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return SmartSwitch( - keyName: "local", - label: "Embedded", - controller: controller, - onSelected: onSelected + value: _serverController.embedded, + label: "Embedded" ); } } diff --git a/lib/src/widget/port_input.dart b/lib/src/widget/port_input.dart index f19fe2c..5ee112c 100644 --- a/lib/src/widget/port_input.dart +++ b/lib/src/widget/port_input.dart @@ -1,29 +1,28 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; import 'package:reboot_launcher/src/widget/smart_input.dart'; -import '../util/generic_controller.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; class PortInput extends StatelessWidget { - final TextEditingController controller; - final GenericController localController; + final ServerController _serverController = Get.put(ServerController()); - const PortInput({ - Key? key, - required this.controller, - required this.localController - }) : super(key: key); + PortInput({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return SmartInput( + return Obx(() => SmartInput( keyName: "port", label: "Port", placeholder: "Type the host port", - controller: controller, - enabled: !localController.value, - onTap: () => localController.value - ? showSnackbar(context, const Snackbar(content: Text("The port is locked when embedded is on"))) + controller: _serverController.port, + enabled: !_serverController.embedded.value, + onTap: () => _serverController.embedded.value + ? showSnackbar( + context, + const Snackbar( + content: Text("The port is locked when embedded is on"))) : {}, - ); + )); } -} \ No newline at end of file +} diff --git a/lib/src/widget/server_button.dart b/lib/src/widget/server_button.dart index 3515ccc..d119f0e 100644 --- a/lib/src/widget/server_button.dart +++ b/lib/src/widget/server_button.dart @@ -1,66 +1,44 @@ -// ignore_for_file: use_build_context_synchronously - -import 'dart:io'; - import 'package:fluent_ui/fluent_ui.dart'; -import 'package:process_run/shell.dart'; -import 'package:reboot_launcher/src/util/locate_binary.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:get/get.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; -import '../util/server.dart'; -import '../util/generic_controller.dart'; +import 'package:reboot_launcher/src/util/server.dart'; -class ServerButton extends StatefulWidget { - final GenericController localController; - final TextEditingController hostController; - final TextEditingController portController; - final GenericController serverController; - final GenericController startController; +class ServerButton extends StatelessWidget { + final ServerController _serverController = Get.put(ServerController()); + ServerButton({Key? key}) : super(key: key); - const ServerButton( - {Key? key, - required this.localController, - required this.hostController, - required this.portController, - required this.serverController, required this.startController}) - : super(key: key); - - @override - State createState() => _ServerButtonState(); -} - -class _ServerButtonState extends State { @override Widget build(BuildContext context) { return Align( alignment: AlignmentDirectional.bottomCenter, child: SizedBox( width: double.infinity, - child: Button( - onPressed: _onPressed, - child: Text(widget.localController.value - ? !widget.startController.value + child: Obx(() => Button( + onPressed: () => _onPressed(context), + child: Text(_serverController.embedded.value + ? !_serverController.started.value ? "Start" : "Stop" - : "Check address")), + : "Check address"))), ), ); } - void _onPressed() async { - if (widget.localController.value) { - var oldRunning = widget.startController.value; - setState(() => widget.startController.value = !widget.startController.value); // Needed to make the UI feel smooth - var process = await startEmbedded(context, oldRunning, true); - var updatedRunning = process != null; - if(updatedRunning != oldRunning){ - setState(() => widget.startController.value = updatedRunning); - } - - widget.serverController.value = process; + void _onPressed(BuildContext context) async { + if (!_serverController.embedded.value) { + checkAddress(context, _serverController.host.text, _serverController.port.text); return; } - checkAddress(context, widget.hostController.text, widget.portController.text); + var running = _serverController.started.value; + _serverController.started(!running); + var process = await startEmbedded(context, running, true); + var updatedRunning = process != null; + if (updatedRunning != _serverController.started.value) { + _serverController.started.value = updatedRunning; + } + + _serverController.process = process; } } diff --git a/lib/src/widget/smart_input.dart b/lib/src/widget/smart_input.dart index 010a452..2eec361 100644 --- a/lib/src/widget/smart_input.dart +++ b/lib/src/widget/smart_input.dart @@ -1,7 +1,6 @@ import 'package:fluent_ui/fluent_ui.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -class SmartInput extends StatefulWidget { +class SmartInput extends StatelessWidget { final String keyName; final String label; final String placeholder; @@ -22,49 +21,16 @@ class SmartInput extends StatefulWidget { this.populate = false, this.type = TextInputType.text}) : super(key: key); - - @override - State createState() => _SmartInputState(); -} - -class _SmartInputState extends State { + @override Widget build(BuildContext context) { - return widget.populate ? _buildPopulatedTextBox() : _buildTextBox(); - } - - FutureBuilder _buildPopulatedTextBox(){ - return FutureBuilder( - future: SharedPreferences.getInstance(), - builder: (context, snapshot) { - _update(snapshot.data); - return _buildTextBox(); - } - ); - } - - void _update(SharedPreferences? preferences) { - if(preferences == null){ - return; - } - - widget.controller.text = preferences.getString(widget.keyName) ?? ""; - } - - TextBox _buildTextBox() { return TextBox( - enabled: widget.enabled, - controller: widget.controller, - header: widget.label, - keyboardType: widget.type, - placeholder: widget.placeholder, - onChanged: _save, - onTap: widget.onTap, + enabled: enabled, + controller: controller, + header: label, + keyboardType: type, + placeholder: placeholder, + onTap: onTap, ); } - - Future _save(String value) async { - final preferences = await SharedPreferences.getInstance(); - preferences.setString(widget.keyName, value); - } } diff --git a/lib/src/widget/smart_selector.dart b/lib/src/widget/smart_selector.dart deleted file mode 100644 index 6f992ee..0000000 --- a/lib/src/widget/smart_selector.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class SmartSelector extends StatefulWidget { - final String keyName; - final String? label; - final String placeholder; - final List options; - final SmartSelectorItem Function(String)? itemBuilder; - final Function(String)? onSelected; - final bool serializer; - final String? initialValue; - final bool enabled; - final bool useFirstItemByDefault; - - const SmartSelector({Key? key, - required this.keyName, - required this.placeholder, - required this.options, - required this.initialValue, - this.itemBuilder, - this.onSelected, - this.label, - this.serializer = true, - this.enabled = true, - this.useFirstItemByDefault = true}) - : super(key: key); - - @override - State createState() => _SmartSelectorState(); -} - -class _SmartSelectorState extends State { - String? _selected; - - @override - void initState() { - _selected = widget.initialValue; - super.initState(); - } - - @override - Widget build(BuildContext context) { - return widget.label == null ? _buildBody() : _buildLabel(); - } - - InfoLabel _buildLabel() { - return InfoLabel(label: widget.label!, child: _buildBody()); - } - - SizedBox _buildBody() { - return SizedBox( - width: double.infinity, - child: DropDownButton( - leading: Text(_selected ?? widget.placeholder), - items: widget.options.map(_createOption).toList() - ), - ); - } - - MenuFlyoutItem _createOption(String option) { - var function = widget.itemBuilder ?? _createDefaultItem; - var item = function(option); - return MenuFlyoutItem( - key: item.key, - text: item.text, - onPressed: () => widget.enabled && item.clickable ? _onSelected(option) : {}, - leading: item.leading, - trailing: item.trailing, - selected: item.selected - ); - } - - SmartSelectorItem _createDefaultItem(String name) { - return SmartSelectorItem( - text: SizedBox(width: double.infinity, child: Text(name))); - } - - void _onSelected(String name) { - setState(() { - widget.onSelected?.call(name); - _selected = name; - if(!widget.serializer){ - return; - } - - _serialize(name); - }); - } - - Future _serialize(String value) async { - final preferences = await SharedPreferences.getInstance(); - preferences.setString(widget.keyName, value); - } -} - -class SmartSelectorItem { - final Key? key; - final Widget? leading; - final Widget text; - final Widget? trailing; - final bool selected; - final bool clickable; - - SmartSelectorItem({this.key, - this.leading, - required this.text, - this.trailing, - this.selected = false, - this.clickable = true}); -} diff --git a/lib/src/widget/smart_switch.dart b/lib/src/widget/smart_switch.dart index ff21c3d..d1581da 100644 --- a/lib/src/widget/smart_switch.dart +++ b/lib/src/widget/smart_switch.dart @@ -1,23 +1,17 @@ import 'package:fluent_ui/fluent_ui.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:get/get.dart'; import 'package:system_theme/system_theme.dart'; -import '../util/generic_controller.dart'; - class SmartSwitch extends StatefulWidget { - final String keyName; final String label; final bool enabled; - final Function(bool)? onSelected; final Function()? onDisabledPress; - final GenericController controller; + final Rx value; const SmartSwitch( {Key? key, - required this.keyName, required this.label, - required this.controller, - this.onSelected, + required this.value, this.enabled = true, this.onDisabledPress}) : super(key: key); @@ -27,29 +21,24 @@ class SmartSwitch extends StatefulWidget { } class _SmartSwitchState extends State { - Future _save(bool state) async { - final preferences = await SharedPreferences.getInstance(); - preferences.setBool(widget.keyName, state); - } - @override Widget build(BuildContext context) { return InfoLabel( label: widget.label, - child: ToggleSwitch( + child: Obx(() => ToggleSwitch( enabled: widget.enabled, onDisabledPress: widget.onDisabledPress, - checked: widget.controller.value, + checked: widget.value.value, onChanged: _onChanged, style: ToggleSwitchThemeData.standard(ThemeData( checkedColor: _toolTipColor.withOpacity(_checkedOpacity), uncheckedColor: _toolTipColor.withOpacity(_uncheckedOpacity), borderInputColor: _toolTipColor.withOpacity(_uncheckedOpacity), accentColor: _bodyColor - .withOpacity(widget.controller.value + .withOpacity(widget.value.value ? _checkedOpacity : _uncheckedOpacity) - .toAccentColor())))); + .toAccentColor()))))); } Color get _toolTipColor => @@ -66,10 +55,6 @@ class _SmartSwitchState extends State { return; } - setState(() { - widget.controller.value = checked; - widget.onSelected?.call(widget.controller.value); - _save(checked); - }); + setState(() => widget.value(checked)); } } diff --git a/lib/src/widget/username_box.dart b/lib/src/widget/username_box.dart index 00ec451..91afce3 100644 --- a/lib/src/widget/username_box.dart +++ b/lib/src/widget/username_box.dart @@ -1,22 +1,22 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; import 'package:reboot_launcher/src/widget/smart_input.dart'; -import '../util/generic_controller.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; class UsernameBox extends StatelessWidget { - final TextEditingController controller; - final GenericController rebootController; + final GameController _gameController = Get.find(); - const UsernameBox({Key? key, required this.controller, required this.rebootController}) : super(key: key); + UsernameBox({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return SmartInput( - keyName: "${rebootController.value ? 'host' : 'game'}_username", - label: "Username", - placeholder: "Type your ${rebootController.value ? 'hosting' : "in-game"} username", - controller: controller, - populate: true - ); + return Obx(() => SmartInput( + keyName: "${_gameController.host.value ? 'host' : 'game'}_username", + label: "Username", + placeholder: "Type your ${_gameController.host.value ? 'hosting' : "in-game"} username", + controller: _gameController.username, + populate: true + )); } } diff --git a/lib/src/widget/version_name_input.dart b/lib/src/widget/version_name_input.dart index b95d11d..ea02d28 100644 --- a/lib/src/widget/version_name_input.dart +++ b/lib/src/widget/version_name_input.dart @@ -1,29 +1,31 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; -import '../model/fortnite_version.dart'; +import 'package:reboot_launcher/src/controller/game_controller.dart'; class VersionNameInput extends StatelessWidget { + final GameController _gameController = Get.find(); final TextEditingController controller; - final List versions; - const VersionNameInput({required this.controller, required this.versions, Key? key}) : super(key: key); + + VersionNameInput({Key? key, required this.controller}) : super(key: key); @override Widget build(BuildContext context) { return TextFormBox( - controller: controller, header: "Name", placeholder: "Type the version's name", + controller: controller, autofocus: true, validator: _validate, ); } - String? _validate(String? text){ + String? _validate(String? text) { if (text == null || text.isEmpty) { return 'Invalid version name'; } - if (versions.any((element) => element.name == text)) { + if (_gameController.versions.value.any((element) => element.name == text)) { return 'Existent game version'; } diff --git a/lib/src/widget/version_selector.dart b/lib/src/widget/version_selector.dart index 05c7892..daca65f 100644 --- a/lib/src/widget/version_selector.dart +++ b/lib/src/widget/version_selector.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use_build_context_synchronously + import 'dart:async'; import 'dart:io'; @@ -5,24 +7,18 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' show showMenu, PopupMenuEntry, PopupMenuItem; -import 'package:reboot_launcher/src/util/version_controller.dart'; +import 'package:get/get.dart'; import 'package:reboot_launcher/src/widget/add_local_version.dart'; import 'package:reboot_launcher/src/widget/add_server_version.dart'; -import 'package:reboot_launcher/src/widget/smart_selector.dart'; -import '../model/fortnite_version.dart'; +import 'package:reboot_launcher/src/model/fortnite_version.dart'; -class VersionSelector extends StatefulWidget { - final VersionController controller; +import 'package:reboot_launcher/src/controller/game_controller.dart'; - const VersionSelector({Key? key, required this.controller}) : super(key: key); +class VersionSelector extends StatelessWidget { + final GameController _gameController = Get.find(); - @override - State createState() => _VersionSelectorState(); -} - -class _VersionSelectorState extends State { - final StreamController _streamController = StreamController(); + VersionSelector({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -32,24 +28,7 @@ class _VersionSelectorState extends State { alignment: AlignmentDirectional.centerStart, child: Row( children: [ - Expanded( - child: StreamBuilder( - stream: _streamController.stream, - builder: (context, snapshot) => SmartSelector( - keyName: "version", - placeholder: "Select a version", - options: widget.controller.isEmpty ? ["No versions available"] : widget.controller.versions - .map((element) => element.name) - .toList(), - useFirstItemByDefault: false, - itemBuilder: (name) => _createVersionItem(name, widget.controller.versions.isNotEmpty), - onSelected: _onSelected, - serializer: false, - initialValue: widget.controller.selectedVersion?.name, - enabled: widget.controller.versions.isNotEmpty - ) - ) - ), + Expanded(child: _createSelector(context)), const SizedBox( width: 16, ), @@ -57,8 +36,7 @@ class _VersionSelectorState extends State { message: "Add a local fortnite build to the versions list", child: Button( child: const Icon(FluentIcons.open_file), - onPressed: () => _openLocalVersionDialog(context) - ), + onPressed: () => _openLocalVersionDialog(context)), ), const SizedBox( width: 16, @@ -73,117 +51,107 @@ class _VersionSelectorState extends State { ))); } - void _onSelected(String selected) { - widget.controller.selectedVersion = widget.controller.versions - .firstWhere((element) => selected == element.name); + Widget _createSelector(BuildContext context) { + return SizedBox( + width: double.infinity, + child: Obx(() => DropDownButton( + leading: Text(_gameController.selectedVersionObs.value?.name ?? + "Select a version"), + items: _gameController.hasNoVersions + ? [_createDefaultVersionItem()] + : _gameController.versions.value + .map((version) => _createVersionItem(context, version)) + .toList())) + ); } - SmartSelectorItem _createVersionItem(String name, bool enabled) { - return SmartSelectorItem( - text: _withListener(name, enabled, SizedBox(width: double.infinity, child: Text(name))), - trailing: const Expanded(child: SizedBox())); + MenuFlyoutItem _createVersionItem( + BuildContext context, FortniteVersion version) { + return MenuFlyoutItem( + text: Listener( + onPointerDown: (event) async { + if (event.kind != PointerDeviceKind.mouse || + event.buttons != kSecondaryMouseButton) { + return; + } + + await _openMenu(context, version, event.position); + }, + child: SizedBox(width: double.infinity, child: Text(version.name))), + trailing: const Expanded(child: SizedBox()), + onPressed: () => _gameController.selectedVersion = version); } - Listener _withListener(String name, bool enabled, Widget child) { - return Listener( - onPointerDown: (event) { - if (event.kind != PointerDeviceKind.mouse || - event.buttons != kSecondaryMouseButton - || !enabled) { - return; - } - - _openMenu(context, name, event.position); - }, - child: child - ); + MenuFlyoutItem _createDefaultVersionItem() { + return MenuFlyoutItem( + text: const SizedBox( + width: double.infinity, child: Text("No versions available")), + trailing: const Expanded(child: SizedBox()), + onPressed: () {}); } void _openDownloadVersionDialog(BuildContext context) async { - await showDialog( + await showDialog( context: context, - builder: (dialogContext) => AddServerVersion( - controller: widget.controller, - onCancel: () => WidgetsBinding.instance - .addPostFrameCallback((_) => showSnackbar( - context, - const Snackbar(content: Text("Download cancelled")) - )) - ) + builder: (dialogContext) => const AddServerVersion() ); - - _streamController.add(true); } void _openLocalVersionDialog(BuildContext context) async { - var result = await showDialog( + await showDialog( context: context, - builder: (context) => AddLocalVersion(controller: widget.controller)); - - if(result == null || !result){ - return; - } - - _streamController.add(false); + builder: (context) => AddLocalVersion()); } - void _openMenu( - BuildContext context, String name, Offset offset) { - showMenu( + Future _openMenu( + BuildContext context, FortniteVersion version, Offset offset) async { + var result = await showMenu( context: context, items: [ const PopupMenuItem(value: 0, child: Text("Open in explorer")), const PopupMenuItem(value: 1, child: Text("Delete")) ], - position: RelativeRect.fromLTRB(offset.dx, offset.dy, offset.dx, offset.dy), - ).then((value) { - if(value == 0){ + position: + RelativeRect.fromLTRB(offset.dx, offset.dy, offset.dx, offset.dy), + ); + + switch (result) { + case 0: Navigator.of(context).pop(); - Process.run( - "explorer.exe", - [widget.controller.versions.firstWhere((element) => element.name == name).location.path] - ); - return; - } + Process.run("explorer.exe", [version.location.path]); + break; - if(value != 1) { - return; - } + case 1: + _gameController.removeVersion(version); + await _openDeleteDialog(context, version); + Navigator.of(context).pop(); + if (_gameController.selectedVersionObs.value?.name == version.name || _gameController.hasNoVersions) { + _gameController.selectedVersionObs.value = null; + } - Navigator.of(context).pop(); - var version = widget.controller.removeByName(name); - _openDeleteDialog(context, version); - _streamController.add(false); - if (widget.controller.selectedVersion?.name != name && - widget.controller.isNotEmpty) { - return; - } - - widget.controller.selectedVersion = null; - _streamController.add(false); - }); + break; + } } - void _openDeleteDialog(BuildContext context, FortniteVersion version) { - showDialog( + Future _openDeleteDialog(BuildContext context, FortniteVersion version) { + return showDialog( context: context, builder: (context) => ContentDialog( content: const SizedBox( - height: 32, width: double.infinity, child: Text("Delete associated game path?", textAlign: TextAlign.center)), actions: [ FilledButton( onPressed: () => Navigator.of(context).pop(), - style: ButtonStyle( - backgroundColor: ButtonState.all(Colors.green)), child: const Text('Keep'), ), FilledButton( - onPressed: () { + onPressed: () async { Navigator.of(context).pop(); - version.location.delete(); + if (await version.location.exists()) { + version.location.delete(recursive: true); + } }, style: ButtonStyle(backgroundColor: ButtonState.all(Colors.red)), diff --git a/pubspec.yaml b/pubspec.yaml index d5547de..6baa62e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,8 +24,10 @@ dependencies: archive: ^3.3.1 win32_suspend_process: ^1.0.0 version: ^3.0.2 - unrar_file: ^1.1.0 crypto: ^3.0.2 + async: ^2.8.2 + get: ^4.6.5 + get_storage: ^2.0.3 dev_dependencies: flutter_test: @@ -52,7 +54,7 @@ msix_config: force_update_from_any_version: false publisher_display_name: Reboot publisher: it.auties.reboot - msix_version: 2.0.0.0 + msix_version: 2.1.0.0 logo_path: ./assets/icons/fortnite.ico architecture: x64 capabilities: "internetClient"