R3a6VyMA%YY^+CuLLgz?c-xS6T0jLC|SZ5)~)@_XsBJN^!GzbY7gEjOWMqG
z`G1vR<+DtUv>1{Su54YBYp5^h%mg|~R}Je&n^hPG%7z4?Tb22ooRsQ#MUe>#8{Q^3
zlJTi&Si9>rPWQ)qq-iH56
z>U3}hbNvmhB2Rnyg>@74C1bv@z5NzDj8Gk{G0@}+^)ID3ZMox%{fark-^To3l;Xc)
zF7man4Pb5l$OG!
zP>EkCpn;YZh7GO=fL9AmMp^tND4IZ4i~Bn0c(%N=DwlxNaW&YN<6(%nwwIHzsAM}k
zh?>;&$#Sj(T964ek?QkB9qmWlM?PIY-tKR!fl{wH``i%;D%3C2ZS8EKx7gBT%Z(>9
z)uzwd3BK;q(%*w>}ExCl9P3iAg1WPg?WHT3#j{lG$6L>^6J(mIBGm(=s8o*Ih>+>f{AM6jCU(U-bI4cO}GX1OxPc@%2v8okh>Ka5}ba
z8y!38*tUMLZQHhO+qP||W82Q{bH00D&Uf$sVUHSPkG-DOs=caa&6@S_4T%}J18ZO&
zZut-GxA1qGh&gPbnAl8s`4CL01>fw!u=_Yvncu6&=mz+bfrv%?w)%Hsyogv~8I9Q9
z{8D+ZxsRHkOX`T>24M#QyBvZ{Q!G1zbK-24uq15JeS1h~4rkVVKUz@5TnniNyWsY(
z2%_?dK0|%y%CjP?u7z?~vWI__x{)|U>
z`ePflYZ*wf`*y`rj)-hRT)iEdYnVM-g7?3pda)Ma@SRz4DJt-LEAyT_@@)u15uCI7
zsvtoFBE$x)0(Ve7wStMr0s2{%=tR-)d{S~-L8vSc{Db+6
z+-U~f99bLvkBh?lP%6_jx4D=l7;b4e-k-cZRNp~^y@kp1%+HsFTZMYi%vyg-Y;O9u
z2DL?BY+SXHA5oN{*t2DR{2UlF$$hCacLvXNXibi(&rT?f7dtszBO!f<)}D0b&CzoA
z68}0h#^<_aiJr{?pZHuIcSFV`T4)l#(l-#mW+d$)!R<_WZSq+!^sS#CBFmJBUW@
zk!9kL^~rhen&|aTkZ4}?ukZYyhsLt-IZ^+MVE*H{^BGBRDI#5?IGUlylOe}q?_P46aXe+x(v~Ep$
zyzzVOp-0au1h^-xufU+>yT$kDE@Oqh>05CEy5MX!Yoa3aQmPKRA}MXuSTTr#J8?-X
zOWQy_*olV3)Q~hrx_(4wBd(Mb9^>$}n8Wt0xO#Kb(H7jCb3oF}#rAktDpHFn_Lid&
zkP57IrdQ^u)!y)g11qe|bS3u|d#8i*b1{L^1z)Dd>nz>Jr!r7b&a+3UOaP+;&8%&B4v>>Q
zk*O*5gSC}I^;~JK25O!*+m3`%-C`YRQ_)6z7?lYfdTZyJNMpytVFv|&E*>*uQ
zKR+t3YY+(pK8D_efo;fmGw@#Wy9;?IF)l&?c6kr~dU0w(1?>0KoE`1K5|uz4_m~9&
zCfAQ)ArSt85idpjo`a%f&!l{D=n675Ib*RNC{)HF|43sB$MX{j7)qMLd=&c6ZjQWr
z0H4`o{5Z%XihdH&?4lM}H%(dL2eW4Ld`CczB!|0SNY1R;JJ65CGk3P$vwX0m$AsN4
zYXbWxUbca8U>npi-`qJTNZ$2_d!T$^VmihSw7DO!J7}*_affe+kinz8dZxKcxuvG4
zS$qRx3Dm{zAkQuWzP`mT61vNv
z!TRYnV&~qrAzE+oFK&t<%5QgB++^_=Oau@?5M>t8)2)nOPnu+GryHBFK_q(+0{kMx
z=u{NCs3SHYw^@YqEX>gmgztn!hCle^?+ScuP1v_dEidIBCT)A)dR}@CyjMSV3}^XY
zfBQLeOxhjp_UDX%pn*s_rCHWTlti5BiFk`B%atqz8I*UROP@2!It5a+88~R-Do*J+
zg^7huJ1tb1VJjn(*Vc*;2TG8kDF;XS%Ve&Iu38s$iyFrG{>~Oh?8j9Mu!K6&)L!Ob
zSEiF)Lb6FKiad?Zf65=xi;7j=qV;EV8}!%+yT`K#V6K{rb`#o?H-OstZ9!R%%8uP~
zR;VecW`N&@DfvP}A}J&|zYn(xe|e%X8HgN`5QHD=82HK!4S8CJkr~cud}<$k?b@uvA6BG7A)?V)sxsH7?m<169U6+g|nEFVRovl*mMRQtyS2;
zxK&Bsp3{PtTnix;_e29-F{JHem<=7mKoj3A3NVMPPV*z5YUgF50?qD21+%KPdYR_F!_-
zk}hn8VsHXukB=omnixKLB_1n5Z)^vdASKgauBb4?wS(>Ns4gJk-U^^{`sQ
zDcF=NbpCyvqe_^MriX4ThrQ<$YG&OiEmAZ;y)d7AqX}hty|+j3z(iM&9#|?(KurQ5
zmH&PhCslAgD>)jp34JyQU8%;A1;iJx8pf)xXX+?a5fK3PFLzFc-aA;>DK$_-Ir{3-
z!nk3{a*6_95gkvL>J^tCP)@ukt$%Pb4?f!D`G)Mq+n;h+I61~!V@2F0-PQxYl;OBh
zEh-t;7mkt)(zMA}CB%O((OWs+#O23QgFxjhm%9Js6l_@fzz!T_6h|FVb;WaS#~Bw3
zQXxI^CE_4YX>1SJ$eKq8YO|zI!l26bGhq#{8AR!2c~r*P9@B92A!<@?o{;CAval;b
zROBocX9xCdMFzToJ=LWL&ggri>8O*pHn0lgk4tT8Xj%K$j#LA-0#@VxkfB>DNV9Tv
zqGMVdAA0kx%vV@qxBIara4>Cf8c}+RkN&bvbEe{|f82FZgl`Y(94GfN%rHR;ISOV9y^eg@$7i_i`@po>?rtuvthOxOGEGo)
zSF^QkwffM_!2{ALZV@`dbe0st6OZI2YTwMs
z0L?64=s$dn8n6=|fWQR)sZaE{C8_|+&^gYctX<)c^6ch_h?U2J8gv%w9&gba@IFL1
z5k}^tcX$EpjTtJFoPaiyQ=Z~`B|s;3SsLOwVKTmgnryqdOQEJi*lk6A#E+Y(ila(|
zIo-+NkY*7Y!Z3N<5lcO6b*3*)v6q?(JqrHp
zC>;)^xO0yG{;L-^98y20&V+<5->hzyX+X8&7SYPZT*`6MYBnK-RC~mcC$fxcs6F6%
zpZT{6{jlG)98BTiBA@B7Bsm+uoTiqqmRr8);pMIgrp=hMnF1Xc^kkBlilv$V
zfQpR9BHg-lsiITkBAHl|y@%x3>Z7aT-WMf1%m(@4wjzItkZ`gvi%cy6J@(8BheYwW
zVd)U$6Hj*6L*r#1(gdG2=_j$4Mt=Mu=YFkOz}$3P32A%KUwL2@rp8xpQw7EXA>?mD
z6S$AGLga5@IX_n9Z|N7YAXOxKW#49QY2T2ZIpq@N^Md3g*MQvu&(;d-k?~r1w_baSp!@lux!pU`pkn^
z;#CinDNCD}@!XbyPgvURAE)a|&7*|hW^AJ=RSm2~+LV=5(U;rY;g#nxhL&IUO|pib
zMcp>YoffeN8Ofvb@%_yxY%j!42OYF8bV1=H0M=+bVezrN-t74uaZ_-1tMT1pnHtyv
znQK_^Oi7E2b2V`7U#@vZ$mjLX=CER);~Nr1`1QfGC9VDf_*9Mg1e4b4vNt5Z)bx_!
zjI~V$sXZcKD7(Z5ZCCQL54lC_YMy^Jy;oChDGUGmx;}M%%{)owKu=hn{B8op?JDRh
z{^V>ml301+#UjAi4G(YwX$&zgMh41IH@syM$r;W~ti$$Qg1d9b6iSN-RVFqYNJV%CGZ@cs;v2Pu}_y)yIEB%%gbuBbXNUi@nfT4qOg#83hX6
z*5RW!rJp^u^JV@X#fYd}x0ieO-dF3EFL`#dfmX{ZCYdv&(3GIIwiEc%{|2e{iVp-#pn!m=k^fT_iSGX%y@~y|qK?WxsS9yiYh!6!TT5rV{}IVW
z{~uB`&9+aTx`Mp2kqRZ$CLE?j;1FnWpAcM0F)1JG;`Zj%!q>#54IJaW&?m+SXf*jZ
zkZ;mmi&@luAOo}G%$DO#yX*1h%dGCNDnp6g?KAVnXVDu;%Rm0rw&$vHy35sbvD$Lv
zHkg<`W+)F4JPC|<#=0XR%M_M~r9M@*&qWxE75JPX3?zeiN2fMcRT>wuoT|${XP)IJ
zj7TrV^&@e>qi|tKI2=>(62sb&Z<7OJyim3#1oq&x^{Y2}}dufW&QNs(k`V<+}dFJKL`Z}p9l
zunwR7pr$9CcM?LsM0N<6G)F*l{oWYTJf4sikM1d^viDrx=ow7s_;>p~^=Kz=x%{UP
z{wtTRmVZxL|A%%p{71Bl`ad0>{|$`)7s~lRPEN4~j2EtPGr#FGV`JhKO=c2(v9W|!
zr+EVuU0jQnVuspgQ)UwjobgAVv{<^G!uXXZX<}Z#
z5T7LncOVN8)m3I(8#AoF$xCqo#Z!-o>_I
zk8Q)=e?-_OY1xvuRrAp1k)jlQv#oj8Y8Z
z9qr_Cl8mLMocg0k$Xx}x61=GC@~BHh8U@Ci;>v-INJOWY$WfI@GghWYc65*sHHCeW
zX9^$EyOdmwI{e0svAa9DO!J(9t*La%7LcKn3Y;gD2M_zxd2i!Uhsvl6WS3h~Qx>6<
zQ|2%9l6^4M^7X=(tvJ$E8QY&O9B&y*&h?BsG}xD}pA>n6l`9It*Cvih^;w`ZqPbZe
z5kT>agWhBJ2yADL7|%bK>R-K$kt!M?|ST+Zj=z3`2r+h
zKaoO~BXM|SmXju7W0%_Nr2-^hEQD=XJe9C)P139V=$a;ksR7U~2nhYT=)xt%gmKb}
z6GEYoR(DU(a72`~vt&+6`LL=RXaP_(4TBm=a=}6QXk1e)tHg=SDB~mI#F3Y-sDe+Y
zCVk+rs5}N7dXErO0=#us6*Yh4Z86OtgEo`~4O%%_z0{?SA2+~*ZTg_>d2uF5q2J4C
zPhf5H*rEAsX-DdvJ*3DB;Yh%Lz&4f}0x(L@$TsM}9g32S5xc@|RL`cJCZ%;WWf5}=
z6w-Hqk7N|rDvTkBYNT6k<8(F)uH{R
z=%8hfTBgOxuZ-Y{T^G2OI|9G6#|dZ=3_mVAl{>O`=qwbBz*oWIUdo(kx>L)~ngjIR
zD|c*-F72+zpU|F-J2K6sZfRvL4Qq_{3jS=O9jnM5xI=n6_iBLY@n>_$@T|qUZ4PW0mJZ&kD;>%q`oS
za!|WQr3gwPK({E;&UKY^p9@Eqr0u>5KUs_6Ue4TR)3~AW0c==-*>dxP)V#vECJBN>
z!Lm%jfQPmeA4v5vF{bDUAP%)`{(=QeUg+u|!H*hqKKar))7yjvY1GxKRD6uBrFp8P
z5UUynExVg@J=1loFAWdHKfdtktKzBRoiDgjeU=dxC|L!%xF29#bjr`Diy6N7x+M(6
znP_WhKJy9wK^PFT{;7)eJ<_vfk7V!AWni^qE9j5wTXmB8ruhrPTr~vS^9T%q_gvkN
z&K|O-=QsnY+@?w=t)OGR`4X%Pbiph$OPVu|-x@Z(LbEV7y^+BB_B3hDZr;DvEjiBv
zC{OH4pM_P2bho7V>!h|2;wxY<^Fe_3Mu)%W_DhSy*5k*+{65Mh`B91+iA{=RSPIs!
z-s@6*=&>ufPkqy7GU+8P>Eoj!-#?^K2|)X5l|2-i$Zh6*OBIBKjE8fBVM)K}zGUNG
zbq^61>)5*=A?C-r#C?P6=ST?Y(3dZY`4`24OaKi{@T~A;&M4i3y4DZha9N~`T4{l*
z-+n~8`MoeSUm#4_XEud%7;a}#bGJf>0moWS5yz+EqusROIr~H|Ni9XHXG!ZSr`qHh
z6=z`j4yJ*r#;VFLjbfCj5j~&UU527;X;uttkrjQ8X#iIn4yIWl!7yo>v0z|N3WnQT
zuNqpmnPMO&wQ_Ab2Ud0(uN_)Wg&}A|zcNEu#G>CJ;G^<39$FgWi;XE?3EDGKScSuQ
zQS${AS*pe(VS6J)bmOGZ<4e%g)SOHd3@2#R42IGej-gB*=sC>u2(k>{1-l-cW6k)K
z=m#N+uwV>Yfx}n0$ZtfO@z_kE-CK}92mi3vy(w~=x$Z7-_>f>Jlf&)sg{BY&6vQ+f
z`Yc7Rc|=$<37TE*n32bT<`qV|77e&OL~zCSqIADVh|85ifxWtk3z0q@b-xH454Jcq
zX+MG$5KWIYa4;k0j(ZJ=W5A(*^{FRGh&?4cDKmJy$Q53umIp~Eg3bE!0{$7t+U8>Y
z#qJ884j#Zpz;ZOR&AGtd1~J+(aGArglh+Xak{NcDrxX>a*!Gx?n5`w?@{t*B2OKA`
zDu?g#C=4!a-B@7nlZpiHMo$x+7H`l5i}zI$7PBjoN?nvO5uRM!N(7TE{?x728%$5^
zXDP4OZX>;U@pEoc?G8WL^UZ=K(0C>i69i=6ue;#%viWVZ_MVTA&}64D>`&XSK>T$E
zR-%*)7ILs*t$s9QUFI3Q74?
zEkm{*Ij$@-gouFblSa(*NIE_1bbl4f-=h2IDEsq14J^J$#tEC0TSbmD^5nmL?1f7XMyN+#O?EZeT!h38>dfjg6#@U{8^eF&ujJ!@7O)Ot^JbCGXl*ouEnS?WN>2*C
zG;y6g9nyly$gGDTPf~wFUdq8t{$u
zqrP;U+a~^*zS=P^@HtHpp#-ti4U@bizMj%#BZw6koWxmh%b@ZdTsBf6znqsMdqIhU
z0amI#IdZvNW!wl1sr+L39sTEvZZV-TU?Qqrkd1i|Nt|k7+Nx-n@ATXMr|kabO->r;
zo4|*fxg-&6C0kHyVeDKj;-cg5*2tyrY$XJ9yUOLM_LDvNXMiDf3a1l7pDk&omqf)V
zv4fAaz&8`fH+i<~69K9*k{KM~NfMa|kB5#k0-&JEr!jCyjL~!8Mux>4bC`m<#+ZJp
zM2Z&N8&%qh9THxY2N!Njvw|9<@7zr5u3{cDMxR6K5W)fM1JJ^@H>V>@GK>glu-#y7
zXOYW5XiY#zHJutzgP`FcE>n~*JVaUT_a9IT3scji>`|so9HL~R%ZkACA)OC5v7U%&
zi^)U^d`5TEpw-EfJdu*>TGIdNkRNNPeH0p}T~nQt!YEfv4k|Z-KM#Rq+H_fDsJv9c
zZPV5yf(^SX;-cYu@9U4rAiK;V$ok@;qFjb{f~AqtuMZKF%35+bTO-_X=p4j&RxJ7_
zMx%$$G#{~QHVxA*VG)NNjQ{)Sbz&=MaseBxFz1Q4K#Cm#PQ0TW_Q;!=QjV9V@FkLa
zUNjRJzFXQtv0PA7%fV0qnoQ~IG0T%kJuB}zAsCu77KYg;Cs+DGe&FR;IOUVe37lPL)h*(n)
z6d}!r+|s$9iE*8ux%x06?0H(tR>5xMU(9-H;BYHKQ(tX`A{zV0qLkki@PZha;~>vu
zq(L|On$FLbB0+)nMQI>N!rhdN3o6o2!ROd%xc8UPWJ_F}x0)o#@B?nm^>FR*Qub4k
z>B_{_5C#L=A@Y5+j44GT#!_rN8D)L+G-gWHE_#yik7p>scrg_95k_a`1-z3;@Rbp
zPCsETixkF8sK+)c-i#|1G+dKn4KM#|OGK;mjG*DH!bPH)pU?+V?fE9{Hjj#lUaX$4
zbIh`d4UwJOe0ZEu=&($RiCz3C&{rF~!7YB_7kpt1-u8JRd1Nd(@Y=XR?&TkU1QicG
z;>qTidHFb+=t4|Hk6OgE13P~mOE<0iR?o@dkoCzMm!CQLy6@-V%`UAC(IGL#@B3b@
z`U0=#@8y*HYlZB$4D{6kr+MW7u>5-W%ITqqivc?OKv(=j$Vnma)!>P2bu#!^*lCXB
z=WFn*Q>@G&%Lg5=_=gV-7@Keqy6Bn4{e9TJ0=a4EhS>H&2y8*6&_jp8Z4T;L%{|
zhEx6RLg8H|$0xUxKfJq?$SLRBAnxZTd6!aF@WN+hlq;<>(`Y+Iv~1
z8pP7@un|yReI@siJs-#6U~DXX8h&!k+3#$SzsK?Py5~2ouVxH;F<-a2uiyMYzK8NW
zSv-R>=yKdp!1_`8%)-i^w}`Rm{6fKZ$|g_-BjKhi}T-(`$=yB%9-cMI~}BK%9Q}
zbvrIskZ*@?WV`WlrcqTpk64fy6|L5pZ-;Y&*AO=t)}a4}T4T?9-eyeQowuNz63>Sv
z>YmkoC5Af(`$U~ZlYeHipb(ef$REY4%rZ*}2w^#QmzI`sL<~OW9`4{X#0-PjHp>=N
zLT#I}my>0p6^L)cJHQs@(HsTy`A+NM^VuxXE7tC^-N&`L+EJgI;IG1#5>XqmOCM4Kxj(&kbfTKlbzM-2;ew55p`0)=6xCY?h2<4!=gVU#Z-*ZqA9E
z)-~C|6KIxHmTWJ-S*M0N`m>VkEhVHtvnsy)S52KMs7*k!*igjdbw?e=q1q?D02L`@
z_U^3o9CAESm_TVj#tCR{M94BRYwTY%B*h951DcDkoDQm>99lBv*Rz~F_W-%rhsF6Y
zf`s?%C;79~htI0y;6fc@*&6zwShBxtkwdE#b@g~xGY{-HCCfzmav6c6t<_1$m?tNN
zz^&hTvAq<6vA*&{AA3~S$G-n>As3w8$11y?(-^Q#X&Bg@cq
z8CC)Vb&uX$Q%JrzH@;BmM2Z4OXkI)0a`@QD?;y)JzZ_m58H6LwL%e5sn!JN}eco8S
zOX(Rn_xR=l4u6F>f6~b5ZMBM2KS-bn5)uhpcasUre6_JqY_L*KSI@XtyiqtRgbbyxho`J}wI5I76bDVPD@ZdZ6ta0n8
z+=|>53*V~Ts;oZIbY1hXak6d6$=uGq7U21(Gq~c|`hk71euCeWW9veYsCCD4+_Mf6=aVU7yo^@AuRt+atj$oRLLDXXNdi3lj^_edQge#kA91HmlhN(SR3vi=a~~;HBudLPJrdBlQs&%a
zfan~m2zv`tW?Wh34a9JCcGo1Na08o`;!9xgTIKufadZw}!IwDz1;(-vbgmOk1Dvsd
zld7WR8A7n^C*_N-BvsB;#q7r5^K3PxF?T=gEJh0_{KClWmwAg5$ZC5&@l=jpsZ=iA
zg2}8}c@)v>=9;6X8MK0@nnIrwYD9pOl<_k$j%7OZOig$
zZ;a;7n-ch!hbE03L9N5qMb$UUC92*(n}_@YYMAT!4B@yx5dVe3j;a6&k)Z(rO;G(O
z2%q@>j>1aVI6CQDS^fX0rO|3|URo+>KYxp+J-xj@%sO49UY4D4oU9jZ0#lGi^qi!r
zi=&(igWx7D?=yPn#%abUJNyZ$iHRYJ%QoW};v))xUCi^N?ubA_0+jv;I6=ZI{1HJ#
zjd!1sX(-WQTm8$xd$Se$;5_@4>-ggJxjP7i9+?chN#AMplps~n8NDmYj9&4q2H{z8
zQ5opcG#h~#V?64mz-_ePwiT5oI#4tYAlZX?&ghR0H)2t^x?v=SYV7G?xQxX1=8H6T
zVrOT7rnf0*z9U=z;vE+c0!Qu+u|=vkp|u*8X0{m~VCfi-q7cW3W-#Zd(GO=ZvZ?4%
z4n~~gx-{Z3t7#%G>4W9Qw}BmvmLIaZjK%TxHtDKoO|gp-H`*Zv0|QQE$IOfx2}6Qm
z&)M$ohvkCi0cJijTc{_F7T`vg9yu_XGPlaN7Ihs`&RZCf5j6q~!DGiiRQEKsgj+jg
z8nZL`Cj4Qnh0=gB4MxMDoOH0Sz;H}lN7^XE%|I=kswf)vsTz47PgUJjzL
z3@i%nnt{}aI}vRecs#D!-G8IwDr&EeO~gjk>Z&kT_ql*a({Nv@kMH~7P)Eb?;4_QN
zJbJj7$SEW?qua{EfPb9tJ-|v4id9r7VbD~ld&Yk+4A(Wv^m(dFK--jz#Yy6|-rn2p
zsYbYQ5gF6bVxC_Dmn3lA*I|Sx@RtXXf~9N
zMrGzNmk?bn4?4-Nx38${GJ#O6pNTaz(;M2&AcMsoaZ+lC`xNZi^AO3mxlBv`MMjf@
zUW%cginm`=d~6C4s}Hq5-EC=-NPo+NDS8)In0
zS_{@qh@^92LavBdmsLRslt$YQ-=PM^n^?!7JbN&*U}Hr6jM&fl?J?DCKhU7Tda!He
zHKjW+iR}~pH;U$DUCMXJ;ajVRvlj&sjv7FAGIkV%_mCK0Yiuuit-XlT`mtDjOwdN2
zE?eSs>K0g7N8n4Ec_l0q#f6CGTaYl5j@CNP7}|H0R%pR8R@8diBFcK6Z51qafI`9c
zPfy3s(t5PtBAHo?`|t4I$XoU<_7*nDcIa;ja88%ZZMo%u9iT$+qlq!gIf&QPke54K
zhuXgp!I$4b5kK-pWkfEpG@^REysND#0FoVjKoW~#KTFsr2EoHO2x_wpO$w)@%G{vd
z#wk5CYAfk_j%*uevB%kVeuUsaHA+T?a(a~Kw&(yB$mpEl*NJro||6@rO}9=m!FZp1p58{aZJO%*nPolTi?;A
zN^e1%?l{4UF&2#$E0H4UuTwReJ?$^IW9MXi?7uRlViGIB3YFp53S!1VT!iPH3EnaE
zUZ6lM8-H$@p@E$Cxb$GpF{XYdroIQfVKqs0b~J2X|;Kn2?O)Re8*66jV`W
zIMfcL{?x!@(qowyXBfl9lN$XaFf-JUHByIuok=<$;{|%(!n`;1
zv}&X8{b?rvudHm--~5SN{%q49Yi9s2jWvk##s5cAKDzzPJ9txqLcjXU7Yzhy%}
zT1vkk@*KEXN2o0IasV}k!MenkDvKkiIIv85Z>id>LMq>w2HQ*y?28(NstQ+BYqv@u
z3&rd&+^nCc!S>fFDba=EZ$(Jw6>#7SbJps#6}~X6Z{Uq%2Hc@4zrRYkg0?4w1wO;u
zRUR3UUWy%>H8#PjHxKAVcJZyh!Ax-?Lhb8y@&3>}q=J2(Cl+1waRZz|Qz1S#5cxjr
z9P2wZ7*;1EZ~CliHES5)Un#^3BfB$FdwS;FgXzKHyUv$CE7ZJ!bkW6qwHfNrM(k8=7EO
zOPa4Qf!2lx3$?S0g#&g3S*TvcxvCV5c6wMAjBVo2qw|(n?nbIVy;zECyqw{LEIIJ6
zFt2`6KRi*rTd}@HxB6#dRPZgPKn&ukFy6)kB0k~IND7FzmqQ=^eyw#hyYwhIF#$~Z
zE~sptiUn<3i_46pU8Nz<@U(D%o%UR*AU@zv*16&N
zX7s6b7H8#K3qXpD;0Uvb>ahs*g>}QW(R2=cX#qPsqEVD-?KtG6pD%NkBK3P
z^u}U$+LF>inSCC6rsiu`DGvnJ9%+a>zoJ-$#Zf1Ooa9I2%fv_4%hX313xYHs@#a*@
zWp$YaDCM3s)dEDLifIV
zJpm#^L@H^oZrAXVbM%Gi3uwLe0`dI*#&w6vy>xo-v~%fUIurjcb`p=$ai}(eWDeB>
zw@$41KNH)Y6Zh|Bu0uDmd&$&|V>i)1(|hHi_HUi34e$bwBx?^7qo=uBmM_X{kwW
zz*CD*JnVT1ermhYz`L?-_3&VihYUroB@k-Jkbi;DT#?FrKxJsf82zj02hs=&(oQu)
zBx+dGE<1q8Y6vBD^po1@hf!ZFk~$gw1apU#Ujw8aZG%>=rFdKc@
z0|fN*FN64hw~_xp2tfZQd-&h>zgx)v2aUrxe)GTPXNCNzH#gU;cy!#^pz%2CR8Wcx
z;|QRl3JBaXZOgXKU-}zqF%0pA+3p2H0{*-GUkeQhYi#QC?O0BHhad14$jSh)TmQG$
zY^zfSYfEvx0+pJEaD-&_yXN$V2leE2bN`)WPs_XuJ`xCl>Key+}
zfAo!?Cj0}(nIg(KDD|yiNy@k=4S8x!KtmATlI2fI#`u|oEM)Cfrm|)YR(!ZvKus+h8`y_4}oXsgBC`-`gpi!wqctkCJop@A-dkC*glg2LHzn7OO+K
z;fy-_2myh%1;8g17@gLwCYkwjh~ptq3ANzjH^?9rpel;#ji;pc1!zO98M|O4n5{}~
zjG4!@m?M{lCyh94uQ+K@L~BA76$+oDbviE=nJ*TZZj9J$l&EFwgcg3<0>u$RO_&$T
z5tx`BPfumOe82RvdmVA-_Q(ovipb6lJD!BN^6qd|6w95Nl(5cc%(RSXE~@$rjG5Qy
zr{8rY&okOwaOyZZNyk{q^6=J_%5esFEoO{aaEiq?%SH`9YzS}d@``qLv=q1A1XXoc
zHt=bU9sS;ovb?iCJwHy&o^
ztzC#{e)Rb11+!%D8tKvQ75#ljaZoc6g4MNa)c*0)41RJm
z7VDd+7%gu
zE5w;gM__dxG_)$gMX4%>Fux^74GG+{d>ornreNkNy#SqJ(=K-V3?EG@$c~@>sGPn4
zoQKaqwzNTZUdcqbV#27MUQhvavl46pXVk4eM?T}0kJWc~;F3VL7yjikPped{wWWh6
zzqOhfM5k@~-XjiMQr+b^;T5giOm@S9r
z8?nHKkoDv#phIr15J5b#5({#D#LS!JQ{yPa28I@?cJtw)e)#i>stLNz@$Td30pluM08sQ
zoa$QFwA?5v*G!sat}TkvBr|K9^lw(XPsm&+!MPrGQAC2;b+2U&+HzLay`GFbR&fGR
zmj*#!@1reJGgG*vu6mSTAMp^L`LS9`r_SmRz2kTwKN+c
z>l=T4*m%SW@=M&i#tnBN#>$8Xo#$3;IB>rKbsy*^%R)>G1j^?`4ia
zSAQ?mbx&14Nr^nwa?-eE;(42k
zrka@Zwz3Xg>>PM(7UR}k3|r$YQ)5WB+P3}laBPiDIe>?$h6qs<`i<^9$XY+2yPO!C
z;_aX4-_WGdyh09yM!7a?SxDe+b9#ClBvY60vQ?QXyb`7~z8av$#F{3S()B7}koIJ(
zqc)IXMb|rh(1kv~g%gwRrjWckAzX5>kwt?_0VP3mV=AW!CR7=&@+h1n#IWtW?Lw9#
ze1-|cNU8gf^RI
zGihfh9WD`8?a*rP4Im%^d;aJMM|jIYutnB;E(=EQfYrA%Q;eb)_cJre{F77nBrLMq
zeBm+yQUszh#|Cxt^y)vyARl1wpo8~`0`KncU|_>rrGJpX9vDHU9L1)mI7Ss8sL85M
zy~ffgRn*i_J0`Y{R%KEfv-0M;49sga#x7bO>7CL_uu^>OcYIWceL6zI1|E!hjW0YM
zJ@7k;f%qqjCrwYePDhqzJC4GGiFh&ze1m_X$N10Inf
zE>BL45hoMCSVEtPv<^&TRH>=bfP7(=OA3vRcOL)&*Y7!HP?Fzl-hh0yQ`c9cCAX(H
zrLtdktAFVjCsY4~qES4eH)zDXfPl49Gko^sZB}fv_&y_`zC(YIv7D@1_%k(q%jYvJ
z668B1LD{*e%Ky^@+H^{uoSIc^o6VieZLs`-1(ZAI<$mMZaR(kk1^$qol=bG-KFp$Y
zLAOi9%Nvo8ft{tErefv$MpHM;QWKuG6~qBs_sl(6xg+Yd__Z`wn^|E?9
z)6%BZFcf%a8(rjI>?Op24a7-v0)DhjKBVz>GTUmIF)M_<`SCNTli}QkIP1lv{*n06
z$*w|B2D|?e#7Cpg>FmhF*QKtL-oE1D;UNUE>fN@u8^A^|+{($-;xWq^g
zq^LUzI??9$Z;a`eiZU*u&k|Rwo65Tgh~BIluN6*!4FbcQi4QwvN_ZYIYQ2}hfKCHb
ztz?@4Sr)0P3cleH?^glTdFRUBp3$<1RH6iKn`RSbfkst&FYOsd>#qWFnXI#^1roTR
z4!y4}To+A>6&hkvHb3B7xASFOl5HiRy+Ajqa+8|->MhsuX+)}h7kdaT;Mhs>U1puJ
z{8O&WQe{^SU3bNj_&tiD7C#jNw1>Ln2}zPUpfkeVj*yret0E^ytHE_1Q0L{=7v@
znkBSIsN7OYc_qzzWQKrCgY?e5DMt5@NKXnWu$;AMV1&e%DNiWkCMWQHd?12imcbsD
zrno7o!rQ}4kyzqulx}rB4sG`h7k%Jm0r|&3D#pfDu}@nxBw4{3yhfy|&+p}F5#K2d
zOr*?@rz+L4_T=w=uu1!GzqL^p+^nC~G*m>fexnwzHoS%a;^B7h#6O&K>eFxy
zEHGgQe{|Y+rORx_mn}F5@%Q#SMb7|u^RW)X2i3>p+*iNi(NWu3l}$C8k}QMZyG78G
zIB<|j&>PIa5*2RHF84tr(yD&v3H$TkV-
z#4aFURp%t_jwK#j{_I4$c#}GNWn|RMpD*pQGz-@(Y@Dh<57#WbPa^ZdHTkoppoc@$
z72#hsm=%e;;?pgpL>5iSd)Y-?N1~f+S=<^|0*4*4VxodUdsbcBx5kd$Dq1y2tO(o&uNe%qH-&o`y%WJc*i|_V-&hF(jkL
zn{V)o5cs1a?shBFu{DTW5%0Ul$E@V9tOwo6lq)kWBxRI@=3Rn8i~XzciPp~BtKRBA
z63CG079`JK?3k7jov-tW0PY-~kP0JX)akjA{AC}Ne#ax0-s1RoU+5nMDu35R>ejhB2@y5)hcbr^mG#<)0|l&CYJeX>R#}X`10(4NbEV
z@;YwD{LOBB*8BxUaQXCW?E=k<-R@0C)Re)5$E=fhLD!z8cg$wvvfWh(wROEOCt8@@
zD%8;Q^j_rN9l)Urv8@|;Sd1~zCsME6Vz0)@mD4S%rd0kV->6M&Nkz_Oj{eb`L!F|g2f)!%0p>`M;ImV-2Blc$SR5u
z=CP>p8d;S$iwt{@aGJ@b+kTF`InxFZQrHQ(F&HcSM}#4PbXupb^DoSm)&M`obY>)1
zN`XgYE6uL4DOFQOqZS(=Af_08RNowm(EN|^yuRD2)ox}wkyFLfdvutf$&W!zho}`z
zfCf=&^1K8SHPUJyl$O3>?JwVw4=k6QtH^vDc(acmJW|+h_F_Pcg?1by}~YwLF!
zHHP><2&^}Q%u4uwc$2^+#^PR%(!`tv8_md!Hc}(Lai4>-q}HA@Px!%P&^`b
zAo$tJN`LCk-F5l?-mEuoe37*Rn4G-bHVgM<9A#~@4wGAQbQ|}>9Z(IroXuBIS|F&b
zbDsCvK7@K!v(mKNB-B#T8RR>z$nxeAH=EwbTp^9_9@I0e(6Uw3x}ouLcdEnvE`Du+
zGK6}sZk{kG+R=40wk@=o&p_X&Nrzjitc2&g%G6&
z{6uM6t#wSFK(wF+2E2kL*%=-~?w$AH+>z+=S<+dYkb1LWI-ESD_G@9{RZqqUNFRRR
zRXCJ4Dt@Ip4!BP&H$i6RbPosT3-%BxRXfN}5NU}3Gujz%pmg>aeJ29qY_nt-edTzU@J9H$oM82r9fW=vom0&)d
z3E7OBEnnml?0M5G(?>(lJ-bwXKPszDK(MY)4+GK}+5)&C-3{-jm_^%Fx|j)2KQe9JB+hDnJf^9>;MWwfi{^D_WVWxtjai
z*-DR}gi4ZHj`t4M_c_rFDukE)@t)z<7>9_?hqoZmSjCq`3~m^w@3S=^tHjhiJVcus
zoe{yQYd!eWL1r1Gdq@i_ui0E$Vq?4bmH;!3f%(9>S)04U4FK;OJDC0RL`13SYTi&M
zI#Q6~biS@5o0G`QgA-0oD^c8uZvR<@c?^NL^68_u2_IxQK&fOO<+Fjcr9M#U8od3n
zQK6u^7v@ynKc{8}9#gLovSA>mSd@L}{gUEn*k8Ys_4v&pMJuCVFasVX(9AdTYeNh_7)B)yf@6fGA
zx>r?2V7iI0C6hKrWsH7&jd|=^i9M6NlUEt?Jorkv5T^NREU}QR@Ayh}9uw=;xlg
z@h-VPwxwK~;pC>bvb86NUZmynNMV8oh%lhB>$#)OlJ=ScNFr^E+$}hJX|?b(fU1c^
z6lImajjwZH3i`|q7;#(GJ~J3MIJWrJ5j{0WMp!*sST>=hdxj{9xTg5$_p0r8F}*(5
zfR7*DTbV-d&(oB~V*_n+S@M}Ssy`k~@z>;PD#)!R((>@pm8N3jKmmr3koNONrX4QV
zrQoX??~^HzC(nnWPpkVKfJDZk^oklZ3stx}+_9hM``enhD({wkjSuan5L59hBpt^q
z;mR*jFJ!0Pq4?mm7Q5b?nSme$B7b-As(0X_S_K3%K+5hJ;m$8~Aqg*1)AvfXJQ&5D
z2gP6_>RxAg#>Ub_zjdW+p*dxBjVjg6(J}5;yMfdz6izmLfj2_N>TSy*^n+i1Hf%PV
z53>_0&Pm@PPH=#5Qm8()@N6$#Xl^EYJ)J0E5#c~rd!g%Y4nAi&~Yaa}a{W&D1O17)|rEK|;lTk>1d-ZD1{OJu6mlKX?d
ze=_it>%wHNX~tb}Z7J~$;kzeeoBTSXpp0L$?2jNll{G&(J{c5sVG}~JP0*o8(I|R6
z+Agbd`x3)IX5PUUo<}YXD}d=qDMN<`jHz;V)H>|~+)edSZzoq-V>h5!-mRc?Z@(B)
zO4AIX@;iSl*zDH0RtKkc2~?rM(x#Gv3zmAfuILpZu
z)LTz$WT7SByXA{!Xk}tLhb8=ks+(`3ad?Qb?nB>7|1oc)z{`W>otP|ySU)gQ790D|
z20W9v5RfRs)Z$qYg{oia9-Pl?>z+QZ%0-`op9HXv+`k~pBc_EN9aCv4{iG2(!}
zFn1>Hysi9RSE83++?ZYl>fHMVO$yGP4OA}pz=fO2{DIl@^$S@(uYNb=(YC$G!!~a=
zZHsSn(p30JNB$!Naw`olKuF5q6qR^|9!xp?#To<4N8g{B^K#eTmcwbTwPg21M^Rk5
z3uaPFQt|3k)!=9zP!wV82`=gBL>nbQU#U}L=2Sh{dq$i;K7Yg02njOvI2DtA#<^$>
zhr1L-YN7~JJx#kCK+cUcz2h>~-EmLuMk_Hp*RPc_JyV@_T9F6nkY|X=^fx}aJx@+xApA#6
z#`a#+wjXg-auvlM493l!wTh3>N92<&JN+th>am_KbvR
zhjCl5S=e@aEd`n|6<ICvtGs}-`gUq{Veqhy|8P@3F&kM1
zAcT(QCe^|Q0Zlp&lD&+~9Z$wsnV?}6dLY&faaPvl`5QcO<T9TS33R&$cn^Bp{(QGE;^ZHLrts=5sh~1zY1bZ`HUt|HtSG7SI#J2fOg@E!
ztXz6qz~viXj3pTCkrQt`NMNlN&KRE-zKKf#aUKq`;Yb?f9^x{000Eb(-Kz^_}RR)GT-$y`v3@W1=AaiBcA9cbq7F)cw4|Vkm%p
z#HbA66XC7SJ|UTJ&zGQ$jCk)k=Ok(T+nfo-ZI%0Zq^oUQ^pg_37wRk@GO}D}IF>+J
zrBiXHmY0kcXt$QdUhW3u@XhJTGxO4T9SATLqK`Zrh}^=)!Z9$3%PGuJ)BR*mLkUnH
zlfa-4Pn=l7ZrDH78EZL7^J9MG4gaN
z-q@LJ__kkHm44um5UZcihno*fyLwSN{7D)!P%@t`^=YrWy%3W0jVO}}DZokB$^BAu
zLm#`RFf~(WE4Qm^rl)c*laVYVDB+%eg0G8t?{}M440rZtiKZ^}vVB1UpC=TX18?6<
zw!aP=0BD&bN9H-SzODwZ=!zK>nH+@zBz=C6cIj
zS<4PbVl3M*kA>9=L(@;Zjn38i?E;eS5XtX`GGIkHi@j#&Y}@@ZrK6^%NKHXcj0z5v
z9#u4BS&G2`l+H^?Yc5;zoh&1>zN!vegU
z-vH$8NLGC1G~-}v3#WFY-!n<(v^o(kxi1x@D?*J
z0C8X?O5($nw~D(#n0Ix2mm8hcTYGjgB6h)nP%jzu)ikM(%;S{2#d2fRe^^*x1mHqY
z=Fri`4!H_peQTile?U|lZ%!N@8hfc2{NG*&=J$7OtqzId8jh-`FV%~gzB8u$UnGx+;^n3
z9!27*+HCzmeV`n5msZz*v})30x=qsqg0tWMQnq{hd1wYeW3Bu;r0X@a_qV*Hx5ew}
z^xeitBy0Hb5XJqS*d+3O13S6b?_9TkHN2nfcx~^3sSF-s63|D!yJ|567~J(|@tIlA
z?D#rsjF{F@p(!ia_p&~4o=F$YE2?tz=UDo1m3m-VU6q&1STk4o?FMk=fCO#FIP-e$
zqlaHV59q4}m6r>JF!MZ%a(G?{ma^g}yIuR42=}FGRzUbMtlM`D9U+8fK^^wI>cob3
z0}+vk5Qq@a(P7vi1ZsrJ`aH@d90Y_G1_T6r*i-9lfF;#LS!EQY*yUx!B^6X9)zwfD
zV8hgZra3;SnlpiIH-mkM&ZGgss$Kt00~S?~k&;wZV*^Y5YbvBRl$qNw5hvK^&Y4tG
zuvCsNmh6^)`UJ6?+F7%oq8P!@>hAV1|5TVIzPZx{oz?R#w=*jTJHVdgFJT>IczNR_
zEb%YXxe!WnKDq6SYpJ@hu_OR)a^aQ&?$W^Uo=0;31GxVovjoBVw^eU
z$-lKO=cb=;=4rGroG5(~oAg)K{Oiv!l9=pk-&Ndp(`R#&o}QhjP2~r#0T_y)!$r*r-4dmlSgp>Bl(|4>Jqx!
z`8_|){ed%?PJ)*I_sOqxGd#^ZIeV3j3!IVoFW^{H0RA`Q4uOvx67ePU4ov
zg?X=FfOvl2Prt1Rcg8yjeXUD0{;ot^;FEV=;PirS_)DKBF>imNz<(BTpQnqQPkef5
z@!6w8{pixfm#hvyuW@?16~0LMB%ofGY5eBIo}M#<&()rUNW_I{FPynOzq6+&g3jLN
zhoUabdfDvT`Q)cd!SK1HlTeMhIQbQ3md=ZuE{>f&rR511id><_d|u=9U '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..9d21a21
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/local.properties b/local.properties
new file mode 100644
index 0000000..0bd5cc5
--- /dev/null
+++ b/local.properties
@@ -0,0 +1,8 @@
+## This file must *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+#
+# Location of the SDK. This is only used by Gradle.
+# For customization when using a Version Control System, please read the
+# header note.
+#Tue Jul 22 15:31:56 CLT 2025
+sdk.dir=C\:\\Users\\gmola\\AppData\\Local\\Android\\Sdk
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..76f3777
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,17 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "SundayAndroidApp"
+include(":app")
From 39a43159fa25fd6f741049a0f7a48c7a89141f9b Mon Sep 17 00:00:00 2001
From: gmolate
Date: Sat, 2 Aug 2025 16:13:04 -0400
Subject: [PATCH 3/5] add /ANDROID android version
# Sunday - Android Version
This is the Android version of [Sunday](https://github.com/jackjackbits/sunday), a powerful application for tracking UV exposure and Vitamin D generation, rebuilt from the ground up in Kotlin for the Android platform.
This version is designed to be completely self-contained within the `android/` directory.
---
app/build.gradle.kts | 86 ----
app/src/main/AndroidManifest.xml | 31 --
.../java/com/example/sunday/MainActivity.kt | 58 ---
.../example/sunday/SundayWidgetReceiver.kt | 9 -
.../example/sunday/data/model/DataModels.kt | 45 --
.../com/example/sunday/data/model/Enums.kt | 18 -
.../com/example/sunday/data/model/UvData.kt | 24 --
.../sunday/data/repository/Repository.kt | 30 --
.../sunday/domain/HealthConnectManager.kt | 49 ---
.../example/sunday/domain/LocationManager.kt | 38 --
.../sunday/domain/VitaminDCalculator.kt | 94 ----
.../com/example/sunday/network/ApiService.kt | 25 --
.../java/com/example/sunday/ui/MainScreen.kt | 281 ------------
.../com/example/sunday/ui/MainViewModel.kt | 240 -----------
.../com/example/sunday/ui/SundayWidget.kt | 46 --
.../com/example/sunday/ui/ViewModelFactory.kt | 25 --
.../java/com/example/sunday/ui/theme/Color.kt | 11 -
.../java/com/example/sunday/ui/theme/Theme.kt | 70 ---
.../com/example/sunday/ui/theme/Typography.kt | 34 --
.../example/sundayandroidapp/MainActivity.kt | 59 ---
.../api/OpenMeteoApiService.kt | 12 -
.../sundayandroidapp/api/OpenMeteoResponse.kt | 56 ---
.../sundayandroidapp/data/AppModels.kt | 26 --
.../location/LocationService.kt | 48 ---
.../logic/SunExposureCalculator.kt | 119 ------
.../sundayandroidapp/screens/HomeScreen.kt | 401 ------------------
.../screens/OptionsSelectorDialog.kt | 59 ---
.../screens/SessionTrackingCard.kt | 83 ----
.../sundayandroidapp/ui/theme/Color.kt | 11 -
.../sundayandroidapp/ui/theme/Gradient.kt | 11 -
.../sundayandroidapp/ui/theme/Shape.kt | 11 -
.../sundayandroidapp/ui/theme/Theme.kt | 70 ---
.../example/sundayandroidapp/ui/theme/Type.kt | 34 --
.../util/LocalisationManager.kt | 17 -
.../res/drawable/ic_launcher_foreground.xml | 13 -
app/src/main/res/layout/widget_layout.xml | 25 --
.../res/mipmap-anydpi-v26/ic_launcher.xml | 6 -
.../mipmap-anydpi-v26/ic_launcher_round.xml | 6 -
app/src/main/res/values-es/strings.xml | 32 --
app/src/main/res/values/colors.xml | 10 -
app/src/main/res/values/strings.xml | 32 --
app/src/main/res/values/themes.xml | 17 -
app/src/main/res/xml/backup_rules.xml | 12 -
.../main/res/xml/data_extraction_rules.xml | 20 -
app/src/main/res/xml/sunday_widget_info.xml | 11 -
build.gradle.kts | 16 -
gradle.properties | 28 --
gradle/libs.versions.toml | 30 --
gradle/wrapper/gradle-wrapper.jar | Bin 43739 -> 0 bytes
gradle/wrapper/gradle-wrapper.properties | 7 -
gradlew | 251 -----------
gradlew.bat | 94 ----
local.properties | 8 -
settings.gradle.kts | 17 -
54 files changed, 2866 deletions(-)
delete mode 100644 app/build.gradle.kts
delete mode 100644 app/src/main/AndroidManifest.xml
delete mode 100644 app/src/main/java/com/example/sunday/MainActivity.kt
delete mode 100644 app/src/main/java/com/example/sunday/SundayWidgetReceiver.kt
delete mode 100644 app/src/main/java/com/example/sunday/data/model/DataModels.kt
delete mode 100644 app/src/main/java/com/example/sunday/data/model/Enums.kt
delete mode 100644 app/src/main/java/com/example/sunday/data/model/UvData.kt
delete mode 100644 app/src/main/java/com/example/sunday/data/repository/Repository.kt
delete mode 100644 app/src/main/java/com/example/sunday/domain/HealthConnectManager.kt
delete mode 100644 app/src/main/java/com/example/sunday/domain/LocationManager.kt
delete mode 100644 app/src/main/java/com/example/sunday/domain/VitaminDCalculator.kt
delete mode 100644 app/src/main/java/com/example/sunday/network/ApiService.kt
delete mode 100644 app/src/main/java/com/example/sunday/ui/MainScreen.kt
delete mode 100644 app/src/main/java/com/example/sunday/ui/MainViewModel.kt
delete mode 100644 app/src/main/java/com/example/sunday/ui/SundayWidget.kt
delete mode 100644 app/src/main/java/com/example/sunday/ui/ViewModelFactory.kt
delete mode 100644 app/src/main/java/com/example/sunday/ui/theme/Color.kt
delete mode 100644 app/src/main/java/com/example/sunday/ui/theme/Theme.kt
delete mode 100644 app/src/main/java/com/example/sunday/ui/theme/Typography.kt
delete mode 100644 app/src/main/java/com/example/sundayandroidapp/MainActivity.kt
delete mode 100644 app/src/main/java/com/example/sundayandroidapp/api/OpenMeteoApiService.kt
delete mode 100644 app/src/main/java/com/example/sundayandroidapp/api/OpenMeteoResponse.kt
delete mode 100644 app/src/main/java/com/example/sundayandroidapp/data/AppModels.kt
delete mode 100644 app/src/main/java/com/example/sundayandroidapp/location/LocationService.kt
delete mode 100644 app/src/main/java/com/example/sundayandroidapp/logic/SunExposureCalculator.kt
delete mode 100644 app/src/main/java/com/example/sundayandroidapp/screens/HomeScreen.kt
delete mode 100644 app/src/main/java/com/example/sundayandroidapp/screens/OptionsSelectorDialog.kt
delete mode 100644 app/src/main/java/com/example/sundayandroidapp/screens/SessionTrackingCard.kt
delete mode 100644 app/src/main/java/com/example/sundayandroidapp/ui/theme/Color.kt
delete mode 100644 app/src/main/java/com/example/sundayandroidapp/ui/theme/Gradient.kt
delete mode 100644 app/src/main/java/com/example/sundayandroidapp/ui/theme/Shape.kt
delete mode 100644 app/src/main/java/com/example/sundayandroidapp/ui/theme/Theme.kt
delete mode 100644 app/src/main/java/com/example/sundayandroidapp/ui/theme/Type.kt
delete mode 100644 app/src/main/java/com/example/sundayandroidapp/util/LocalisationManager.kt
delete mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml
delete mode 100644 app/src/main/res/layout/widget_layout.xml
delete mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
delete mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
delete mode 100644 app/src/main/res/values-es/strings.xml
delete mode 100644 app/src/main/res/values/colors.xml
delete mode 100644 app/src/main/res/values/strings.xml
delete mode 100644 app/src/main/res/values/themes.xml
delete mode 100644 app/src/main/res/xml/backup_rules.xml
delete mode 100644 app/src/main/res/xml/data_extraction_rules.xml
delete mode 100644 app/src/main/res/xml/sunday_widget_info.xml
delete mode 100644 build.gradle.kts
delete mode 100644 gradle.properties
delete mode 100644 gradle/libs.versions.toml
delete mode 100644 gradle/wrapper/gradle-wrapper.jar
delete mode 100644 gradle/wrapper/gradle-wrapper.properties
delete mode 100644 gradlew
delete mode 100644 gradlew.bat
delete mode 100644 local.properties
delete mode 100644 settings.gradle.kts
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
deleted file mode 100644
index fa1f172..0000000
--- a/app/build.gradle.kts
+++ /dev/null
@@ -1,86 +0,0 @@
-plugins {
- alias(libs.plugins.android.application)
- alias(libs.plugins.jetbrains.kotlin.android)
-}
-
-android {
- namespace = "com.example.sundayandroidapp"
- compileSdk = 34
-
- defaultConfig {
- applicationId = "com.example.sundayandroidapp"
- minSdk = 24
- targetSdk = 34
- versionCode = 1
- versionName = "1.0"
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- vectorDrawables {
- useSupportLibrary = true
- }
- }
-
- buildTypes {
- release {
- isMinifyEnabled = false
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
- )
- }
- }
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
- }
- kotlinOptions {
- jvmTarget = "1.8"
- }
- buildFeatures {
- compose = true
- }
- composeOptions {
- kotlinCompilerExtensionVersion = "1.5.1"
- }
- packaging {
- resources {
- excludes += "/META-INF/{AL2.0,LGPL2.1}"
- }
- }
-}
-
-dependencies {
-
- implementation(libs.core.ktx)
- implementation(libs.lifecycle.runtime.ktx)
- implementation(libs.activity.compose)
- implementation(platform(libs.compose.bom))
- implementation(libs.ui)
- implementation(libs.ui.graphics)
- implementation(libs.ui.tooling.preview)
- implementation(libs.material3)
-
- // Google Play Services for Location
- implementation("com.google.android.gms:play-services-location:21.0.1")
-
- // Retrofit and Gson for API calls
- implementation("com.squareup.retrofit2:retrofit:2.9.0")
- implementation("com.squareup.retrofit2:converter-gson:2.9.0")
- implementation("com.google.code.gson:gson:2.10.1")
-
- // Splash Screen API
- implementation("androidx.core:core-splashscreen:1.0.1")
-
- // Explicit Material Components dependency (for theme resolution)
- implementation("com.google.android.material:material:1.12.0")
-
- testImplementation(libs.junit)
- androidTestImplementation(libs.androidx.test.ext.junit)
- androidTestImplementation(libs.espresso.core)
- androidTestImplementation(platform(libs.compose.bom))
- androidTestImplementation(libs.ui.test.junit4)
- debugImplementation(libs.ui.tooling)droid resource linking failed
- com.example.sundayandroidapp-mergeDebugResources-58:/values/values.xml:6443: error: style attribute 'android:attr/postSplashScreenTheme' not found.
- error: failed linking references.
- debugImplementation(libs.ui.test.manifest)
-}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
deleted file mode 100644
index 5ffd0a7..0000000
--- a/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/java/com/example/sunday/MainActivity.kt b/app/src/main/java/com/example/sunday/MainActivity.kt
deleted file mode 100644
index 5bad0fa..0000000
--- a/app/src/main/java/com/example/sunday/MainActivity.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.example.sunday
-
-import android.Manifest
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.activity.viewModels
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.ui.Modifier
-import com.example.sunday.ui.MainScreen
-import com.example.sunday.ui.MainViewModel
-import com.example.sunday.ui.ViewModelFactory
-import com.example.sunday.ui.theme.SundayTheme
-
-class MainActivity : ComponentActivity() {
-
- private val viewModel: MainViewModel by viewModels { ViewModelFactory(this) }
-
- private val requestPermissionLauncher =
- registerForActivityResult(
- ActivityResultContracts.RequestMultiplePermissions()
- ) { permissions ->
- if (permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true ||
- permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true
- ) {
- viewModel.fetchData()
- }
- if (permissions.keys.any { it.startsWith("android.permission.health") }) {
- viewModel.requestPermissions()
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- requestPermissionLauncher.launch(
- arrayOf(
- Manifest.permission.ACCESS_FINE_LOCATION,
- Manifest.permission.ACCESS_COARSE_LOCATION,
- "android.permission.health.READ_VITAMIN_D",
- "android.permission.health.WRITE_VITAMIN_D"
- )
- )
- setContent {
- SundayTheme {
- // A surface container using the 'background' color from the theme
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = MaterialTheme.colorScheme.background
- ) {
- MainScreen(viewModel = viewModel)
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/com/example/sunday/SundayWidgetReceiver.kt b/app/src/main/java/com/example/sunday/SundayWidgetReceiver.kt
deleted file mode 100644
index 4ba1f5a..0000000
--- a/app/src/main/java/com/example/sunday/SundayWidgetReceiver.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.example.sunday
-
-import androidx.glance.appwidget.GlanceAppWidget
-import androidx.glance.appwidget.GlanceAppWidgetReceiver
-import com.example.sunday.ui.SundayWidget
-
-class SundayWidgetReceiver : GlanceAppWidgetReceiver() {
- override val glanceAppWidget: GlanceAppWidget = SundayWidget()
-}
diff --git a/app/src/main/java/com/example/sunday/data/model/DataModels.kt b/app/src/main/java/com/example/sunday/data/model/DataModels.kt
deleted file mode 100644
index 99f1cf6..0000000
--- a/app/src/main/java/com/example/sunday/data/model/DataModels.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.example.sunday.data.model
-
-import com.google.gson.annotations.SerializedName
-
-data class OpenMeteoResponse(
- val daily: DailyData,
- val hourly: HourlyData?
-)
-
-data class DailyData(
- val time: List,
- @SerializedName("uv_index_max")
- val uvIndexMax: List,
- @SerializedName("uv_index_clear_sky_max")
- val uvIndexClearSkyMax: List?,
- val sunrise: List,
- val sunset: List
-)
-
-data class HourlyData(
- val time: List,
- @SerializedName("uv_index")
- val uvIndex: List,
- @SerializedName("cloud_cover")
- val cloudCover: List?
-)
-
-data class FarmsenseResponse(
- @SerializedName("Phase")
- val phase: String,
- @SerializedName("Illumination")
- val illumination: Double,
- @SerializedName("Age")
- val age: Double,
- @SerializedName("Distance")
- val distance: Double,
- @SerializedName("Azimuth")
- val azimuth: Double,
- @SerializedName("Altitude")
- val altitude: Double,
- @SerializedName("EclipticLongitude")
- val eclipticLongitude: Double,
- @SerializedName("EclipticLatitude")
- val eclipticLatitude: Double
-)
diff --git a/app/src/main/java/com/example/sunday/data/model/Enums.kt b/app/src/main/java/com/example/sunday/data/model/Enums.kt
deleted file mode 100644
index 016f1d3..0000000
--- a/app/src/main/java/com/example/sunday/data/model/Enums.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.example.sunday.data.model
-
-enum class ClothingLevel(val descriptionText: String, val exposureFactor: Double) {
- NONE("Nude!", 1.0),
- MINIMAL("Minimal (swimwear)", 0.80),
- LIGHT("Light (shorts, tee)", 0.50),
- MODERATE("Moderate (pants, tee)", 0.30),
- HEAVY("Heavy (pants, sleeves)", 0.10)
-}
-
-enum class SkinType(val descriptionText: String, val vitaminDFactor: Double) {
- TYPE1("Very fair", 1.25),
- TYPE2("Fair", 1.1),
- TYPE3("Light", 1.0),
- TYPE4("Medium", 0.7),
- TYPE5("Dark", 0.4),
- TYPE6("Very dark", 0.2)
-}
diff --git a/app/src/main/java/com/example/sunday/data/model/UvData.kt b/app/src/main/java/com/example/sunday/data/model/UvData.kt
deleted file mode 100644
index f378010..0000000
--- a/app/src/main/java/com/example/sunday/data/model/UvData.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.example.sunday.data.model
-
-import java.util.Date
-
-data class UvData(
- val currentUV: Double = 0.0,
- val maxUV: Double = 0.0,
- val tomorrowMaxUV: Double = 0.0,
- val burnTimeMinutes: Map = emptyMap(),
- val todaySunrise: Date? = null,
- val todaySunset: Date? = null,
- val tomorrowSunrise: Date? = null,
- val tomorrowSunset: Date? = null,
- val currentAltitude: Double = 0.0,
- val uvMultiplier: Double = 1.0,
- val currentCloudCover: Double = 0.0,
- val currentMoonPhase: Double = 0.0,
- val currentMoonPhaseName: String = "",
- val isVitaminDWinter: Boolean = false,
- val currentLatitude: Double = 0.0,
- val isOfflineMode: Boolean = false,
- val lastSuccessfulUpdate: Date? = null,
- val hasNoData: Boolean = false
-)
diff --git a/app/src/main/java/com/example/sunday/data/repository/Repository.kt b/app/src/main/java/com/example/sunday/data/repository/Repository.kt
deleted file mode 100644
index 5b12f40..0000000
--- a/app/src/main/java/com/example/sunday/data/repository/Repository.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.example.sunday.data.repository
-
-import com.example.sunday.network.ApiService
-import retrofit2.Retrofit
-import retrofit2.converter.gson.GsonConverterFactory
-
-class Repository {
-
- private val openMeteoApiService: ApiService by lazy {
- Retrofit.Builder()
- .baseUrl("https://api.open-meteo.com/")
- .addConverterFactory(GsonConverterFactory.create())
- .build()
- .create(ApiService::class.java)
- }
-
- private val farmsenseApiService: ApiService by lazy {
- Retrofit.Builder()
- .baseUrl("https://api.farmsense.net/")
- .addConverterFactory(GsonConverterFactory.create())
- .build()
- .create(ApiService::class.java)
- }
-
- suspend fun getUvData(latitude: Double, longitude: Double, elevation: Double) =
- openMeteoApiService.getUvData(latitude, longitude, elevation)
-
- suspend fun getMoonPhase(timestamp: Int) =
- farmsenseApiService.getMoonPhase(timestamp)
-}
diff --git a/app/src/main/java/com/example/sunday/domain/HealthConnectManager.kt b/app/src/main/java/com/example/sunday/domain/HealthConnectManager.kt
deleted file mode 100644
index ff708e6..0000000
--- a/app/src/main/java/com/example/sunday/domain/HealthConnectManager.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package com.example.sunday.domain
-
-import android.content.Context
-import androidx.health.connect.client.HealthConnectClient
-import androidx.health.connect.client.permission.HealthPermission
-import androidx.health.connect.client.records.VitaminDRecord
-import androidx.health.connect.client.request.ReadRecordsRequest
-import androidx.health.connect.client.time.TimeRangeFilter
-import androidx.health.connect.client.units.InternationalUnits
-import java.time.Instant
-import java.time.ZonedDateTime
-
-class HealthConnectManager(private val context: Context) {
-
- private val healthConnectClient: HealthConnectClient by lazy {
- HealthConnectClient.getOrCreate(context)
- }
-
- suspend fun checkAvailability(): Int {
- return HealthConnectClient.sdkStatus(context)
- }
-
- suspend fun requestPermissions(permissions: Set) {
- val request = androidx.health.connect.client.permission.PermissionRequest.create(permissions)
- healthConnectClient.permissionController.requestPermissions(request)
- }
-
- suspend fun writeVitaminD(amount: Double, startTime: ZonedDateTime, endTime: ZonedDateTime) {
- val records = listOf(
- VitaminDRecord(
- amount = InternationalUnits(amount),
- startTime = startTime.toInstant(),
- endTime = endTime.toInstant(),
- startZoneOffset = startTime.offset,
- endZoneOffset = endTime.offset
- )
- )
- healthConnectClient.insertRecords(records)
- }
-
- suspend fun readVitaminDHistory(startTime: Instant, endTime: Instant): List {
- val request = ReadRecordsRequest(
- recordType = VitaminDRecord::class,
- timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
- )
- val response = healthConnectClient.readRecords(request)
- return response.records
- }
-}
diff --git a/app/src/main/java/com/example/sunday/domain/LocationManager.kt b/app/src/main/java/com/example/sunday/domain/LocationManager.kt
deleted file mode 100644
index 8d6c01e..0000000
--- a/app/src/main/java/com/example/sunday/domain/LocationManager.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.example.sunday.domain
-
-import android.Manifest
-import android.content.Context
-import android.content.pm.PackageManager
-import android.location.Location
-import androidx.core.content.ContextCompat
-import com.google.android.gms.location.FusedLocationProviderClient
-import com.google.android.gms.location.LocationServices
-import kotlinx.coroutines.tasks.await
-
-class LocationManager(private val context: Context) {
-
- private val fusedLocationClient: FusedLocationProviderClient =
- LocationServices.getFusedLocationProviderClient(context)
-
- suspend fun getCurrentLocation(): Location? {
- val hasAccessFineLocationPermission = ContextCompat.checkSelfPermission(
- context,
- Manifest.permission.ACCESS_FINE_LOCATION
- ) == PackageManager.PERMISSION_GRANTED
-
- val hasAccessCoarseLocationPermission = ContextCompat.checkSelfPermission(
- context,
- Manifest.permission.ACCESS_COARSE_LOCATION
- ) == PackageManager.PERMISSION_GRANTED
-
- if (!hasAccessFineLocationPermission && !hasAccessCoarseLocationPermission) {
- return null
- }
-
- return try {
- fusedLocationClient.lastLocation.await()
- } catch (e: Exception) {
- null
- }
- }
-}
diff --git a/app/src/main/java/com/example/sunday/domain/VitaminDCalculator.kt b/app/src/main/java/com/example/sunday/domain/VitaminDCalculator.kt
deleted file mode 100644
index 544e4cb..0000000
--- a/app/src/main/java/com/example/sunday/domain/VitaminDCalculator.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-package com.example.sunday.domain
-
-import com.example.sunday.data.model.ClothingLevel
-import com.example.sunday.data.model.SkinType
-import java.util.Calendar
-import kotlin.math.abs
-import kotlin.math.exp
-import kotlin.math.max
-
-class VitaminDCalculator {
-
- // UV response curve parameters
- private val uvHalfMax = 4.0 // UV index for 50% vitamin D synthesis rate
- private val uvMaxFactor = 3.0 // Maximum multiplication factor at high UV
-
- fun calculateVitaminDRate(
- uvIndex: Double,
- clothingLevel: ClothingLevel,
- skinType: SkinType,
- userAge: Int?,
- currentAdaptationFactor: Double
- ): Double {
- // Base rate: 21000 IU/hr for Type 3 skin with minimal clothing (80% exposure)
- val baseRate = 21000.0
-
- // UV factor: Michaelis-Menten-like saturation curve
- val uvFactor = (uvIndex * uvMaxFactor) / (uvHalfMax + uvIndex)
-
- // Exposure based on clothing coverage
- val exposureFactor = clothingLevel.exposureFactor
-
- // Skin type affects vitamin D synthesis efficiency
- val skinFactor = skinType.vitaminDFactor
-
- // Age factor
- val ageFactor = userAge?.let {
- when {
- it <= 20 -> 1.0
- it >= 70 -> 0.25
- else -> max(0.25, 1.0 - (it - 20) * 0.01)
- }
- } ?: 1.0
-
- // UV quality factor based on time of day
- val currentUVQualityFactor = calculateUVQualityFactor()
-
- // Final calculation
- return baseRate * uvFactor * exposureFactor * skinFactor * ageFactor * currentUVQualityFactor * currentAdaptationFactor
- }
-
- fun calculateBurnTime(currentUV: Double, skinType: SkinType): Int {
- val medTimesAtUV1 = mapOf(
- SkinType.TYPE1 to 150.0,
- SkinType.TYPE2 to 250.0,
- SkinType.TYPE3 to 425.0,
- SkinType.TYPE4 to 600.0,
- SkinType.TYPE5 to 850.0,
- SkinType.TYPE6 to 1100.0
- )
-
- val uvToUse = max(currentUV, 0.1)
- val medTime = medTimesAtUV1[skinType] ?: 425.0
- val fullMED = medTime / uvToUse
- return max(1, fullMED.toInt())
- }
-
- private fun calculateUVQualityFactor(): Double {
- val calendar = Calendar.getInstance()
- val hour = calendar.get(Calendar.HOUR_OF_DAY)
- val minute = calendar.get(Calendar.MINUTE)
-
- val timeDecimal = hour + minute / 60.0
- val solarNoon = 13.0
- val hoursFromNoon = abs(timeDecimal - solarNoon)
- val qualityFactor = exp(-hoursFromNoon * 0.2)
-
- return max(0.1, minOf(1.0, qualityFactor))
- }
-
- fun checkVitaminDWinter(latitude: Double, maxUv: Double): Boolean {
- val calendar = Calendar.getInstance()
- val month = calendar.get(Calendar.MONTH) // 0-11
-
- return if (latitude > 35) {
- when (month) {
- Calendar.NOVEMBER, Calendar.DECEMBER, Calendar.JANUARY, Calendar.FEBRUARY -> true
- Calendar.MARCH, Calendar.OCTOBER -> maxUv < 3.0
- else -> false
- }
- } else {
- maxUv < 3.0
- }
- }
-}
diff --git a/app/src/main/java/com/example/sunday/network/ApiService.kt b/app/src/main/java/com/example/sunday/network/ApiService.kt
deleted file mode 100644
index 6d86f55..0000000
--- a/app/src/main/java/com/example/sunday/network/ApiService.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.example.sunday.network
-
-import com.example.sunday.data.model.FarmsenseResponse
-import com.example.sunday.data.model.OpenMeteoResponse
-import retrofit2.http.GET
-import retrofit2.http.Query
-
-interface ApiService {
-
- @GET("v1/forecast")
- suspend fun getUvData(
- @Query("latitude") latitude: Double,
- @Query("longitude") longitude: Double,
- @Query("elevation") elevation: Double,
- @Query("daily") daily: String = "uv_index_max,uv_index_clear_sky_max,sunrise,sunset",
- @Query("hourly") hourly: String = "uv_index,cloud_cover",
- @Query("timezone") timezone: String = "auto",
- @Query("forecast_days") forecastDays: Int = 2
- ): OpenMeteoResponse
-
- @GET("v1/moonphases")
- suspend fun getMoonPhase(
- @Query("d") timestamp: Int
- ): List
-}
diff --git a/app/src/main/java/com/example/sunday/ui/MainScreen.kt b/app/src/main/java/com/example/sunday/ui/MainScreen.kt
deleted file mode 100644
index 763b688..0000000
--- a/app/src/main/java/com/example/sunday/ui/MainScreen.kt
+++ /dev/null
@@ -1,281 +0,0 @@
-package com.example.sunday.ui
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import com.example.sunday.data.model.ClothingLevel
-import com.example.sunday.data.model.SkinType
-import java.text.SimpleDateFormat
-import java.util.*
-
-@Composable
-fun MainScreen(viewModel: MainViewModel) {
- val uiState by viewModel.uiState.collectAsState()
- val uvData by viewModel.uvData.collectAsState()
-
- val gradientColors = getGradientColors()
-
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(Brush.verticalGradient(gradientColors))
- ) {
- if (uiState.isLoading) {
- CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
- } else if (uvData.hasNoData) {
- Text("No data available.", modifier = Modifier.align(Alignment.Center))
- } else {
- LazyColumn(
- modifier = Modifier.fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
- contentPadding = PaddingValues(16.dp)
- ) {
- item { Spacer(modifier = Modifier.height(32.dp)) }
- item { Header() }
- item { Spacer(modifier = Modifier.height(32.dp)) }
- item { UvIndexCard(uvData, uiState) }
- item { Spacer(modifier = Modifier.height(16.dp)) }
- item { SessionControl(uiState, viewModel) }
- item { Spacer(modifier = Modifier.height(16.dp)) }
- item { VitaminDCard(uiState) }
- item { Spacer(modifier = Modifier.height(16.dp)) }
- item { ClothingCard(uiState, viewModel) }
- item { Spacer(modifier = Modifier.height(16.dp)) }
- item { SkinTypeCard(uiState, viewModel) }
- }
- }
- }
-}
-
-@Composable
-fun Header() {
- Text(
- text = "SUN DAY",
- fontSize = 40.sp,
- fontWeight = FontWeight.Bold,
- color = Color.White
- )
-}
-
-@Composable
-fun UvIndexCard(uvData: com.example.sunday.data.model.UvData, uiState: UiState) {
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(8.dp),
- shape = RoundedCornerShape(20.dp),
- colors = CardDefaults.cardColors(containerColor = Color.Black.copy(alpha = 0.2f))
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text("UV INDEX", fontSize = 12.sp, color = Color.White.copy(alpha = 0.7f))
- Text(
- String.format("%.1f", uvData.currentUV),
- fontSize = 72.sp,
- fontWeight = FontWeight.Bold,
- color = Color.White
- )
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceAround
- ) {
- InfoColumn("BURN LIMIT", "${uvData.burnTimeMinutes[uiState.skinType.ordinal + 1] ?: "---"} min")
- InfoColumn("MAX UVI", String.format("%.1f", uvData.maxUV))
- InfoColumn("SUNRISE", formatTime(uvData.todaySunrise))
- InfoColumn("SUNSET", formatTime(uvData.todaySunset))
- }
- }
- }
-}
-
-@Composable
-fun SessionControl(uiState: UiState, viewModel: MainViewModel) {
- Button(
- onClick = { viewModel.toggleSession() },
- modifier = Modifier
- .fillMaxWidth()
- .padding(8.dp)
- .height(60.dp),
- shape = RoundedCornerShape(15.dp),
- colors = ButtonDefaults.buttonColors(
- containerColor = if (uiState.isSessionActive) Color.Yellow.copy(alpha = 0.3f) else Color.Black.copy(alpha = 0.2f)
- )
- ) {
- Text(if (uiState.isSessionActive) "End" else "Begin", fontSize = 18.sp, color = Color.White)
- }
-}
-
-@Composable
-fun VitaminDCard(uiState: UiState) {
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(8.dp),
- shape = RoundedCornerShape(20.dp),
- colors = CardDefaults.cardColors(containerColor = Color.Black.copy(alpha = 0.2f))
- ) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- horizontalArrangement = Arrangement.SpaceAround
- ) {
- InfoColumn("RATE", "${uiState.currentVitaminDRate.toInt()} IU/hr")
- InfoColumn("SESSION", "${uiState.sessionVitaminD.toInt()} IU")
- InfoColumn("TODAY", "${uiState.todaysTotalVitaminD.toInt()} IU")
- }
- }
-}
-
-@Composable
-fun ClothingCard(uiState: UiState, viewModel: MainViewModel) {
- var showDialog by remember { mutableStateOf(false) }
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(8.dp)
- .clickable { showDialog = true },
- shape = RoundedCornerShape(15.dp),
- colors = CardDefaults.cardColors(containerColor = Color.Black.copy(alpha = 0.2f))
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text("CLOTHING", fontSize = 10.sp, color = Color.White.copy(alpha = 0.7f))
- Text(uiState.clothingLevel.descriptionText, fontSize = 16.sp, color = Color.White)
- }
- }
- if (showDialog) {
- ClothingPickerDialog(
- onDismiss = { showDialog = false },
- onSelect = {
- viewModel.onClothingLevelChange(it)
- showDialog = false
- }
- )
- }
-}
-
-@Composable
-fun SkinTypeCard(uiState: UiState, viewModel: MainViewModel) {
- var showDialog by remember { mutableStateOf(false) }
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(8.dp)
- .clickable { showDialog = true },
- shape = RoundedCornerShape(15.dp),
- colors = CardDefaults.cardColors(containerColor = Color.Black.copy(alpha = 0.2f))
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text("SKIN TYPE", fontSize = 10.sp, color = Color.White.copy(alpha = 0.7f))
- Text(uiState.skinType.descriptionText, fontSize = 16.sp, color = Color.White)
- }
- }
-
- if (showDialog) {
- SkinTypePickerDialog(
- onDismiss = { showDialog = false },
- onSelect = {
- viewModel.onSkinTypeChange(it)
- showDialog = false
- }
- )
- }
-}
-
-@Composable
-fun InfoColumn(title: String, value: String) {
- Column(horizontalAlignment = Alignment.CenterHorizontally) {
- Text(title, fontSize = 9.sp, color = Color.White.copy(alpha = 0.6f))
- Text(value, fontSize = 20.sp, fontWeight = FontWeight.Bold, color = Color.White)
- }
-}
-
-@Composable
-fun ClothingPickerDialog(onDismiss: () -> Unit, onSelect: (ClothingLevel) -> Unit) {
- AlertDialog(
- onDismissRequest = onDismiss,
- title = { Text("Select Clothing Level") },
- text = {
- Column {
- ClothingLevel.values().forEach { level ->
- Text(level.descriptionText, modifier = Modifier
- .fillMaxWidth()
- .clickable { onSelect(level) }
- .padding(8.dp))
- }
- }
- },
- confirmButton = { }
- )
-}
-
-@Composable
-fun SkinTypePickerDialog(onDismiss: () -> Unit, onSelect: (SkinType) -> Unit) {
- AlertDialog(
- onDismissRequest = onDismiss,
- title = { Text("Select Skin Type") },
- text = {
- Column {
- SkinType.values().forEach { type ->
- Text(type.descriptionText, modifier = Modifier
- .fillMaxWidth()
- .clickable { onSelect(type) }
- .padding(8.dp))
- }
- }
- },
- confirmButton = { }
- )
-}
-
-
-fun formatTime(date: Date?): String {
- return date?.let {
- val formatter = SimpleDateFormat("h:mm a", Locale.getDefault())
- formatter.format(it)
- } ?: "--:--"
-}
-
-@Composable
-fun getGradientColors(): List {
- val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
- return when (hour) {
- in 0..4 -> listOf(Color(0xFF0f1c3d), Color(0xFF0a1228)) // Night
- 5 -> listOf(Color(0xFF1e3a5f), Color(0xFF2d4a7c)) // Pre-dawn
- 6 -> listOf(Color(0xFF3d5a80), Color(0xFF5c7cae)) // Early dawn
- 7 -> listOf(Color(0xFF5c7cae), Color(0xFFee9b7a)) // Dawn
- in 8..9 -> listOf(Color(0xFFf4a261), Color(0xFF87ceeb)) // Sunrise
- in 10..15 -> listOf(Color(0xFF4a90e2), Color(0xFF7bb7e5)) // Midday
- in 16..17 -> listOf(Color(0xFF5ca9d6), Color(0xFF87b8d4)) // Late afternoon
- 18 -> listOf(Color(0xFFf4a261), Color(0xFFe76f51)) // Golden hour
- 19 -> listOf(Color(0xFFe76f51), Color(0xFFc44569)) // Sunset
- 20 -> listOf(Color(0xFFc44569), Color(0xFF6a4c93)) // Late sunset
- else -> listOf(Color(0xFF6a4c93), Color(0xFF1e3a5f)) // Dusk
- }
-}
diff --git a/app/src/main/java/com/example/sunday/ui/MainViewModel.kt b/app/src/main/java/com/example/sunday/ui/MainViewModel.kt
deleted file mode 100644
index 20f102e..0000000
--- a/app/src/main/java/com/example/sunday/ui/MainViewModel.kt
+++ /dev/null
@@ -1,240 +0,0 @@
-package com.example.sunday.ui
-
-import android.content.Context
-import androidx.glance.appwidget.GlanceAppWidgetManager
-import androidx.health.connect.client.permission.HealthPermission
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.example.sunday.data.model.ClothingLevel
-import com.example.sunday.data.model.SkinType
-import com.example.sunday.data.model.UvData
-import com.example.sunday.data.repository.Repository
-import com.example.sunday.domain.HealthConnectManager
-import com.example.sunday.domain.LocationManager
-import com.example.sunday.domain.VitaminDCalculator
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.launch
-import java.text.SimpleDateFormat
-import java.time.Instant
-import java.time.ZonedDateTime
-import java.util.Calendar
-import java.util.Date
-import java.util.Locale
-import java.util.TimeZone
-import kotlin.math.roundToInt
-
-class MainViewModel(
- private val repository: Repository,
- private val locationManager: LocationManager,
- private val vitaminDCalculator: VitaminDCalculator,
- private val healthConnectManager: HealthConnectManager,
- private val context: Context
-) : ViewModel() {
-
- private val _uiState = MutableStateFlow(UiState())
- val uiState: StateFlow = _uiState
-
- private val _uvData = MutableStateFlow(UvData())
- val uvData: StateFlow = _uvData
-
- val permissions = setOf(
- HealthPermission.getReadPermission(androidx.health.connect.client.records.VitaminDRecord::class),
- HealthPermission.getWritePermission(androidx.health.connect.client.records.VitaminDRecord::class)
- )
-
- init {
- fetchData()
- readTodaysVitaminD()
- }
-
- fun fetchData() {
- viewModelScope.launch {
- _uiState.value = _uiState.value.copy(isLoading = true)
- try {
- locationManager.getCurrentLocation()?.let { location ->
- val response = repository.getUvData(location.latitude, location.longitude, location.altitude)
- val altitudeMultiplier = 1.0 + (location.altitude / 1000.0 * 0.1)
- val today = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
- val todayIndex = response.daily.time.indexOf(today)
-
- val todaySunrise = parseDate(response.daily.sunrise.getOrNull(todayIndex))
- val todaySunset = parseDate(response.daily.sunset.getOrNull(todayIndex))
-
- val tomorrowIndex = todayIndex + 1
- val tomorrowSunrise = parseDate(response.daily.sunrise.getOrNull(tomorrowIndex))
- val tomorrowSunset = parseDate(response.daily.sunset.getOrNull(tomorrowIndex))
-
-
- val now = Calendar.getInstance()
- val hour = now.get(Calendar.HOUR_OF_DAY)
- val currentUv = response.hourly?.uvIndex?.getOrNull(hour) ?: 0.0
- val maxUv = response.daily.uvIndexMax.getOrNull(todayIndex) ?: 0.0
- val tomorrowMaxUv = response.daily.uvIndexMax.getOrNull(tomorrowIndex) ?: 0.0
- val cloudCover = response.hourly?.cloudCover?.getOrNull(hour) ?: 0.0
-
- val isVitaminDWinter = vitaminDCalculator.checkVitaminDWinter(location.latitude, maxUv)
-
- val moonPhaseResponse = repository.getMoonPhase(
- (System.currentTimeMillis() / 1000).toInt()
- )
- val moonPhase = moonPhaseResponse.firstOrNull()
-
-
- _uvData.value = _uvData.value.copy(
- currentUV = currentUv * altitudeMultiplier,
- maxUV = maxUv * altitudeMultiplier,
- tomorrowMaxUV = tomorrowMaxUv * altitudeMultiplier,
- todaySunrise = todaySunrise,
- todaySunset = todaySunset,
- tomorrowSunrise = tomorrowSunrise,
- tomorrowSunset = tomorrowSunset,
- currentAltitude = location.altitude,
- uvMultiplier = altitudeMultiplier,
- currentCloudCover = cloudCover,
- currentMoonPhase = moonPhase?.illumination ?: 0.0,
- currentMoonPhaseName = moonPhase?.phase ?: "",
- isVitaminDWinter = isVitaminDWinter,
- currentLatitude = location.latitude,
- lastSuccessfulUpdate = Date(),
- hasNoData = false,
- isOfflineMode = false
- )
- calculateBurnTimes()
- calculateVitaminDRate()
- updateWidget()
- } ?: run {
- _uvData.value = _uvData.value.copy(hasNoData = true)
- }
- } catch (e: Exception) {
- _uvData.value = _uvData.value.copy(hasNoData = true)
- } finally {
- _uiState.value = _uiState.value.copy(isLoading = false)
- }
- }
- }
-
- fun onSkinTypeChange(skinType: SkinType) {
- _uiState.value = _uiState.value.copy(skinType = skinType)
- calculateBurnTimes()
- calculateVitaminDRate()
- updateWidget()
- }
-
- fun onClothingLevelChange(clothingLevel: ClothingLevel) {
- _uiState.value = _uiState.value.copy(clothingLevel = clothingLevel)
- calculateVitaminDRate()
- }
-
- fun toggleSession() {
- viewModelScope.launch {
- val isSessionActive = !_uiState.value.isSessionActive
- _uiState.value = _uiState.value.copy(isSessionActive = isSessionActive)
- if (isSessionActive) {
- _uiState.value = _uiState.value.copy(sessionStartTime = Date())
- startVitaminDUpdates()
- } else {
- val sessionVitaminD = _uiState.value.sessionVitaminD
- if (sessionVitaminD > 0) {
- healthConnectManager.writeVitaminD(
- sessionVitaminD,
- ZonedDateTime.ofInstant(
- _uiState.value.sessionStartTime!!.toInstant(),
- ZonedDateTime.now().zone
- ),
- ZonedDateTime.now()
- )
- }
- _uiState.value = _uiState.value.copy(sessionVitaminD = 0.0)
- readTodaysVitaminD()
- }
- }
- }
-
- private fun startVitaminDUpdates() {
- viewModelScope.launch {
- while (_uiState.value.isSessionActive) {
- val elapsedSeconds = (Date().time - (_uiState.value.sessionStartTime?.time ?: 0)) / 1000
- val vitaminDPerSecond = _uiState.value.currentVitaminDRate / 3600
- _uiState.value = _uiState.value.copy(
- sessionVitaminD = vitaminDPerSecond * elapsedSeconds
- )
- kotlinx.coroutines.delay(1000)
- }
- }
- }
-
-
- private fun calculateBurnTimes() {
- val burnTimes = SkinType.values().associateWith {
- vitaminDCalculator.calculateBurnTime(_uvData.value.currentUV, it)
- }
- _uvData.value = _uvData.value.copy(burnTimeMinutes = burnTimes.mapKeys { it.key.ordinal + 1 })
- }
-
- private fun calculateVitaminDRate() {
- val rate = vitaminDCalculator.calculateVitaminDRate(
- uvIndex = _uvData.value.currentUV,
- clothingLevel = _uiState.value.clothingLevel,
- skinType = _uiState.value.skinType,
- userAge = _uiState.value.userAge,
- currentAdaptationFactor = _uiState.value.currentAdaptationFactor
- )
- _uiState.value = _uiState.value.copy(currentVitaminDRate = rate)
- }
-
- private fun readTodaysVitaminD() {
- viewModelScope.launch {
- val startOfDay = ZonedDateTime.now().toLocalDate().atStartOfDay(ZonedDateTime.now().zone)
- val endOfDay = startOfDay.plusDays(1)
- val records = healthConnectManager.readVitaminDHistory(startOfDay.toInstant(), endOfDay.toInstant())
- val total = records.sumOf { it.amount.inInternationalUnits }
- _uiState.value = _uiState.value.copy(todaysTotalVitaminD = total)
- }
- }
-
-
- private fun parseDate(dateString: String?): Date? {
- return dateString?.let {
- val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm", Locale.getDefault())
- format.timeZone = TimeZone.getTimeZone("UTC")
- format.parse(it)
- }
- }
-
- fun requestPermissions() {
- viewModelScope.launch {
- healthConnectManager.requestPermissions(permissions)
- }
- }
-
- private fun updateWidget() {
- viewModelScope.launch {
- val prefs = context.getSharedPreferences("SundayWidgetPrefs", Context.MODE_PRIVATE)
- with(prefs.edit()) {
- putFloat("uv_index", _uvData.value.currentUV.toFloat())
- putInt("burn_time", _uvData.value.burnTimeMinutes[_uiState.value.skinType.ordinal + 1] ?: 0)
- apply()
- }
-
- val glanceAppWidgetManager = GlanceAppWidgetManager(context)
- val glanceIds = glanceAppWidgetManager.getGlanceIds(SundayWidget::class.java)
- glanceIds.forEach { glanceId ->
- SundayWidget().update(context, glanceId)
- }
- }
- }
-}
-
-data class UiState(
- val isLoading: Boolean = true,
- val skinType: SkinType = SkinType.TYPE3,
- val clothingLevel: ClothingLevel = ClothingLevel.LIGHT,
- val isSessionActive: Boolean = false,
- val sessionStartTime: Date? = null,
- val sessionVitaminD: Double = 0.0,
- val currentVitaminDRate: Double = 0.0,
- val todaysTotalVitaminD: Double = 0.0,
- val userAge: Int? = null,
- val currentAdaptationFactor: Double = 1.0
-)
diff --git a/app/src/main/java/com/example/sunday/ui/SundayWidget.kt b/app/src/main/java/com/example/sunday/ui/SundayWidget.kt
deleted file mode 100644
index 97293c4..0000000
--- a/app/src/main/java/com/example/sunday/ui/SundayWidget.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.example.sunday.ui
-
-import android.content.Context
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.glance.GlanceModifier
-import androidx.glance.appwidget.GlanceAppWidget
-import androidx.glance.background
-import androidx.glance.layout.Alignment
-import androidx.glance.layout.Column
-import androidx.glance.layout.fillMaxSize
-import androidx.glance.layout.padding
-import androidx.glance.text.Text
-import androidx.glance.text.TextStyle
-import androidx.glance.unit.ColorProvider
-
-class SundayWidget : GlanceAppWidget() {
-
- @Composable
- override fun Content() {
- val context = androidx.glance.LocalContext.current
- val prefs = context.getSharedPreferences("SundayWidgetPrefs", Context.MODE_PRIVATE)
- val uvIndex = prefs.getFloat("uv_index", 0.0f)
- val burnTime = prefs.getInt("burn_time", 0)
-
- Column(
- modifier = GlanceModifier
- .fillMaxSize()
- .background(Color.Black.copy(alpha = 0.5f))
- .padding(8.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = "UV: ${"%.1f".format(uvIndex)}",
- style = TextStyle(color = ColorProvider(Color.White), fontSize = 18.sp)
- )
- Text(
- text = "Burn: $burnTime min",
- style = TextStyle(color = ColorProvider(Color.White), fontSize = 14.sp)
- )
- }
- }
-}
diff --git a/app/src/main/java/com/example/sunday/ui/ViewModelFactory.kt b/app/src/main/java/com/example/sunday/ui/ViewModelFactory.kt
deleted file mode 100644
index 829f84f..0000000
--- a/app/src/main/java/com/example/sunday/ui/ViewModelFactory.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.example.sunday.ui
-
-import android.content.Context
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
-import com.example.sunday.data.repository.Repository
-import com.example.sunday.domain.HealthConnectManager
-import com.example.sunday.domain.LocationManager
-import com.example.sunday.domain.VitaminDCalculator
-
-class ViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
- override fun create(modelClass: Class): T {
- if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
- @Suppress("UNCHECKED_CAST")
- return MainViewModel(
- Repository(),
- LocationManager(context),
- VitaminDCalculator(),
- HealthConnectManager(context),
- context
- ) as T
- }
- throw IllegalArgumentException("Unknown ViewModel class")
- }
-}
diff --git a/app/src/main/java/com/example/sunday/ui/theme/Color.kt b/app/src/main/java/com/example/sunday/ui/theme/Color.kt
deleted file mode 100644
index d51bbf2..0000000
--- a/app/src/main/java/com/example/sunday/ui/theme/Color.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.example.sunday.ui.theme
-
-import androidx.compose.ui.graphics.Color
-
-val Purple80 = Color(0xFFD0BCFF)
-val PurpleGrey80 = Color(0xFFCCC2DC)
-val Pink80 = Color(0xFFEFB8C8)
-
-val Purple40 = Color(0xFF6650a4)
-val PurpleGrey40 = Color(0xFF625b71)
-val Pink40 = Color(0xFF7D5260)
diff --git a/app/src/main/java/com/example/sunday/ui/theme/Theme.kt b/app/src/main/java/com/example/sunday/ui/theme/Theme.kt
deleted file mode 100644
index d7c3bb8..0000000
--- a/app/src/main/java/com/example/sunday/ui/theme/Theme.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.example.sunday.ui.theme
-
-import android.app.Activity
-import android.os.Build
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.darkColorScheme
-import androidx.compose.material3.dynamicDarkColorScheme
-import androidx.compose.material3.dynamicLightColorScheme
-import androidx.compose.material3.lightColorScheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
-import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalView
-import androidx.core.view.WindowCompat
-
-private val DarkColorScheme = darkColorScheme(
- primary = Purple80,
- secondary = PurpleGrey80,
- tertiary = Pink80
-)
-
-private val LightColorScheme = lightColorScheme(
- primary = Purple40,
- secondary = PurpleGrey40,
- tertiary = Pink40
-
- /* Other default colors to override
- background = Color(0xFFFFFBFE),
- surface = Color(0xFFFFFBFE),
- onPrimary = Color.White,
- onSecondary = Color.White,
- onTertiary = Color.White,
- onBackground = Color(0xFF1C1B1F),
- onSurface = Color(0xFF1C1B1F),
- */
-)
-
-@Composable
-fun SundayTheme(
- darkTheme: Boolean = isSystemInDarkTheme(),
- // Dynamic color is available on Android 12+
- dynamicColor: Boolean = true,
- content: @Composable () -> Unit
-) {
- val colorScheme = when {
- dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
- val context = LocalContext.current
- if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
- }
-
- darkTheme -> DarkColorScheme
- else -> LightColorScheme
- }
- val view = LocalView.current
- if (!view.isInEditMode) {
- SideEffect {
- val window = (view.context as Activity).window
- window.statusBarColor = colorScheme.primary.toArgb()
- WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
- }
- }
-
- MaterialTheme(
- colorScheme = colorScheme,
- typography = Typography,
- content = content
- )
-}
diff --git a/app/src/main/java/com/example/sunday/ui/theme/Typography.kt b/app/src/main/java/com/example/sunday/ui/theme/Typography.kt
deleted file mode 100644
index e007b05..0000000
--- a/app/src/main/java/com/example/sunday/ui/theme/Typography.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.example.sunday.ui.theme
-
-import androidx.compose.material3.Typography
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.sp
-
-// Set of Material typography styles to start with
-val Typography = Typography(
- bodyLarge = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 16.sp,
- lineHeight = 24.sp,
- letterSpacing = 0.5.sp
- )
- /* Other default text styles to override
- titleLarge = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 22.sp,
- lineHeight = 28.sp,
- letterSpacing = 0.sp
- ),
- labelSmall = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Medium,
- fontSize = 11.sp,
- lineHeight = 16.sp,
- letterSpacing = 0.5.sp
- )
- */
-)
diff --git a/app/src/main/java/com/example/sundayandroidapp/MainActivity.kt b/app/src/main/java/com/example/sundayandroidapp/MainActivity.kt
deleted file mode 100644
index b64e0cd..0000000
--- a/app/src/main/java/com/example/sundayandroidapp/MainActivity.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-package com.example.sundayandroidapp
-
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.tooling.preview.Preview
-import com.example.sundayandroidapp.ui.theme.SundayAndroidAppTheme
-import com.example.sundayandroidapp.screens.HomeScreen
-import com.example.sundayandroidapp.util.LocalizationManager
-import androidx.compose.ui.platform.LocalContext
-import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen // Nueva importacin
-import java.util.Locale
-
-class MainActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- val splashScreen = installSplashScreen() // Activa el splash screen
- super.onCreate(savedInstanceState)
- setContent {
- val context = LocalContext.current
- var language by remember { mutableStateOf(Locale.getDefault().language) }
-
- SundayAndroidAppTheme {
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = MaterialTheme.colorScheme.background
- ) {
- HomeScreen(
- currentLanguage = language,
- onLanguageChange = { newLang ->
- language = newLang
- LocalizationManager.setLocale(context, newLang)
- recreate() // Recreate activity to apply language change
- }
- )
- }
- }
- }
- }
-}
-
-@Preview(showBackground = true)
-@Composable
-fun DefaultPreview() {
- SundayAndroidAppTheme {
- HomeScreen(
- currentLanguage = Locale.getDefault().language,
- onLanguageChange = { /* Do nothing for preview */ }
- )
- }
-}
diff --git a/app/src/main/java/com/example/sundayandroidapp/api/OpenMeteoApiService.kt b/app/src/main/java/com/example/sundayandroidapp/api/OpenMeteoApiService.kt
deleted file mode 100644
index d45f840..0000000
--- a/app/src/main/java/com/example/sundayandroidapp/api/OpenMeteoApiService.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.example.sundayandroidapp.api
-
-import retrofit2.http.GET
-import retrofit2.http.Query
-
-interface OpenMeteoApiService {
- @GET("v1/forecast?current=uv_index,cloud_cover&daily=uv_index_max,sunrise,sunset&timezone=auto")
- suspend fun getSunData(
- @Query("latitude") latitude: Double,
- @Query("longitude") longitude: Double
- ): OpenMeteoResponse
-}
diff --git a/app/src/main/java/com/example/sundayandroidapp/api/OpenMeteoResponse.kt b/app/src/main/java/com/example/sundayandroidapp/api/OpenMeteoResponse.kt
deleted file mode 100644
index 863fcb7..0000000
--- a/app/src/main/java/com/example/sundayandroidapp/api/OpenMeteoResponse.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-package com.example.sundayandroidapp.api
-
-import com.google.gson.annotations.SerializedName
-
-data class OpenMeteoResponse(
- val latitude: Double,
- val longitude: Double,
- @SerializedName("generationtime_ms")
- val generationTimeMs: Double,
- @SerializedName("utc_offset_seconds")
- val utcOffsetSeconds: Int,
- val timezone: String,
- @SerializedName("timezone_abbreviation")
- val timezoneAbbreviation: String,
- val elevation: Double,
- @SerializedName("current_units")
- val currentUnits: CurrentUnits,
- val current: CurrentData,
- @SerializedName("daily_units")
- val dailyUnits: DailyUnits,
- val daily: DailyData
-)
-
-data class CurrentUnits(
- val time: String,
- val interval: String,
- @SerializedName("uv_index")
- val uvIndex: String,
- @SerializedName("cloud_cover")
- val cloudCover: String
-)
-
-data class CurrentData(
- val time: String,
- val interval: Int,
- @SerializedName("uv_index")
- val uvIndex: Double,
- @SerializedName("cloud_cover")
- val cloudCover: Int
-)
-
-data class DailyUnits(
- val time: String,
- @SerializedName("uv_index_max")
- val uvIndexMax: String,
- val sunrise: String,
- val sunset: String
-)
-
-data class DailyData(
- val time: List,
- @SerializedName("uv_index_max")
- val uvIndexMax: List,
- val sunrise: List,
- val sunset: List
-)
diff --git a/app/src/main/java/com/example/sundayandroidapp/data/AppModels.kt b/app/src/main/java/com/example/sundayandroidapp/data/AppModels.kt
deleted file mode 100644
index 545f4ea..0000000
--- a/app/src/main/java/com/example/sundayandroidapp/data/AppModels.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.example.sundayandroidapp.data
-
-enum class SkinType(val type: Int, val description: String) {
- TYPE_1(1, "Very fair, Always burns, never tans"),
- TYPE_2(2, "Fair, Usually burns, tans minimally"),
- TYPE_3(3, "Light, Sometimes burns, tans uniformly"),
- TYPE_4(4, "Medium, Burns minimally, tans well"),
- TYPE_5(5, "Dark, Rarely burns, tans profusely"),
- TYPE_6(6, "Very dark, Never burns, deeply pigmented");
-
- companion object {
- fun fromType(type: Int): SkinType? = values().find { it.type == type }
- }
-}
-
-enum class ClothingLevel(val level: String) {
- NUDE("Nude!"),
- MINIMAL("Minimal (swimwear)"),
- LIGHT("Light (shorts & t-shirt)"),
- MODERATE("Moderate (long sleeves)"),
- HEAVY("Heavy (fully covered)");
-
- companion object {
- fun fromLevel(level: String): ClothingLevel? = values().find { it.level == level }
- }
-}
diff --git a/app/src/main/java/com/example/sundayandroidapp/location/LocationService.kt b/app/src/main/java/com/example/sundayandroidapp/location/LocationService.kt
deleted file mode 100644
index fefe664..0000000
--- a/app/src/main/java/com/example/sundayandroidapp/location/LocationService.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.example.sundayandroidapp.location
-
-import android.Manifest
-import android.content.Context
-import android.content.pm.PackageManager
-import android.location.Location
-import androidx.core.content.ContextCompat
-import com.google.android.gms.location.LocationServices
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-
-class LocationService(private val context: Context) {
-
- private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
-
- suspend fun getLastKnownLocation(): Location? {
- val hasFineLocationPermission = ContextCompat.checkSelfPermission(
- context,
- Manifest.permission.ACCESS_FINE_LOCATION
- ) == PackageManager.PERMISSION_GRANTED
-
- val hasCoarseLocationPermission = ContextCompat.checkSelfPermission(
- context,
- Manifest.permission.ACCESS_COARSE_LOCATION
- ) == PackageManager.PERMISSION_GRANTED
-
- if (!hasFineLocationPermission || !hasCoarseLocationPermission) {
- // If permissions are not granted, return a default location or null
- // For now, returning a default location (Santiago, Chile)
- val defaultLocation = Location("defaultProvider").apply {
- latitude = -33.4489 // Santiago latitude
- longitude = -70.6693 // Santiago longitude
- }
- return defaultLocation
- }
-
- return suspendCancellableCoroutine { continuation ->
- fusedLocationClient.lastLocation.addOnCompleteListener {
- if (it.isSuccessful) {
- continuation.resume(it.result)
- } else {
- continuation.resumeWithException(it.exception ?: Exception("Unknown location error"))
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/com/example/sundayandroidapp/logic/SunExposureCalculator.kt b/app/src/main/java/com/example/sundayandroidapp/logic/SunExposureCalculator.kt
deleted file mode 100644
index 7f21767..0000000
--- a/app/src/main/java/com/example/sundayandroidapp/logic/SunExposureCalculator.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-package com.example.sundayandroidapp.logic
-
-import com.example.sundayandroidapp.data.ClothingLevel
-import com.example.sundayandroidapp.data.SkinType
-import kotlin.math.pow
-
-object SunExposureCalculator {
-
- // Base UV sensitivity for different skin types (Fitzpatrick) - higher value means less sensitive
- private val skinTypeSensitivity = mapOf(
- SkinType.TYPE_1 to 0.7f,
- SkinType.TYPE_2 to 1.0f,
- SkinType.TYPE_3 to 1.5f,
- SkinType.TYPE_4 to 2.0f,
- SkinType.TYPE_5 to 2.5f,
- SkinType.TYPE_6 to 3.0f
- )
-
- // Clothing protection factors (0.0 = no protection, 1.0 = full protection)
- private val clothingProtection = mapOf(
- ClothingLevel.NUDE to 0.0f,
- ClothingLevel.MINIMAL to 0.1f,
- ClothingLevel.LIGHT to 0.4f,
- ClothingLevel.MODERATE to 0.7f,
- ClothingLevel.HEAVY to 0.9f
- )
-
- // Vitamin D conversion factor (IU per J/cm^2, this is a simplified placeholder)
- private const val VITAMIN_D_CONVERSION_FACTOR = 1000f // IU per MED (Minimal Erythema Dose)
-
- /**
- * Calculates the burn limit time in minutes.
- * @param uvIndex Current UV index.
- * @param skinType User's skin type.
- * @param clothingLevel User's clothing level.
- * @param cloudCoverPercentage Cloud cover in percentage (0-100).
- * @param altitudeMeters Altitude in meters.
- * @return Burn limit time in minutes.
- */
- fun calculateBurnLimitTime(
- uvIndex: Float,
- skinType: SkinType,
- clothingLevel: ClothingLevel,
- cloudCoverPercentage: Int,
- altitudeMeters: Int
- ): Int {
- if (uvIndex <= 0) return 0
-
- val adjustedUV = adjustUVForEnvironment(uvIndex, cloudCoverPercentage, altitudeMeters)
- val effectiveUV = adjustedUV * (1 - (clothingProtection[clothingLevel] ?: 0.0f))
-
- // MED (Minimal Erythema Dose) for Skin Type 2 is approx 200 J/m^2 = 20 J/cm^2
- // Using a simplified model: MED = UV Index * Exposure Time (minutes)
- // A typical MED for Type 2 skin is reached in ~20 minutes at UV 5.
- // We need to adjust this based on the skin type sensitivity.
-
- val baseMEDTime = 200f // Reference MED time for a base UV
- val skinFactor = skinTypeSensitivity[skinType] ?: 1.0f
-
- // Simplified calculation: Time = (BaseMEDTime * SkinFactor) / EffectiveUV
- val burnLimit = (baseMEDTime * skinFactor) / effectiveUV
-
- return burnLimit.toInt()
- }
-
- /**
- * Calculates the vitamin D production rate in IU/minute.
- * @param uvIndex Current UV index.
- * @param skinType User's skin type.
- * @param clothingLevel User's clothing level.
- * @param cloudCoverPercentage Cloud cover in percentage (0-100).
- * @param altitudeMeters Altitude in meters.
- * @return Vitamin D production rate in IU/minute.
- */
- fun calculateVitaminDProductionRate(
- uvIndex: Float,
- skinType: SkinType,
- clothingLevel: ClothingLevel,
- cloudCoverPercentage: Int,
- altitudeMeters: Int
- ): Int {
- if (uvIndex <= 0) return 0
-
- val adjustedUV = adjustUVForEnvironment(uvIndex, cloudCoverPercentage, altitudeMeters)
- val effectiveUV = adjustedUV * (1 - (clothingProtection[clothingLevel] ?: 0.0f))
-
- // Simplified vitamin D production: proportional to effective UV and skin type sensitivity
- // Higher skin type sensitivity means less light reaches, so need to adjust conversion
- val skinFactorInverse = 1.0f / (skinTypeSensitivity[skinType] ?: 1.0f)
-
- // This is a highly simplified model. Real vitamin D synthesis is complex.
- // Factors like time of day, angle of sun, exposed skin area, etc., are also crucial.
- val rate = (effectiveUV * VITAMIN_D_CONVERSION_FACTOR * skinFactorInverse) / 100
- return rate.toInt()
- }
-
- /**
- * Adjusts UV index based on cloud cover and altitude.
- * @param uvIndex Base UV index.
- * @param cloudCoverPercentage Cloud cover in percentage (0-100).
- * @param altitudeMeters Altitude in meters.
- * @return Adjusted UV index.
- */
- private fun adjustUVForEnvironment(
- uvIndex: Float,
- cloudCoverPercentage: Int,
- altitudeMeters: Int
- ): Float {
- // Cloud cover reduction: Simplified model (e.g., 0% clouds = no reduction, 100% clouds = significant reduction)
- val cloudFactor = 1.0f - (cloudCoverPercentage / 100.0f).coerceIn(0.0f, 0.9f)
- var adjustedUV = uvIndex * cloudFactor
-
- // Altitude adjustment: +6% UV for every 1000m increase (based on common approximations)
- val altitudeAdjustmentFactor = 1.0f + (altitudeMeters / 1000.0f) * 0.06f
- adjustedUV *= altitudeAdjustmentFactor
-
- return adjustedUV
- }
-}
diff --git a/app/src/main/java/com/example/sundayandroidapp/screens/HomeScreen.kt b/app/src/main/java/com/example/sundayandroidapp/screens/HomeScreen.kt
deleted file mode 100644
index 0c0b8ad..0000000
--- a/app/src/main/java/com/example/sundayandroidapp/screens/HomeScreen.kt
+++ /dev/null
@@ -1,401 +0,0 @@
-package com.example.sundayandroidapp.screens
-
-import android.Manifest
-import android.content.pm.PackageManager
-import android.location.Location
-import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.core.content.ContextCompat
-import com.example.sundayandroidapp.R
-import com.example.sundayandroidapp.api.OpenMeteoApiService
-import com.example.sundayandroidapp.data.ClothingLevel
-import com.example.sundayandroidapp.data.SkinType
-import com.example.sundayandroidapp.location.LocationService
-import com.example.sundayandroidapp.logic.SunExposureCalculator
-import com.example.sundayandroidapp.ui.theme.SundayAndroidAppTheme
-import com.example.sundayandroidapp.ui.theme.BlueGradient
-import kotlinx.coroutines.delay
-import retrofit2.Retrofit
-import retrofit2.converter.gson.GsonConverterFactory
-import java.text.SimpleDateFormat
-import java.util.Date
-import java.util.Locale
-
-@Composable
-fun HomeScreen(currentLanguage: String, onLanguageChange: (String) -> Unit) {
- val context = LocalContext.current
-
- var selectedSkinType by remember { mutableStateOf(SkinType.TYPE_3) }
- var selectedClothingLevel by remember { mutableStateOf(ClothingLevel.LIGHT) }
- var isTracking by remember { mutableStateOf(false) }
- var sessionTotal by remember { mutableStateOf(0.0f) }
- var todayTotal by remember { mutableStateOf(0.0f) }
-
- var showSkinTypeDialog by remember { mutableStateOf(false) }
- var showClothingLevelDialog by remember { mutableStateOf(false) }
-
- var locationPermissionsGranted by remember { mutableStateOf(false) }
- var currentUvIndex by remember { mutableStateOf(0.0f) }
- var maxUvIndex by remember { mutableStateOf(0.0f) }
- var maxUvIndexTime by remember { mutableStateOf("") }
- var sunriseTime by remember { mutableStateOf("") }
- var sunsetTime by remember { mutableStateOf("") }
- var cloudCoverPercentage by remember { mutableStateOf(0) }
- var altitudeMeters by remember { mutableStateOf(0) }
- var userLocation by remember { mutableStateOf(null) }
-
- // Retrofit and LocationService instances
- val openMeteoApiService = remember { Retrofit.Builder()
- .baseUrl("https://api.open-meteo.com/")
- .addConverterFactory(GsonConverterFactory.create())
- .build()
- .create(OpenMeteoApiService::class.java)
- }
- val locationService = remember { LocationService(context) }
-
- // Launcher para solicitar permisos de ubicacin
- val locationPermissionLauncher = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.RequestMultiplePermissions()
- ) { permissions ->
- locationPermissionsGranted = permissions.entries.all { it.value }
- // Si los permisos fueron concedidos, intenta obtener la ubicacin de nuevo
- if (locationPermissionsGranted) {
- // Re-trigger LaunchedEffect para obtener ubicacin y datos de API
- // Esto se manejar implcitamente por el LaunchedEffect(locationPermissionsGranted)
- }
- }
-
- // Solicitar permisos al iniciar la pantalla
- LaunchedEffect(Unit) {
- val hasFineLocationPermission = ContextCompat.checkSelfPermission(
- context,
- Manifest.permission.ACCESS_FINE_LOCATION
- ) == PackageManager.PERMISSION_GRANTED
-
- val hasCoarseLocationPermission = ContextCompat.checkSelfPermission(
- context,
- Manifest.permission.ACCESS_COARSE_LOCATION
- ) == PackageManager.PERMISSION_GRANTED
-
- if (hasFineLocationPermission && hasCoarseLocationPermission) {
- locationPermissionsGranted = true
- } else {
- locationPermissionLauncher.launch(arrayOf(
- Manifest.permission.ACCESS_FINE_LOCATION,
- Manifest.permission.ACCESS_COARSE_LOCATION
- ))
- }
- }
-
- // Fetch data from Open-Meteo API when permissions are granted or location changes
- LaunchedEffect(locationPermissionsGranted, userLocation) {
- if (locationPermissionsGranted) {
- val location = userLocation ?: locationService.getLastKnownLocation()
- userLocation = location // Update userLocation state
-
- location?.let { loc ->
- try {
- val response = openMeteoApiService.getSunData(loc.latitude, loc.longitude)
- currentUvIndex = response.current.uvIndex.toFloat()
- cloudCoverPercentage = response.current.cloudCover
- altitudeMeters = response.elevation.toInt()
-
- if (response.daily.uvIndexMax.isNotEmpty()) {
- maxUvIndex = response.daily.uvIndexMax[0].toFloat()
- }
- if (response.daily.sunrise.isNotEmpty()) {
- sunriseTime = formatTime(response.daily.sunrise[0])
- }
- if (response.daily.sunset.isNotEmpty()) {
- sunsetTime = formatTime(response.daily.sunset[0])
- }
- } catch (e: Exception) {
- // Handle API error
- e.printStackTrace()
- // TODO: Show error message to user
- }
- }
- }
- }
-
- // Calculate values first
- val burnLimit = SunExposureCalculator.calculateBurnLimitTime(currentUvIndex, selectedSkinType, selectedClothingLevel, cloudCoverPercentage, altitudeMeters)
- val vitaminDRateCalculated = SunExposureCalculator.calculateVitaminDProductionRate(currentUvIndex, selectedSkinType, selectedClothingLevel, cloudCoverPercentage, altitudeMeters)
-
- // Vitamin D Tracking Logic
- LaunchedEffect(isTracking, vitaminDRateCalculated) {
- if (isTracking) {
- while (isTracking) {
- // Update every second (vitaminDRateCalculated is per minute)
- sessionTotal += (vitaminDRateCalculated / 60.0f)
- todayTotal += (vitaminDRateCalculated / 60.0f)
- delay(1000L)
- }
- }
- }
-
- Surface(
- modifier = Modifier
- .fillMaxSize()
- .background(BlueGradient),
- color = Color.Transparent
- ) {
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(horizontal = 16.dp)
- .verticalScroll(rememberScrollState()), // Make content scrollable
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- // Title and Language Selector
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 32.dp),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = "SUN DAY",
- fontSize = 40.sp,
- fontWeight = FontWeight.Black,
- color = Color.White
- )
- Row(
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = "EN",
- color = if (currentLanguage == "en") Color.Yellow else Color.White,
- fontWeight = FontWeight.Bold,
- fontSize = 14.sp,
- modifier = Modifier.clickable { onLanguageChange("en") }
- )
- Text(
- text = "ES",
- color = if (currentLanguage == "es") Color.Yellow else Color.White,
- fontWeight = FontWeight.Bold,
- fontSize = 14.sp,
- modifier = Modifier.clickable { onLanguageChange("es") }
- )
- }
- }
-
- Spacer(modifier = Modifier.height(32.dp))
-
- // Main UV Index Card
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 8.dp),
- shape = RoundedCornerShape(16.dp),
- colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.3f))
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(24.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = stringResource(R.string.uv_index_label),
- fontSize = 24.sp,
- fontWeight = FontWeight.Bold,
- color = Color.White
- )
- Text(
- text = "%.1f".format(currentUvIndex),
- fontSize = 80.sp,
- fontWeight = FontWeight.ExtraBold,
- color = Color.White
- )
- Text(
- text = stringResource(R.string.burn_limit_template, burnLimit),
- fontSize = 20.sp,
- color = Color.White
- )
- }
- }
-
- // Details Card (Sunrise/Sunset, Max UV, Clouds, Altitude)
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 8.dp),
- shape = RoundedCornerShape(16.dp),
- colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.3f))
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(24.dp)
- ) {
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
- Text(text = stringResource(R.string.sunrise_template, sunriseTime), color = Color.White)
- Text(text = stringResource(R.string.sunset_template, sunsetTime), color = Color.White)
- }
- Spacer(modifier = Modifier.height(8.dp))
- Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
- Text(text = stringResource(R.string.max_uv_template, maxUvIndex, maxUvIndexTime), color = Color.White)
- Text(text = stringResource(R.string.clouds_altitude_template, cloudCoverPercentage, altitudeMeters, 6), color = Color.White)
- }
- // TODO: Investigate "Retro" label and add it here if applicable
- }
- }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- // Session Tracking Card and Start/Stop Button
- SessionTrackingCard(
- rate = vitaminDRateCalculated,
- sessionTotal = sessionTotal,
- todayTotal = todayTotal,
- isTracking = isTracking,
- onToggleTracking = {
- isTracking = !isTracking
- if (!isTracking) {
- sessionTotal = 0.0f // Reset session total when stopping
- }
- }
- )
-
- Spacer(modifier = Modifier.height(16.dp))
-
- // Clothing Level Selector
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 8.dp)
- .clickable { showClothingLevelDialog = true },
- shape = RoundedCornerShape(16.dp),
- colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.3f))
- ) {
- Column(modifier = Modifier
- .fillMaxWidth()
- .padding(24.dp)
- ) {
- Text(text = stringResource(R.string.select_clothing_level), color = Color.White, fontSize = 16.sp, fontWeight = FontWeight.Bold)
- Text(text = selectedClothingLevel.level, color = Color.White, fontSize = 20.sp)
- }
- }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- // Skin Type Selector
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 8.dp)
- .clickable { showSkinTypeDialog = true },
- shape = RoundedCornerShape(16.dp),
- colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.3f))
- ) {
- Column(modifier = Modifier
- .fillMaxWidth()
- .padding(24.dp)
- ) {
- Text(text = stringResource(R.string.select_skin_type), color = Color.White, fontSize = 16.sp, fontWeight = FontWeight.Bold)
- Text(text = stringResource(R.string.skin_type_template, selectedSkinType.type), color = Color.White, fontSize = 20.sp)
- Text(text = selectedSkinType.description, color = Color.White, fontSize = 14.sp)
- }
- }
- }
-
- // Skin Type Dialog
- if (showSkinTypeDialog) {
- OptionsSelectorDialog(
- title = stringResource(R.string.select_skin_type),
- options = SkinType.values().toList(),
- selectedOption = selectedSkinType,
- onOptionSelected = { skinType -> selectedSkinType = skinType },
- onDismissRequest = { showSkinTypeDialog = false },
- optionText = { skinType ->
- stringResource(R.string.skin_type_template, skinType.type) + ": " + when (skinType) {
- SkinType.TYPE_1 -> stringResource(R.string.skin_type_1_desc)
- SkinType.TYPE_2 -> stringResource(R.string.skin_type_2_desc)
- SkinType.TYPE_3 -> stringResource(R.string.skin_type_3_desc)
- SkinType.TYPE_4 -> stringResource(R.string.skin_type_4_desc)
- SkinType.TYPE_5 -> stringResource(R.string.skin_type_5_desc)
- SkinType.TYPE_6 -> stringResource(R.string.skin_type_6_desc)
- }
- }
- )
- }
-
- // Clothing Level Dialog
- if (showClothingLevelDialog) {
- OptionsSelectorDialog(
- title = stringResource(R.string.select_clothing_level),
- options = ClothingLevel.values().toList(),
- selectedOption = selectedClothingLevel,
- onOptionSelected = { clothingLevel -> selectedClothingLevel = clothingLevel },
- onDismissRequest = { showClothingLevelDialog = false },
- optionText = { clothingLevel ->
- when (clothingLevel) {
- ClothingLevel.NUDE -> stringResource(R.string.clothing_nude)
- ClothingLevel.MINIMAL -> stringResource(R.string.clothing_minimal)
- ClothingLevel.LIGHT -> stringResource(R.string.clothing_light)
- ClothingLevel.MODERATE -> stringResource(R.string.clothing_moderate)
- ClothingLevel.HEAVY -> stringResource(R.string.clothing_heavy)
- }
- }
- )
- }
- }
-}
-
-// Helper function to format time strings from API response
-private fun formatTime(timeString: String): String {
- val parser = SimpleDateFormat("yyyy-MM-dd'T'HH:mm", Locale.getDefault())
- val formatter = SimpleDateFormat("hh:mm a", Locale.getDefault())
- return try {
- formatter.format(parser.parse(timeString) ?: Date())
- } catch (e: Exception) {
- e.printStackTrace()
- "N/A"
- }
-}
-
-@Preview(showBackground = true)
-@Composable
-fun PreviewHomeScreen() {
- SundayAndroidAppTheme {
- HomeScreen(
- currentLanguage = Locale.getDefault().language,
- onLanguageChange = { /* Do nothing for preview */ }
- )
- }
-}
diff --git a/app/src/main/java/com/example/sundayandroidapp/screens/OptionsSelectorDialog.kt b/app/src/main/java/com/example/sundayandroidapp/screens/OptionsSelectorDialog.kt
deleted file mode 100644
index 45ab0ba..0000000
--- a/app/src/main/java/com/example/sundayandroidapp/screens/OptionsSelectorDialog.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-package com.example.sundayandroidapp.screens
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-
-@Composable
-fun