From 1b5d105400dd3ce3c7073bc0c4cf7c4ae98ae2ce Mon Sep 17 00:00:00 2001 From: alvocool Date: Mon, 9 Nov 2026 08:22:41 +0300 Subject: [PATCH] Show real photos for people instead of scene art Your avatar is your photo, picked from the device by tapping it on Home or from a new Profile photo row in Settings, shrunk to 256 px and kept on the phone (there is no upload endpoint yet). Group members show the photo the server sends in a new `photo` field, everywhere avatars appear. Without a photo, an initial on a neutral disc. Mock mode bundles free Unsplash portraits for the sample people. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw --- frontend/assets/images/PEOPLE-CREDITS.txt | 13 +++ frontend/assets/images/person_ade.jpg | Bin 0 -> 7048 bytes frontend/assets/images/person_alex.jpg | Bin 0 -> 9876 bytes frontend/assets/images/person_jordan.jpg | Bin 0 -> 7482 bytes frontend/assets/images/person_lena.jpg | Bin 0 -> 9712 bytes frontend/assets/images/person_mei.jpg | Bin 0 -> 10251 bytes frontend/assets/images/person_priya.jpg | Bin 0 -> 10182 bytes frontend/assets/images/person_sam.jpg | Bin 0 -> 10224 bytes frontend/assets/images/person_tom.jpg | Bin 0 -> 9903 bytes frontend/design.md | 1 + .../about/external/data/GroupMember.dart | 6 ++ frontend/lib/Grounded/designs/Checkbox.dart | 3 +- .../lib/Grounded/designs/DashedOutline.dart | 7 +- frontend/lib/Grounded/designs/DotDigits.dart | 9 +- .../lib/Grounded/designs/MemberAvatar.dart | 46 ++------- frontend/lib/Grounded/designs/Portrait.dart | 56 +++++++++++ .../lib/Grounded/designs/ProgressRing.dart | 4 +- .../Grounded/designs/input/CountStepper.dart | 4 +- .../Grounded/designs/input/InputFields.dart | 5 +- frontend/lib/Grounded/designs/text/Text.dart | 4 +- frontend/lib/Grounded/mock/MockComms.dart | 87 ++++++++++++------ frontend/lib/Grounded/mock/MockGroups.dart | 86 +++++++++++------ frontend/lib/Grounded/mock/MockMemory.dart | 8 +- frontend/lib/Grounded/mock/MockStore.dart | 24 ++--- .../Grounded/see/group/GroupChatState.dart | 24 ++++- .../Grounded/see/group/GroupDetailState.dart | 8 +- .../Grounded/see/group/GroupEditorState.dart | 2 +- .../lib/Grounded/see/group/GroupsState.dart | 8 +- frontend/lib/Grounded/see/home/HomeState.dart | 39 ++++---- frontend/lib/Grounded/see/home/ViewHome.dart | 12 ++- .../Grounded/see/settings/SettingsState.dart | 67 ++++++++++++-- .../Grounded/see/settings/ViewSettings.dart | 12 ++- frontend/lib/Grounded/utils/PhotoPicker.dart | 39 ++++++++ 33 files changed, 406 insertions(+), 168 deletions(-) create mode 100644 frontend/assets/images/PEOPLE-CREDITS.txt create mode 100644 frontend/assets/images/person_ade.jpg create mode 100644 frontend/assets/images/person_alex.jpg create mode 100644 frontend/assets/images/person_jordan.jpg create mode 100644 frontend/assets/images/person_lena.jpg create mode 100644 frontend/assets/images/person_mei.jpg create mode 100644 frontend/assets/images/person_priya.jpg create mode 100644 frontend/assets/images/person_sam.jpg create mode 100644 frontend/assets/images/person_tom.jpg create mode 100644 frontend/lib/Grounded/designs/Portrait.dart create mode 100644 frontend/lib/Grounded/utils/PhotoPicker.dart diff --git a/frontend/assets/images/PEOPLE-CREDITS.txt b/frontend/assets/images/PEOPLE-CREDITS.txt new file mode 100644 index 0000000..c58d268 --- /dev/null +++ b/frontend/assets/images/PEOPLE-CREDITS.txt @@ -0,0 +1,13 @@ +Sample portraits for mock mode (person_*.jpg), from Unsplash under the +Unsplash License (free to use, no attribution required). Source photo ids: + +person_alex.jpg photo-1507003211169-0a1dd7228f2d +person_sam.jpg photo-1500648767791-00dcc994a43e +person_priya.jpg photo-1494790108377-be9c29b29330 +person_jordan.jpg photo-1472099645785-5658abf4ff4e +person_mei.jpg photo-1544005313-94ddf0286df2 +person_tom.jpg photo-1506794778202-cad84cf45f1d +person_ade.jpg photo-1531123897727-8f129e1688ce +person_lena.jpg photo-1438761681033-6461ffad8d80 + +They stand in for real users' photos in the preview only. diff --git a/frontend/assets/images/person_ade.jpg b/frontend/assets/images/person_ade.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cbab9c1696b5a55475c8c649f2337f4045819f8c GIT binary patch literal 7048 zcmbt&cUV(P*Y6IYm(aUF04WJgK#%}R@4bYM)X+N#D2NEsq)L@00!j;@g3^2FARry2 zBO)N8BBJyg&bjaVp6`3^AK$%q&7PUxthHul&0c#wduA@?E>-~=ZFMbm00aU7n)n1< ztb<#%R8;H?jSbYbbk*nP%} zkF&S)C4P;^_0r7d;qAw^1s6 zOV0lb{bN1>W0VUj0OjL^f*N4Zei$@V59RER@y1u^0Cy+B%g+=Bh5l<6LA*i6019xy z=Ky^60i5t96mKF1|K|PvDxrWLp63j><6GW%*<}lj*LKJ23gUYk;5m8#3@`a>*#Aol z{$-4o z=%NRZ#s??irRD!Nnixz-L;@lpB?HJoc&q;*!N(aPh=2$nzW4x8g7HEcFn-;@=7E1= zxJ?0*ay^8DJBsx{rqYd|!gn%qt0i4#iC>z0%k9Nst$+5Tc3H`>gf-V@T63z5X)QkM zgT3*fRHtg@F2xwSUT00p%Cy~^P>J>!h6HI7%+`c;LbYj!i0u-Y1WR$#Si8_Q`q7=A zZEz0>8WR}DYfkfTYaRSt z&{c~iu)zUDM}UbV0MxZzde52E;_Hug?Yc?CR@5KISO)v+oZG&=Jn6UI<`E%t=T7Pa z>?I}&KZMMxba&)<56C^E_@ATuuP_9=f>2$q|Y)ZcC<1a?+sD$*fg11|x(LzjNRiZOU4Ydj{vu{(f5~J}_532mv z$K@yQNmk|}9IJZ2Je6eBCO0E?rq~=vLw!5Hx;i~F{FC(%nlIN)bPsO-nPyWa*idGAZ6*9z7-M(m89Jr>$FqXp{$s5# ziyxr6SKB0IH}qEXb^AjKb{4~Cm#kv3gFo7f&j=$?-gnSt)vXShWy1@-+Ml);CcJEC zbIfO4>zi4ZO+xZ!jqaxp`N(Qczx!~bUo5ZyKD${WW_IUeUkF^X)I6=vLqDdeUnR7m z>fVFao14@jtljCMx)N=?>!tGdKDCdmR((^#hngG>00I*b;t5OwCc2Eb%lIOsC8FaN zC+1NyqM?`YXW(^+ATds5lvH*MC|YBN)?8IFi5$nr8!0{xK?UI5|)R>vQP9jdeJgSXx^W`MvZhlcuFgCYpMEUFY~;->U8 z_0=*aO>my$#6yfmSKmO{*Huk(HPeuB6J!_D-fV4U<=s8bgMO z#w}p{VqCo*Iun21eZ%h6BqvNl16o$vL0j>zej|a#6EPazokm%6yPPg%?7Kc5+L+O( z5FK}6`p{iW1+e>sRy~pRsQ7;TC@Fhd4<|`OF>q@rJr?G`Ja7wp4-%u9JITVdTX`p3 zxJ-0K&FG2aDb$Re-JEjAQUG1S{Ay!^88!b zw5=`uS~KY%iz-Jgt68Ss1_Kz&6w@mrl_p#{Gqul4-Dj~yH4P%w zlxmh9658H<9+&yOZSpx7n;`(g@KqH|8$nRI67o?t5$ZG6yRTL^zRWxB@ngJ+AJy}z z3>o-TQYRl7I2N)CO!pdaEWYT++8~J9rv7{l4GwLK45l-0_%pUuv~JbF=~z*tB!p2n z`?O@LKSOj&z{vr{$^Szx@awdlT6XD&q2X5#UzUXBn!(t6nvw@RvZoCo`9sMNMX#p3 z+CZql_56XU+Ib6xjHbL`O9R#6t*Jj?>Jnp`S-0GuC|l{Q=6#h}T;B~9^{8OW=<((+ ztDj^(!45>LAcl{P=tk3^RwPg$qn)0*>O!5P4I;XxqlIAUtwjtcQyE?RksE=EcV%IkKruxR)=us18jg>dx&k=fh6eksvH|sGr#I0h%r(A{^ z<~XJqmhl}`9XyLPiWMJiWP)YfH!EkatomwuPqZ2SY6&YLKQZ|xx_t*rjLiN-uvSmk zP+nUgerMj03ud|(cQf_;7Me4D*?Rnw|hm}wbSP>}M zhp;x~k9$a!+nk#%m~k}(Ia`aMKizW0uO?i9agb7hb;5EAbGN1h@ms;hT1&m&>H~`` z$Aj8e02H*YvF-OC;m$kxKlWND}l zRz)irnVune36sgaB$7)rURV6l5gKjaLK;QZvm&VCb0b|Au<^mt(6ZFAbU_vMP~nZ| zXp$3n8>#oID3&!#z2hep@<4yy6AjJ~@rRQ=Bq*);G?EQktneSwo##1vwTE^?G8#(N zr;da^+8)=jW`Aan-f9=5SuM}|Nl``JoGzwHlsXOgJkX!wuEib{tuAR-CLf#+zBISu zx8O(B7LYd`Io-Fz25}@7n0FZL=0rF_4rC?5PGH|#9fdx-teBokw@bcb(EZ{(GoM60 zjP@b-iQoQ2fYcw_Ip*hdK6CIphAFu>TGy|EvE(y00(me+-RS;Lmg zmvD^3B}Z$v!X5oG8R^w6U0ic+v`p8jLoWb~6f0X2IU`Zb#QlooiPc5^DZ-)0$~1p)?8WUj4jE6bz8K7TGqu}Kf4qWL`=)* zII=a(E|d`ca|;)cxy=P|YJ>@mJ%v<83%+J!>Ai^v&JC8JKz!lL+9Rtjm4As`l3u1-BT? z;E5`~07@R5JB;2R2A#^URdJ4a-SX-_-^lMj<-d_N_%)Ej&z#n8nYt$<5?=o)v!f-a zy)-lV`wbd#dih%vZTw0QZNZ z6(Fzk8I#H@sR|m}zq%fj$zA}e8zWZ2B}p>|Oh!{}%|0D8ub0_9Su8dCkV9WKQ=Vk_ zXtI|>llhlYZ6&PIPemvOGndOU{B}ZZDg*_k5J*2SfhUvG4lKXWD->Fi!?}Y>4yWDw z&z$@C>kzH+9jx8o%h`@|gh}7~7|i?o(x@ft&-~N?Q}dG5jRj!ATsAX(!W1mj(=#pk z9r(Q(Hw_2?1Nbw}zmG;BfRmp_oGC zg~1IW;UDd5mlxBf%D*<>dnR~Z!cE%gRxj~PK;Q@4d@Yj!PpdU)5NVk};4Ptdj*d%Q zM(od@t2BkN4Lave>GoO)$5Xr{#sIAjt2BOWiP_wtf`n*N)adjBivN8A~t5#$o-rmI>Jd zn7OM$dFD~GCT-CL+@>{r1bc8N;{mXLv3RRTYus)m<~6tWqJ_vlA3q6QD$hA)!%-|O zOtEv{d~BW0=aj7DEXjs{?Y+lYmvazhx3E)YUklG}PC)TcK=;6I?O=0=DCtbc$sw8N z$CX{1ptao+@+`}DJ;lb^Pjya=unSr69YPO7@2uTcHF0U|>H?{aKx+OJ>39thtj69d z=PFUpDtVEuK;9aa=Nc+Yg_>G(B4#Iwq1};6gX2xC!U?D?%dk?>%t~1{gs=OG=qA0V zpgFsXO`WMjNgZAN4-yAoiU;zG-XtR>Y&G!ssJAWHocp*PLA!n}8@rik?zxpVdT_~+ zwn{Hi&~>L@S32z5t2f1sznT=KyguTO+ zl}WYxh5St(eWgGcXHnWnM&^?u;cmD10M4F*iOmjGzxKn0^JczqMB9`dYUGa0t!pia z|werO)QE zv>hB!_`&erp%52zUbp^-k!Q!+6`LMLov9qM>wH{05sU{191>Ad>>0@* z(<^m;C9k}#OrOsTFV{awq&-Oy4yKyp|28kZYqjDrs`SCays=crlV($-?;O-)#7>{a zPlOT^bnFa-6P+R(;TCw>Dg55!HxZ%*-06JqGc6(eWnrE&e7q#3={u;zTQ0J z7qld#-=1>Sz`GiY7@!4(U3Vhi8o-oDrNw=h+bxSxuZddq;|3VS13`o<8`i%FiA=P6 z^KJXKr6i=wBzhjGL?wWwEMQ?SQEhZ(AH-!s5{oqph4mc=zYL4A+4~2FPuSE5>Zq_N z(wq;Xp>ZEdXKzS6F6vmg)+1;_eYLSVY>}kKmN$$40w_0%i0YZ6<5BvgU7WTfPnVqE zX~jH7FD3ost}r#PEcw=Vy4fW0<@KFO63>l6)2`B|+A@)=8->11Iqsg~R|{9a##w}I z2|&F4VX`l^O22fA5Qsb$Eh~&zwYmBMz_BjWHhQ9JRu~>A#tv|k`9BwlY-Dxoa5ALY zI|;SXoe7-Bt&c>&TaG({Cc>wUL3?x=r{IrQUTbS-d$9kp>vuiIB?^IaYCV}AS9k1x zIWNpET6252-A&}RR>-@^d)K}l!K!R>7~Ti6Kf+u9sBVPYdjrj8p>r#48;0u`NJIe) z6kaoUt5xkq%_%*q+isweZrgnA>kY$K734K3zv>O|c060@kCD1P-EVS;?!|y#B-uQ! z&a)=)hCDwHDeQCikn<7r2OE(nzY^uR_1*Lg%I7}4-{^e%6>N46r|)TE=t$W2*s7Yu z>!#mx_tP&z-+ESlmh(*td|#Igtu|T?3DH?5|7ACwiz3@*7#A9cP@H4A?Dx&`Vfq9F zY%bc(UWswYK4y*BUEgf%aU17!FI(V1?d6U# ztYn{08B_IUE%^gzjVSR=i^fp3`w`*hm3f~&R)Gi+B3|4^d7rDUJD)*|OUePxsOT}f@@%C#KxPK_+fQ66FkDvg2UWJ+ibQaU+Q zcb7Y(aVznN_LG)(6e&7NY*4qxQj6F$Ivd26XBxUk>BJiMRF{mTheiC#tGCRL#VKOr z&zLr)OZ0JIvc#e%Gz1Wb)y{SKS7{{T`xXGi!C;7L>@9&rayAYfC(fwhsJi_|+%)c1 zm-wGfL%$fr^<11$O9squ zQWAWNu*?7%PCJ%9_+WKJJ-#LO;id=nn2F&ky>Q)_AE9yKXKaWsHa|>G-{8ns;kh_n zgA0y8YgeE;B=r6C&A69`pXXBydFx5&@y>z$7ZC>S_9uSJ6zTgEyZ)!%qhPx-jpsUoNaepHzK zBnGQBpW>ay8Y!3nyH~fGirwuZ#ZNa*2&cv=c_m~yYA zSqkOzT@UdZ=NxXz`zZg6yzA=qa2v~*X1YI>ZrCaVu`W@a$;7GIwYWaR)q$QgoKS+Q zC07wOYHo0rrK);EK-@kt@Omq=6W=65Vv4}X-hFM}h56kWk3CEuELb4Q?_FcOR#Emw zJWg|T%WHbav@YS?sf#Jsz$L*upBLv9QfX}<(`CM2SSQbn4AROt%RM2dodNCyQ4 z6u9C4-S_{#_tv`iy>;K7WY#Zx_MEfN%%057|A}RdwO;zxDr5L}>5j;|BmBU6i$;gQKrKiepiX2=w#1 z)1RQ2#LoSW!Dx5vgK`kX6nE_W7jyl~<}a4LV+RkU1Ip&kXDHQq-T~N%2Vq70XeFYRt0stni^FP?` zAMEECgz^&r6g<6xeO#Sg{NQZ%>~MZDF;TdxV<5uO&yQQz*52LL#{sV3>EUII3c5A(x&6!Hhu**D3`92dx9;!0f9pKU z0YLN|H8yE~>+JFYpfe5tD3<@$F_!`WK`a1tO#kC~$nN6B#m~=6oR>E+Fp$U9(VpjS zK>scOSA&0B{@3u2`|;e}@84qwS9El;^+))@?*`T06XEIa1NZf^wRePb|DQqpKkoPs zxBlS=m%gKuqmLsJbt_|3l({0EQPYidaP@QbM8aK>|J4lt54-)thdcZ$u2Dc-c>|DG z^8m1E5&&{I1)ve*01(S!R14_e^QMVq4BV|e6Z)OM;vU7Q_J7s?TLn%YbO z3i^g{dw-vRJ4UUEy9x?m0|WpGKnc(ROaL3e4F~`tfCL~5C;{q#4qynF0S^Fszyn#6PJo}lZ`2nV z1_%#C459?lgYJPiK>{FAkTggMqzTdonSpFT&LAYn9~1_P0X+s~fC@lmpgK@1s2el@ z8V9`xeFSZQc0u1kS6~Pj7fb@C1+##8!J=R}um;!=Yz1}(dxJy3vEUSN9=IIb0PX+} zfG5EV;C1jm_#6U2a3Ew51_(Dq6ruppftW*_AU==?ND?FmQVwZ?yo8KG<{;~k1IQ&B zIvOz=9E}@I0!_pw+6Q#_ap8PJUl!WJXt(*JU_e?ygIxgyia%+ z_{8`;_-gnL_z&?5@jLKm@s9|w2<{Qc5j-FWA;=+UBbX*QfMLN{USOlyP)(!gr zJ0~P2U9#6qM*;zshAq>3vdT(jd}8(q7VaG6)$9nHm{_ES2mz*&^8$IW4&yxg&WZ zc@z08`7a7;3RwzAiX@6=iUo=*N;stwr8{LhWhdnt6&e)>l^#_fRWa2Yssm~gY6)t4 z>Llvt)XOwr8V(wLnoybwnkkxdS~^-4S})ps+9BElIx;$0Iybs3y4Q5O^u+Yi^se+- z^nLVua8kG&90AXR55rFwXc$x({29s^-ZA`UWMwpBjAm?RTw}s!5@m8?%3>O1I$@?~ z)?yB0e#X3f59^-jJ(qjA_eSsiVqs-5VR^*T$?}brf>n(*g!LKgDjOb~44XGwIomut zI=d*lJ9`oP3Jx5D2b@9XtL;-7*tGN?4ekX z*rm9zxS#lQ@nZ>Y33rKliEomul8%y2(tI^1l?s6~Yzz6(Nc$ib;y^l!%m!lnRtSDKjcND?d{{ zQ4vxJRq0oSs%ogFtA0?URkK&CQ#(-?Q4d!i*1**;&?wOOqRFA@qxn(`tfj7%skN$o zPaCP-sRQV!>167x>9Xp2>%P)M*VEN2)cdN>uOFsAYCvpYZBTFU(@@?p)o{g#&B)(q z(3rs3(zwp}mx-cDhRM1quW7jHq#2c&t67&hhPknMwfVV)l0~+~wxy6|yyc=5t5uNI z_yej3hzGsa1lG3JZ8lIF6PtRQKejry<+kT`YIaZUPV5!zbM5yXWF4{`b{(Z0GaPrE zq?|IGzBx-dXFBh?$hhRV9JngD7Py|esk)W8{c_iFuW`Rcm>`-xFgjVnj1M62qmsj44WU)LaO)@$`^hwDV@>gs9gQ=fsK`9IrjuxyxV zRBY^O;%+KyCTo7&0<`$I?6umoEK|x?MWS=j^K`yPM6O0F0-!p z-Rj+gFC|{Kzv6jS)5FkH*h|`*`WpLn?CaaUkiPSNul|Dp=Yh>Z>%o;F)1mocz2SFn zG~SGjD31({%8mAoNsaZ4i;s6th)#4)3Qu-S2~D-X6?)tLPWWBNw8(VVjM&V}_mb~l z&&tdW%qh-|%&W~$E@&^jUo=`=`e6BCeaT_z+egI5)8&BW-z$-;(AA_h*jmmfnos4Q z**`b03$OQXC~i!BG5E5wX}`I@<-7G~JN7Hy*PI>tow{!V-+FeHcW3r2_O|!E_J1G5 z9}*ruJz_h0aV&d0bz*w5ed=?1`#t#w<&T=P`)7may62yMdi?zTE9rvrqV`hsa`ejN zYUg+0AB;bF*X-9XZ!~UJZ#`~rZ&!d=01SaZ&>&Ehpkts47A8ut@Ud}F1)mg;01x#d zB_bmtM7qgc%I`1R?4Bm`3%Rpl0Ryn8p!-=>68+$nnFJ&D|Bfy(&#~2@J#sO(N!IH z?@<<4$64n6NU1|mhjI$X6qTH*ySYH-d ztUad=q_^&`lE#)zH8^(GvR^nToM)`R7rMuc80@CeSDZW##j~j$&ocidfrf^lPc*L5 z)6DYYOPLtt)zH84RytOWD$Wu9D&|x})UZ(MUH77j;y`)Cb(i!l!`Mb*th1n^o^SU{ zLG!Rcizb1hc(~su&dgFQ5IjPW1q&ky|J5`e+sY@}ASnZ8OJv5I^)oJjnW9}4Sf{s~3)K9DyGqgIfB0bM!lG37eY)(kuXc984$6;JLh}L=Tw&uFDy1Yx> zJ3ge5GT?|+&X(NIHDaz%(z8u+K5>NMf3vT451Egy5ko{l)7M-Sy3M1}iYuSXrrzn` z#L|o_a5{UgD<~N@!T;)P@sBzCiCoraqGb+h>$r|Q^&{_7F?~*i+B#R3@%o=r;=tiT zx$o>Amih14tQNUriZ(^sD&=<8zIi!m(70VmC+&FW$MZo4n-@c6%gjverd6nr3g%HB z8Ft(NPXOVUWCztw8_`Mpgg0Xu%K= z8Wcb$B*GvjVPM20Wg=rntxinTx&=W2f5)uFx2D}y`~ng04Mj^6@VF%pme+Q+8dfN) zq=PpTby`EN?vu&gN19dMlr8NaEtqRUIWxFhgO{^+2ahFWP2#NHsy3iK)k9;?C^0)J zq0{c`Bexz>A|$SJoqyiZdDecUa~$2BujM1n(h$op5Si?%2M# z>guG?Zhg?XA{6BdU5Dq*2 z@-WJW4L3PCdiOWO=ovJO7Acf>!o8RZKhS+wF)>p@X6okFoL&(5Mo79sUXn84(G{f8 z(zTRZEjyci#&Y{;Z4=j@6_n={{22Q>W5P_PiAN^;`tf?+Schx^IUltl_@^t^{L!mo zVUO}s9YL4if-^EnZNH3LV3AAy$mYtbS(@7b6zEaj;`}B+y)VprG^@ovlaOygCAA*Y z%(7fo5;Xjjk4p8O(M*U(VscR8@=Yj#j-pz0DJyzpmho7Oz7IBY>w5Jk^H?F@NG9#4 zxe`_Kl%v6mpPO_C#Vdw87(Eas;JR;WL5k@F-wdto8CtFucHX~`OtRDszbX0hOS`!G zdp*+!Z0E(~CNcxMQ9j8|Z(RxQ=JVyWyn}hi@rioPgBSb#F%NYY8W=~!Y3OJ%Rv?q%l|M?t<6dlRK_bt#@T z?pRG}z`Ch0um76;h_s@qbE}8d?nrpSo!p#Z?MD-`O=45~k=k;&Z`lq1`A{W=p_A~b z2}7R!XePrLza8mU+&>yLk_>HF*gsQ9@UZQ@Uf1*Znyfx|8uq*2*u9pi^V}#R9>bIC z=2`v{yPj3ad3fpzcWl8k0fhO}=D4%>?$$M~+lWu3w?OHB2&wDtPQjOgPu<&-3txP! zzCs6nK54&ha+s!4*l_#lPDZc4ADfYjy)zblJbG~*L&|Yp-`TA~Y*)|eXld#qZ1M58 z7mlEKuldyr%0(n2;=F!?whmKEvxmKFIUB3}y)2*}eKU0haQHJmW`B3)%@uB;Khsgs zV|o6a56q&s6?pT7E^koHzi{@(#BwS?3s#pOkDQr36w4*xZA9`^KVQLrP}xMd+Ia2p z?c>{SEl!g9H!Xzm!_juqHOz?_wdbjXqgOCSHS>~eC~S-=xtPYxwSK+=(t7>z73CrRM_)rycUPXdL#aabNWFRa zs2*pXD+}B zC05$WF!gtqiRi>U_}tpGYG+4sD8RGh5#}uL3zaS8Xt91PzLuw>Xo_=Dp;ND+&1zc- z#{q8UGV$4-X4QJfxs!y})bi?w_Z*kT=gYGfo6XveY%asbVLGs}ylYKY=gJD)HTmTgto=z0>SC9Fg7pmg}PRXU10Y=z@eQC5}c_Q zOvTbX{ElT<>b8(FL%CvZ#NM#N@*NNzw;+x)aBdp zY@SttwcSYR_fPVcy^_TETx$28D-%r1Fqv0|zsS7K?RY>Fbd}SKL;|i-fxw}^{y#T+bQwTG8!p)@@?B)_Nc|ZGr9HLUMy_Dn z!GXcD;ScJ|DD1Oo=^2M`UAOy9dLP15`In|Rqe4vy@_InqlCuXyD14N$TOIcaNS9B!=S}PplHA z^kE!x%^DtAyP6{X>50Rt~$CuVQb!FBRw5eRt~+4I0FmxT*Ux@AKRY*>CIi@b&Y!r6X-^yOSr=$kPY!;{+c)O4|Y4zH7cU)kmKSh(gu{;a?`SZ*utf zz-e$g1uBBxDDy9p2%cy}StY%B)p}F(=U3z#?67umCtc$SGyY$Md;ZY$q>^QJ$*d>k z4V1)wke$RS4XFlMY*!ay#z+R{|Y*Py_@Sc&KKtD2h5V|ig?afZi&w#-;Dt@#CK z+t+VPz7k{t{o^n)>hBbZ+26#E`FplY{%8_TBSzSi1u0T=g~haJ#{|Y}8zZmRS!>Z9 zx=)_(F5Eog^eAJK%JYQu%6Opwv&6#X<)`h03I+(GA;{CZQP(9YBj z9kI$vUk`cEpMjq+OzIvotivr9YWppnwC}9c6Ag;CyQym9Tbd1Q^s?DFT$q*XSf*4- z<4lt4Vg@TPC*)FGQ#<&kB4Paj&2c0ZSzVs7vfNj=1Wh6mhpp;InwQ+}{nxK@+a5tv z1<{Q1rDdDup7IIgQBRwygVi4ytPoX0SZ83?wI4`YN4XH?GV79VExq6^C9Mb5y-JXh zho9J6Md_)NztX-`)_L=?dONxUV;M)%dW4;sCP}NlZ`#b}ea_^S;Ld8AvQonh@zlT`Q>LKAEB7pxuw^wB&51JWTsof+E@~ba}p4XI2#pS zKX8la(>S7}F5Ym78AR`%qkh%v?D4(e;=v%N+6eIItR;8r*re4VhtUnS7~e|G{4Fro zIxb|)Zxv<*^P`8Qzlt4T?Kx%cc+KL~#hSK4)SR?PoImvGzQka!)1cQIy$P>B^89@F zK)#RgYlyOC(rru{yu)QP!6vm2=!-TjYUj!N$CQ@nd+PE|LnBvOl2qiGbbkt%KF!z0 zg}Xf_$R?f-be0hv_R6Ct9u{Ia-F@!y8Ejz3zwS1l;A1ZaYuU&DkvtV8vJJlcsy`7G z`Gxw71&Vapf=S~uo()5jyRT(qFoG@SaiHN-nz%F&DG8b~*4UK%sIDm5VMJ82iriR! z1M|tEKQoOlf^C@t6sH#{nMJ4g24`gw*1(suo&REIORk`j3F_(n=jj`QL&+~>5=D_C zY65L_3~rm0@&RBG=(#lhhAMxriow#)+y$ekLp#ik&luhOCXXdKux^0^o*E8gYgY+K z6t~C8RW75anEFg$LWE?u=9GfXR|c`DwoK{#BQ%YcfG+NmJ|;<_eHqgYM0lH&QfthI z2gFBfvF%OHKM$%yuE@HPb&2o9BkIa35(^g_3gx3ky*n8|ZjGzarDc@K^$kxshjR8< znpS3b_zWj0u65k)T;6vV43%7Zex-OaOiJ8U-0$>b?>k(HCW_yZo=3mY1D>go!uylX zSINgzt?3Oyae>^{W1&70!)S+=y%7N#6QlG$5X7$-`eUKvN>|VlqMl}o8`Hf_Yhkc) z&L$gfzV}Nw#NNgD79e|l=sjELUY!jx`;pB5%SL71z2Th8H}=aaM{&&7$lUJuxoA>; z08cYf^&Q+#X8Dw-BalBKOWO- zxk^7idUOlu&UyQqI_}(vWVsCz^(iHxZ&Q1%_%b>?z|s`*w6KX5q3J}Y2`{ z^blpIaPo-vIU138)e%u%^a{%{Ow%A?!}$v@`tt46Rsdrk2TGzjNjvy@#u5WtqozDh>VXnYLAV`?AnoqFmZ?sxleSqL;a0Y6ra-+ zsPF}y8Q7a=iO&fnEE3H79mjXY zt~I)%>0qM$OX(bQZc;{)&Y!T(E<$--?Uw%hqJVGZ1B^Q_?8NY5A!P-<6p%&8eC1ESRrwrt6Jw5v;Aml{@$`KYj66n2 z#ez7S80nH|mTdK!2tcvr3voN zOu(pUWDCrRlH&W>2Ck>P`A>xp1wT)2A?$+|Gnx;ZY%kI(ZdPs{wiYI*oV?wc=^KW_ zVLVdh&eS^OHLZ=?9&6s1j6{VLK{K0^U$|W^48%?`Nw8d_F(n^Og?K216sbRN8H1nk zeU>kjWZ4;{bBdSw=9w~r#GG0Q68^+Too5kGiX&-v5Guv7XwYe?{a{0c`kihzaa>hE zb5N7E55uz9vGYX?Ob++5q~5G%-mt^KF5AyLO)maQK_-4_NkyU{@fJ`B3lKz0A>Q&4 z7L2FC4PkC|Stj6GC#p6b-9 zYeZDfb9@_u$KdDjhu{0?A@r!zP=?WFicmmYOFBB6^+}t3gd)9Owr&#GxXdsNQ%BZ| zm3@K&S2mbzxYtdROWD|}rghAb8bV-k#N{Z~E(27zP%rCKm}UiCYmCQnwrFk(Eu!x>l0Ee9?@eum4kqwf*W4=m5Ek7J{ObrU~p#AlSackph9>GKUu>K^R1rvIvnDx~V_a(e;Re(cL%dotar+W)X2;R1NhpJ%#1F>47t~Rl4DkFZ zXO_ZK`NxD$%7bOfU20F4N&zjr6GBK;>585*&y^nA{l=8auS?n_U&PTu2IX+n=BPzK z-F)cRbGfkb=}$?GbXaRwtJ=u%8u;i=WzUovs-$O-^iWn$j;QpUv~<+;iGH6;Y0PSQ z=(FrbW%{I(h0OhH5>jp%+BRux?u4#~*zDCQ=m~{RY15OTb*G{X%!M9Wxy5su;GRrX z+_rsqv{X(FuHJ4d74@26_jO#Xxc3Zc#TaGnXn2B`%&F|#CB#sTK_a@iHIR4dn4kP^)QRw^c&vU;P@Xz?e;S5i#2Wm z-}bs@n*tnNXDoOv&(2)ao@%P)GlW*?wcsR4N-!@ z7)(^8o@otmB{tjM0*o{Jmp@JCGMSdYC_HjxF-(DvrxH(Rs{F>$o5($#d@2j-xlfue znPH%V{FYKQ_~Mdkey4l9EL=#?abzi6?<9i2F}DVbhHj{Ra}C3(#h{G&(vv}ko)x}Y z;f~SSZQeslunMjllOC*;HSE2DR_~}KrhPO!+1TQ;?DDZeq!+pxVi!wzaQooB46E7v zEjvrZpcKlm(8Dcgkc;o}cKgx%XHF`Yn#7EeefPN74aabkoW)T((R0zGz18r+(k4B6FU;qig0SN$b!2|~wTj*E<1ku;l zLpwUI z7eQDj0BHILL1_1qFGu9xmJm7ZM_3>Volh3BsZ^{e1&m{2~G1k3FCF z0;tbzizXOZRzg8mRzy;iVE(`4zpebM)qfA?XZuHsJ+ptE8HjH9Z{6R0|JM1J0f77g z!8RFx>s<2zpe+Fa7~cP_{W zbDSG42p*{Wns}ue|cKgQ~&hZbwCIC(OZvf>a3Q&%r z0LYhN07^pwKpgK9YCwOTn-S6)IFCFV_RT;1p1_3qf0h5C0H+gf!NH#H=ySEExg{DC z8WeWUgqS!lU;r^d0iXazfDPaR_yG|>5|9HF0X0AyFaS&dOTZ4e3}65cz!wMt!hvf* zERYDK0vSLKPymzwfnxE(6ztTfr~EL*Oa! zGWaw200KZrAaoE8hzLX;q6sm9*hAbPL69g&3M3a&25E$JK>8tXA1Xh}elFiFAnUiTsG-h;oT)iJlWp5N#42BPo$Q$O}jlBo-NoOh=X@ zJCI|@4dgK~6)_*NGO;DG7jZ0c9&saaFY!F_9tjBvCy4@y8Hp!J3`riz1Cjxf6_Q`1 z)TDx>nxu}Tp`^D-t4O;@XG!YAp2)>ig8MsMly9G+Z>AG#HwAnsS<6nhg{j#fQ>Id7)BK4X9Dn z4lOmUG_5sl1Z^ShGumZ32pun-KAjI;I^9FMX}TkNR(f@M9DOo73($|9h(}P7h5*lbG9vZ8g>}Ls zgWlm_;n3p< z!E=YFpXZR5kJpCxCT|<>M?MBVeZDJvb$s*uWc;f9f&69slLBx7c>!;MVu3e;U_lu{ z55YpgF(I&!tdOTrvCz0MOjtqKPq<8YMub#ET_jATPGm)tPSi*=PP9$*s~ESKy;!DL zzu0ea8F3%+a`8C{Y6%00IEfC4?~;O&7|BA(NhvZZ9jR!kcBx%yA!(fSUFml+G&06A zH)XnIPG#j}gJc_Jx8%6xoaKt-X5>-wrt)d>0}3z&4TWfhPK93=WG{qVczEHPqKKlm zVy)swB|asbQn}KaGN-bOa;fsN3R=Zk<(|s2Du=3zYMJV)8kZVYtx|1MT|nJSyg z(?HXwW^gl8vqG~E<`U-D%=;~9ESxNAEq+*PSf*RfS@BziT6J4fSUXzRSRdMG+2C!K zZN+S_+YZ?=*?HQv+av6)?JMmM9JC#B95x(f91|UTt&1tbP62VMxw z3ET-X45|zU1z!qo4?z)vu%F@9;g2GyBK#tTBLyRqBR8+;T&cJU zy^6WoeU0Neox7KvAwy-O*gpH=;LUbYrSxiDSKEN8-fe@NxU`Ht`)dSZ~DK zSWD1Ns7oYI3`(52sd%#_37X`YG@2}xoS*zV1)DOMDxR8~dXk1o8@wfPEAQ6XZMWN_ z>9Xl}GvFD18Pl1nnbmkI{1yCKmT^{VHaa^cdq2l1=XI`hZpj_uJE3=$^NjM^^SSdg z@=ppp3Z@D*3Y&^pijs>Ci?PKMch&ATmavwjmK@#ly!WnDx3sm4w=DNQ{C?Q|59QY7 z{T1>RHI+=2sg5AsSkh$ zp$|SaJ2y{1G_uMPnSCc(vJ)^yby#swZeXsh}`(F$w z4|Kl1@cPN1{9xOV>`?2l%y7$y%t*_q>}czl+*tb?g*P4JO5@KbR3~1()q2}EsXsY1 zb#ZEZ+G_gUjN{DmJM6oUvp%!C@5A07&&ABc=2I3Z7jhR_7R#0dmYSAjm!GX@tqiYP ztj?`r);_NXub*t(_(1j{cawdy=A-1t&Mlp-H=i6nZF~;+e7v3bh3ZSuSN^Y$zo~s2 z-m%@;*bUk}`<}MPxL36=z2ALcda(Gz_s8*J%1_3h)xYF_^&iPg}?{`M-VO~5kZjT#3Y1^oR*A&jPRnRrlY1Jyg8WY>6tiq z(QIg5Q2_x_1z80{LmcS;JHS~tKt%*}0bLLfCjh1bL8w4yJ%l6#0D~cf+~KdR1`30~ z!3ZKG2p}dX&+E<;m_G#shQiFk$b#F6vDsNv$p*HNZ%k z2ezn)ku1zBlQVtE?ROtrvIjD*9v zgxjq(Dm?1rIpnrJCe;kV(NA7hp<^PGGDws|r%qnr8YlK|H@_O0`Zd+WYJ#ezE8B2w zV9Z8Z3#pe^^S)#hf0@5COU{9Q7=y*3pFg@XanLq8@?ACfq<{M^{<9xn@P}Dw@WrF; z=b;bKfpllUT`k22K9)4j*qY#k`?5|oDvE9X$s3kYiqYpI z@4PPM+$!u}u(Fvy9t{UwrkJ}{N<;?;H#J5zP9!^Q@Z|THQ0=xFyD;`Vtces!N!EGz zrQll9lH+uI;?bS*D>K$xBck8L%tPe#57}kaq?Qk_4Fc@(INYTi4m~RnaP7ESFOIamwar(o&j;7HpLa*gs80BerU-=-FJAY`8M+jN zadoVvm$%&(*hdt1vQOp2$*|pWoO+<$o1i>p5+%Zm59zudFHC)!bL0J6gWTh>Ws1fp zE#C^JDqC7Bte<$)5Bo4z2!1AU&;DKa)8dmUNO9%=yB>?#|EdKI)2*L3Y01-6}6^h{f zM1-gSfdNaqvTK~k`rj(5s>4Pmds32;CuszE@8*R+1^}W}OU5>LW1-^CFLUB*&!t)|77QcV z+dDzW5eX6o7_3gJrZTfqvr>yyQ30d)E!OLV;MO*l+6DItM$AmnYK=#y_g$4LdkNEY z)z!g?s#8+pTn)ujZg@LrS5>MUYh*bwG8*Jyaaf#|ww9J%axyacyU}9ZW2K`do2(b? zud)0CRyk9zZs?a8^FFdmMxF#;`cZW^@ZuS8(>>_!(%Wzfgn6RBtd6b?iU_Y&V9d?^ zkm^J+K6ST9l>XJhll)F_F|J?wgR!ixj=fM_a`{BCK6kF61c&J89saq{TP=>>a(qcx z7wY=k*#zMi;*KO9f9ydt_rG&WbM$jP3fX_ydAfe zRy2jnuPTzS4OL+qON}94c(X?E$y&8mf)9!+@!9UE6m3_oZJWU0v@P!oCAv*33;tmZVN;U$-XNzV7Ek?sBA(=$NBHd;lDqueex zS~+@~nP++n+urYp!@pLp+U~Hcoy;+tPZ4@E+MTd8cIb@5icAmTB|3Yl?8ryf-S*2r zlL!%pP=`&@2F!kWGnW<{+Lq6TiA-HbJWzT~JoA}`kNPtN6<`-PJ)odUpGdder=a?b;=h9U zNOAA?;|L~|tUdXon>X1V&496?HVe~dGuh78i=z^F9YG$A^#SCn+032AECpTjXHR6f z>oUJ1hOEqOmx*6YAKbJmzTcwrA#aGSYwA`}#BCmnr5fdrRK1`&{WZ~>gLl|Tnxd;4 zE+Q8-nS=y-iUROI`Y{Fr3_)C~W$7m4etxXsan%e*&AmKRb;enVnR%xpwCX)klLpf( zj@Ik=4su>`9hKr<-9W5W^g^pjf6}iH>50YiowJ3$zgn&~$m-vZwTa^xtiQcUd|xKm z*Vnh)!`H{Z^Wl(R8ZN&rT1wt5hD>BSpR9cPLef;hm+cJZaJt`KaTndWf-^?0YQAmF z=bUtYCa$0)^5xowN!14n`}w<@KUE3}21mKcs3GpbROzs`T*uf1PnV-ojq!bCo*z0}?oO?lL^dzbq_xkVoDhFwp}a!l zz*Xic5QwQ*8Vjj=j-}%87f*8^7Kt^VKbF0%h_4HlSzG)TTOt~kF<4a@;B8y5`)o}& zg=C6bB4uUPksNeqg@wXvqD9&3lCG(Vs5&xDD+PnNS}5^a*xU8qjMAanZ1E-gmDIj8 zB4Q^NgLwPgVwqtZ`pPYa4corV7+vR>fL~P;soRH+!E9&19qjjnub7{oR`+I)gz5eL zg~!ne8Ot>;8Q$-sYRbvPoTl!j>L+U;m-DoP`9CtG`TbsfTrJ(w*4myG$s8pxJR)^J zdU74FnDR`Pd2c7jDjt(GxWjE#>y4}Q*!?=(qiRpx`ym131Ov{1covn2-P>n?QOD!x zAtef`Gr+dj&YKw-hzobCvKVh-TKoXXOuQN66KdY6ZOl7Xdeiu!!li}27{QbYjCP&_ zPs?jiUe~qel4=$*dpmohBo%Z*)Q76MV*6f-ycow)YiQF?p+2Lv`@~wNwc_^xgtMxK zl1P~0oIL}_!r?8pg~q|}*>^(Cbpv3YbUo@k-F>yF8j%EFEWvNXx%&QeeN8V?N1 z4=Z7e?K<*!V~1Duh8y=tU zx`vv$xu(9B^7vf=-!*+GvOT3*h4Q|L{b}7-<-&nyuR9-vixOl^v+#o?cCc5OJQp6QRo!abzsNomu??N7wQSM5%Kg@;; zI_FrMI;P!!QsuZ3JYC+jc&d(Jlk!pqN7(Y_5yi}wgYs1mNIx?*h?L^uateOLV*aZ= zZ(4?2nKi?g%oL`u;=u4gmE*4xi!Sz!!&fa^U*zgIC~L?k3`%rAzPL`OYB{P62C?YY zrNzFx87q4%&|kGM`TQ^E}D^zNAV_ zKG2zMNxo>M+q&B+deF)yBu!=3*%GX z71yZ$3@c^KODl|wew7zp@!T)<(G}_UxmEIsiyUM5Zb~`2)-sL0Xn)HDqZO{1gPX<+?50-17x1Hk$@8xbU zI-(=VH6T5l`{0~NQK6d`RtlbuhxL((+C>jI4RD5HUh;4)2OjXoB7VW(=}VK-$1(-4;tS*l%3j+_^D}|l$`&?vYkrdnmKudrxY){M*EDcDB{vF&G-aoM=vZ?a&0Rf>K6g`{5-k7JkR z^jZ3u7L%^4aVBoDddHMbKaC(Yy>(>0()ALhUr+i*9}?n+I4rF_=6r({>3q{QT_4rD zjoIDY%EBpUeztk=#4x{%E^IxR+OfE6$yvW)9@GVj)s-(TScnfPLIu6R^q2}#%n<$* z@^Fo{(CS8Rb;s5RN>}FF^L!k+y!-IVblync`(JMAvwS9}O#=eBhwqu1*?eZ?zA7eJ zYTY|sFYQF_as9f_Qa9IJYDDMyVv2F>E5Bb11&3AfM_;otP;v=^2)BA#5r)2bP z+2n4r>_5vi%BTZ}52fi#Q~{dU6}ugt>bfl50Pzjo$U7Yc?s2pVtJ{;(C0>QfLD>1U z#UyU!=>W{{FSdL=HiVQwfQpbRgTOE-2=-U93{XL!lrU-zP6@QQq`C`@shN4;zn=eK z5J(j`t+MNhQ_#+9ud$ftCH+Y*(82wSjinF});S zqWOd|y*Q(pYvTvz{V0=^N(F^P1v3RkG=Jx5jEG+k@yY2T9|?WMho($Lip%aBC6fykBN+M{mflwN={tO)$ds;r*9@XD zJAH=m8P#tpC7Rc~>N!_R>Ej}eyjhb&w7)F9_Fb0Q?PDSl2W7H@<;7{I!)U_J`3XA* zLBKG`zi<5ClOZ%EHHWwa4W~MaOU>nvjT3%Gs{-4xzuH$)-tN2QikTgWn3M{z^}ViH zjwq_xPUmcBY&hlC=%T#m`n-ZGIL8MJ>KZF~)1aR-n@4$lAumRo!R4$pG6<`%o|Df|nNEU}jbORpVw2$ylH6(0jH%8foc%bY+`a z+jdSfm^g0^rUtcYc~L2TWwk{G6D^@CV!L#k(nOjXaVyEinrXHuJDWd9xjaJVx_rUb Xe8hV?FH8wG&MI8sg06KsI@Bx4|!pBoXPf;I0ii(09 ztq;=Zp8t`n&j9jH05HR)tW8V%kN*FP@DZNgz5oEyMp_Hl+W8=m9ED`p0AJ60{RxtZ zY+UXc483P>q=QH%yJv^Lne#83zgha8ZQa~$kv8`}d)m6$-t!=mWBvW?kPJ~ra*V%| zT>z42k<94o=jMdupGYQhv$OUA04UbI-q#M{h-6+QV|nZA${|?-0MM`;{>3)`Vqd$* zNIwBU&cid%+sVPvmzEX5M#~3>i_$9F1-RPz`f_PoBV4S#ZE58^+&rz_0|DUgcfP+0 zAh@qvT4W^)^TCCMx%jz})&C#)Zwvop_20vN+x~5FuJc!$fk;06qx$D03dpa zteez-bT)Ya&=w5mAA9E_H*^6y{}Y+hpUI5H?5DSHNuXT>wgd8|K|(;<<`Ia z;MBFVxAV4hM?T6B8D&oH4#?(qw{`M$@^Ghha{sSd`2X>;fBA5af5$Zv2+MB)B1>)n zZ-NMb9DV|zgqQ%t{5i4&^q+Q9M>hoSGtY=-=kK^jGP3-y^Z#-JryzfVeViO<@6~d; z`m_i?Z~uEn=EVI824DcV01-eAPy-AAE5HTt10sMpAPp!0s(==t510Ut00iI&xB=ck z01yI105L!UkP2i0`9LvH4%7gzfJUGV=mPqH55Ooe3CsaYz$&l>>;osj74Qq$k)eXH zL4+W35Dkb4!~x<5iGn0S3LtfmF31FA1#$qngZw}tpr@b&P&z0dR0662y#aNA`amB+ zQ=kRV8fX`E2D$-5z*t}+Fcp{?%mWq$%YfCu`d|yN1K0~31dakHgLA>9;5u+CxDPxE zo&&Ff_raGC0D=i2fzUy?AfgaCh!(^YVh{0#ghJvW*^pAmE67{O5M&0j3ORsWLs6iF zP+BM#R2-@dHGm?ZUeHizA~YXb4Q+)ELT8|B&=cq_3>!uXS{lwOo6lns05(M-|Y(IU~Z(Q47U(WcRM(0-xgp);Y2p=+Vr zq6ea3z$U+iS;8teh=73`llggD$d zDmb<{VK@aitvJ&-N4V&?Ot><*k8p!WUE&ks z^WkgZyWuC|*W!P~|4x8Pz)YY(U`r51P)0C7ut5kRWFV9yL=ZkDEGK+VxJ86Q#6qM( zo350~1M1{naB!#4rrXyc~k>b2h=3g($vn>nbh6XyEKF}k~B^U@(X>*fV4@^fR0=(lBZ; zhA_TlTx3FL5@m8^%3&H}`oYY?Y{dMGxsCZd3mJ774HWW5d zHW#)cwn=s{yAZo0djb0d2be>c!-=DaV~P{T3Fma@Eam*dg~cVq<} z<}21Hb}Y^%?jl|*{#}Aa!cL-GV&ftGL+ghn4_75=C9Nc1NUloJNm)ykO07#XNZU$R zO7F<9$vDf@$sEb@%6iMT$o`NMlY1i9D-V%Zl8=`kS0GR@P{>zUQKVONP<*L)q9mjg ztkkOvQ&v+>Q~shtrGikYQ8`f+QGKF1sD`Dcr@%IVI*&qZnSF5WBkN;)P%ys$)w#B)zr|m()7|y!7R&c%UsAj#{9Ddi^XG$kB=xG zxjuSliEC+X*<=N?GP0_*`faUcU21)4qheENbApgV^OO?x=tC4Gi8=9MyTZcP=yNmmv2erpz zk10&cG6pBcJLbzX@n^-c&{(I~@i>9FytvzV+xU?L-h}Lg--(FC zktDvP+@!l?`{eNy;gsj8D5>tLpVOq$s?+h)gVHxLG%{KAN|VyfbV;+q#vFXl^>N?J>q zOS8*R%KXc=%MHtiDnu)4Dk&-xD*sfuSFKj-Ru9&Q)YQ~c)~37!zw~>#TW4N3SubDT z{)+2WNdrkk!fW8Q-|M|MR&PExsx|gEi8j4zW@;{MA#90n1+@mYp0qi(t+t!APj#qv z^uHB<+tSJ1S=B|?Rq&4ZT}n4bcU1RXPf*WguV?Q;pF`h9zh(c@fbqcWpw8gqjQ>bQ=bh!&wnxhvN~@&|9!!A;dIe|@z+xLGHf~iE8f@a70Q*;Z*1Qh zR)tsJt;w%_TGv}&+CXgVZ~AQh-iq4B-p<~k*{S)?|GjHhad&dhY;SAdbN|;t%pv|^ z;SuXm^Re{trxW9otyAyQyR*b|^7E<-!HfP&?aP%bx2s=2;(wC=tiBe#9=b8Q+4&Xl z8})bYAGSYlZ`E#>@7(V0?v{Wk01SaZpb!{RP*9NvIvP^YaWF8E2M#edE;jN*Oh7__ zkNnb6kdji+G1F4hGIO)Bal?h-YHD_%|KA(j^#k~5Krhe>0nr0sd=La5bTP z_+YnvGly|t#j8&TcPGkDs?HA$J7Ee;>*ne*ph=eOtwy>=o9Rkc+eT)P->{s+FP^?Q zO^+zV`B^Gc1k+Asjtz87iW{3-epRJ!0q?OMecHmHc&s)@+LR*iMN%S$aVlWzZK1>W zm8ZLjBJuX6_*1j4)b&{^=V;>OmS+N#(V#<5KcD2{-n!05FJ*Wq{7dtNM<{ibiFnz_ zdHqCZQ%A0Z(;wt#&G&N{6pwS*aYgI3K$_DFGY2xx#fbH*02>cG+s!G0xhe!htD8w> zzDASZ`gl_J;%88yfmvd5xsE zFeiL7??$aI<5J=0BYrh$#%ED~jxMq&Owl#n<6^8f8d#Q>zEapu=>zL&eLhFSFR+_-6b_z?qB-7?4Wo)Byhrw{m{Fh1TPpft^_VuJ>Dc_ zxLZjo1QYYK75iXFVRdO`Fu!m;~G@YUP%pS<^j2p>c0;?Y@JW&F{E~ z4W^fG#y!+0iQYM3eBgf@71==0=Zl{q7VK%%VXE~=iL5h=dU!BygK?h&y!k^qkOoHM2L@722$CgWwto*N8(}eoLHTL5l#^;#g10)}< z`8V|uYj2*=%lUjlJPdJ=@mBg{gc{^(s9|w-t-c}}Th5`%8!hkh{ycf*TbCd`j~to zeTG8r7a0H;^jGo{0BHDxctoVgBt=7(fxy67WW>f$F||bC`OZYKq{Q`E>Ai%9Bef}# zA@jzLJsMIsM>P8(KZ2g|3`G1B%pDkYR79_cUAda3>UraTx*xWOoI34f&t&bFXZv|4>`Y-aG&ZYz*95;%q2$|}# z%8az>^T>ppX45Oeq{F$bz=iaqs(mgov~m~cCtJcJ*qed2VJ})HOQ!UVjG}dQd?mwT z1Ak_$ZTPzRdVeP=;{H;tIXxVrH=XEYARcmiT;lsj=<4Cy)mJyHjoDLIZ(h+nzci17 zHst3Kn`y@TctYigWM44m57YulNi_Dqi*Kc1tU- z2dCR{yN%ajPiP5KjKxeGU8k-90+Azz)M40F1{K!&RRlZH9U(|Hs>&08Q@*`G%V|7| zY4~n8)HL+^kNBMTT7iv5DaHrmC(d)49YVtXKI<1@6Mi(sEfTWXZ<&&a=R!tDHT$OX$e!ha(}rFuCnhF__lEZl z-T~#+{+Cz9tU|1pi?`1tT*}mLUP;>L6!=UF2ng`b*Wnd={&zs_Ua(nH&nM%+4nB(t&0vd(njXoWj>fl|1yO6F z?oVGF-T_)Yf9Oj0hVB5$^=pPGtHe#BR}o4m)of+Gugg0pR1OWkPIoG57PKu|v?1V9 zJR{$(8O#UdUq7g)2(L{6Wk*pEThU)&SWr<0wsR+I`e~HLvz(G%@9aG&EE@I1PqS{F zw{EZgc3rwh>SF)vlxbDSy`5Q#qJ};U&s3dyM#^6!(cakY4GpWx4~H4Lj?;IEt%NdK zY4~CFtv#%9R$?Rw_4!uxvk+A&<$$(B)9VPPm^%QOtx|aR=?MX$fRJM=>iuww97_Qx zK8%2l4~39kM#tIDu)FOl61^a0k@W)Ku+W z)q4ec5#cqMFk010Hd&TEWOs-s?d(p|l$v?Jc9VDYIRj{CNa-@ zd}hZ;acJpqWT>Xrp(IKBEaUW1lBwbwO6|Q)+O%PVcCD-28UF#{oG6)N zI%N;%Y@ULhEb3-GQOtzd^Arp`2^e2Skpd2 zM)XE`sXXDk0wbmy;)%F$F-L%x#4{cjmb2wM0Jr3Y(q8p>b{jQ5wMVqZCm}Z$78gpKYp6h%&y4YHw*0ZyA|*|b1^=%w zE;mUMofL+hQ=-L~eBXkF01ML>d~k_T+_azZ$)%Uo*(K5;vi0lZ<_2}Ne^{@14!s)X z;?p@f%j09LBy37kafuageZd4;fc-NkafF~3ABmxKA9ct)WhZW2^-lYPAYJ-~n6kw# zjJRH?mti9}2AF;2F|}hU`49uKWZGir>So}^EY6ErL$Tp!PS?%5Yf@7zcR+^8n{<)B zY!RWrDxsE7&>yWyX?s%_c0$%dUEXm3){E8rCXT_EhG{i7I$GTFW=YODi9T#P-`S1E ze;`f;S#|TfT3tVEH@MAnb_`X&3^eh+B*`n$q?Mf(Beqn}PY=G59z4%+HjBYcLAyM! zz^@k~(UE7Z$X|${*3Y+ecH`CuNVWpPoJ56I96!5P4dL5Pw8($-@G6E4vYRc0zvmPy zNZcT}xXme}a6^qAzUfL*$AJ`g=erf%&P!AT0bAo_*L6OXjz^xrqBLWaq1XjtQ}^sEMYu;lvTg*HI7*>cN@*@I$jIw zp@Q@cxQQpl&LvHgnqOai2nWTV2iY|&1)B|-@Yp;4cy5weLxYk2XtX=f8gF7&MB5GL zCr$|>;|@S5SS3E~<;S`MytgAU`G+7KK#E2$#r1Y#`XBx0nZ#ncFUDCZl4`}|bc)$i zrptok*5%eBnWdbI7PeTJa@)RYe);Jx4V_l0!aWjl3X2@bZ)wyRF)vo1e(=KVg#^Yz zB}_86`q&1M7%^QMXDhKKQ*w@Obs0q&!d7~GIAPSBPm{nBr?qCW74JsATpRYD|1I7C zO!_wrvk1P5_%2 zMVUnurJMb{&*V`xr^&QLw5V$kg)XUJQ6J*HDc6h!XMF15a{4^~&lDz#xzuXvDx*Fwp=FljIJsQB@EYs=TR zbb*80&!_bsF(`Wm9lv(Pr4}a&-qQ)6(`(^Y&Wua(sW3Wkr1s*U2sS?(DZ8jBkk5$M_pYNGDYL$T=s~!d&7dC4TMzB3W4wM07U4 z_Jn`PCR4e)y>8=g&@>(#yO8srEX6f>I|ZJuNJ zLs$s6%DR>(>?X7}{yVgZH0{$xCabrudUU^3ogFWRmBka$$bw5m8TN_ZMn^q`GX<1| zz)`9^3Z5rEgBAxRs2ygfRab&Yr;c@o-But!o|^f7|M@|Umxe%V-|Gw<6*Q~jCN#@y zeR4RYxXebq6@MZFCmOWEDx`Q0H9;_0pxIf^sh(jAmz0{CcYu|mO}ZI%9*b9PSDK8< z#)e!I*Px3$U~PjW&ZxyPCyDNWoVe;8yuww_99!Sv%cVDQSOr1GI8Ec)ApJU}L$BAH zfTRfy0otDwe9O%WDt)~}AL_WpJsdpz*OpZo=id<}=e>9nO)YmNoYPtl8+1N>P5V@F zTTe18Ch-lkz?_dMvPa<^puQL!EM6I^&?sAoUa5D1pMGrPLYK;|n!b4HqPvz)7U@of zBk`V?K?si{okJ_l$bYf9txen*qs`*Vu%R3Ah4S#{Yz{?wXRC^9#`R=yb5T1;geIJ5 zj6a?Cfe($_P&|4dTvMfHsr7351&eFvrc~n?p4XY#iiw0bD4ZX+l1*bd3Y2uHG*T6{ zDde2Jo5l3z*rMcJ)Y1@doM%Ae^HAXPwhh@E7S6eD#Aaup(YKTg+2>ZN zn;Ii@-@|1zn1LHEMEo{4y#5K7jQ3=tH)n~9I~A$9Af}|z5C>3?;$yD%+gk}WIDX1I zzEjz#clwh*Wep}K)m6kgo;{LS4aR_6iM|@R^5*xrRE8v{DF%PU?KAA;p~SrdaAuB~ zLmr!I)2kltI31ZlQx~NR+ap5MwX!#ijMz<`Oy;lOe0=dC*(IK5%VM*zMo6fiRG)F9 z}l6+Q>O1PNgywH;r~x7u3GrY<2zIdHXJ$ z@|TCYk$bQJ{AtU>;-l-{e8SwtP9ER1<|z$=9wlm_-yBM_>$nbnL}fQ-#X0Je&D)(_ z-gtO`PPR=*c)g3EL*!&jOM^!Bitz0=u|$!|_UnhICZY@|30{d9;MYFM&RiZEB_ycOeW~TVBu5PrRndCzE(zm_r5UgM9kA;*|Ka)JWQ*htyn${ECm>!O z!9cB#ahmTodQH(iEyU{KR=&jPmMRf@u6N*~ddQrBeOa@asQldV*4dG5RU30U!rM!F z1DsOiql!j^F@=)z4tjx~p%%8N_C;^)7z1)rsYICw+Sg^M8GndY7UpzrQq2EBg7G^3 z_(9=jJ`*HZ`)l8?m)7Gx6ZH;-)Q&0a78>34dm=cyBy33}b zoq|5(5pR_zFzRy4+ySVQ@nq|{8^q0@Syz2bBi`oepDh zKZI7ySwAx7>wCXQ7A5*D){Lcf9BOb;i|sZ6@B6(kMPn)*qK?hBgA${x>$p*P`JJtA zQPBn0eDyhp-a-A(MXc7ww3R_E?4xevWW1+}X)bY;o|??pRx(i{$v*a>q?TCkdg1Ge zc>Ygb@f15wrf8PDravWl)Xx1Sw|nL)HAD2hm8hK7vr%teB9}AV_{il96o5e>VAQ|% z9`3gpAW(b)n@GXfT2~v*oi(&C>m1>=v>Ew zl0UKi;h#I(Y;Nbwfek(7E-tbwW~gh1>#(uT6CMe`A{APsY0Fu-90;nShpCzHyx2iC znV8)3+Wi(x{x)9carWpiT!y#AIkPtr0tuLQHFJyIo)90H9RqBBsoIUj!-VF##_>^7{93;+`<_uUoptV?jLr{ z2V_z5eBxHQ>?xleAGj($rEV}j&K2L8&fssA$xa+~5C72{-&f*#V?%Z^H~SLnDNo&_ zr?!5UDENFSnpEyC<`GxPl{5qefAo+s!-)?dSHQ?^a1iwG{VL>N>B#+LWX#a<;mPRG z^ILln64OS!vGGp(D`Ft`5%b>WjiGVo82`HgM~mNwf2wv(pPKnV4o`Ug(9f)yn3&kS zQGimYyv9^6i6=k@KJE-nr$0B#u+xa}=O&Rf2u)DapDBRsnSw_C(B%{^V%Cu*J+XdJ91OU!8NU9L53W-VtlQPKP;;y$i_#BDYDoTR0azcFo$daV<~mtQc| zR0hVMZFHrx+Z!BDhgV*a^bJ$1!WLmw_M^HLy+YCT+#@pKkKut~8DJ+PsSPQCj0^^9 zIC2Bb0?011U1&3NBz>FkX*jh)rd8}2?MZd5l;F`tN9!49rCHM;LQ>_ynKOIdzl_g5AjdcKhjT!v$67snQS6t8Yb3>%qs zqryt={@G!kF?l2=IhhP*j(*r&n&#x%S+4}CbkweRbw2iLH%`mHS$94lQMCFt4mN^4jkD4y#Ep8oS_&x*W8)D-h0>j|9jVe=OnY9?Adc>&dlDKJ#((#Uateh8Y=2400;yE)G!xty@_wC zuBiAx&p=m2T}v5L008)ET25~6U}6Apb@TQzP*p&hnwcZv(*PL20bqb209d0QdT8HO zF$6G4U0D(N5TkU%|4BcW0L+~L;0>>u9uoN<`TrAp}alT(1=ZE11470lUxH@6@7lsk8cGeF801N*{?rn#1#IOK{@x2TU6frCf0ND5r z|6rScu(zE*rk?g#i5X;3K?zY&ULiir@c%pgw}t=K`tRXpZvWQ!uK(AZfhZ>bBl}O? z|H#}+0YLl|Gd9Wpk=f(`Kt}`sP_6t&###&jMBxC?KJ$;~p}4UZM{jQrNq&AmKR-Sv zI~3o|fc`uEUlsnX`Cr37+T*)v@84sGRI;-V7?r-NoI<3;EE)8fAy%{XdQP ze_Zh&-TFsAZW-9w+j-f!VVW|=SecWX17^D2Y@NKF+})5)ZvWK`{|~GEqYpRuw_Rg^ zto#pvc)$k`&maKE*GT}23=e=<7Gg?3|DHE39An_d^Gp~&|84ge#+3h+|8E&M3G);D z(8&RLBUUspM527Wd~X=z6E_(Yzy*i^1V9Zi04x9}zzYZgVt^DN2Pgv?fG%JNm;v_z z6yONB0$zY05D0_-kw6@f3}gVgKoL+5R09n_GtdFN1O|Xn;59G{ECQ>*Ig@NKgsi0g?38)&> z1nL3}fF?k5pm(4R&>rX;=mHD@!_B(S?{p>>*x|U`Q+^6H*FkfOJE~Aa5Y+ zkORm$77U9F3yH;xC55GiWrT&o^27?pipR>us={i=8pe8qwSjev^#@7-rGxT7C86q2 zQ>X*f4;l&0gjPb^pd-*l=r;5x3`d$$>|X47?9bSjIK((? zIFdNJIJP*CaFTG!ak_D4aJF$SaY=AFaAj}}anZOTxY@W3xFfi0xZm;c@R;!=@bvMV z@Sfsj<2}b4$J@X=$0x<-##h9*#P`8Z#D9k0kN*z;I{^U!JAoX5Ie|ApB0)955Wxq6 zUxZ|Ye1z(RwuDaz^9b7s=LrvqaERE5N{-5oDwe8|YLV)K8cD58?M$6Q-9i1H28)K9MxVxyrjTZY=71JKD@BW< zjiqg-U7-Whanl*l1<;kzP12pxGtjHjd(h|5578emP%y|bpc&E`dKvZ@$r$f2Ix(g* z_A%}w$&vC%7i2ba7CQNmUV>2m2U8D<%4nG%`xJIFg$cZ%<<%QDGY%a+P+%CX4V z%2mpJmgka3%h$;tDhMceDYPk^DM~5^DfTNtlvI^sm8O+Rm5r2hl|QI3t2n6CsvN6| zs0OI^t3lN?)l$@!)#=qy>ecGU8e$qj8pE3Sns+sGH8-`mwY;>twZYmN+G*NrI&3;_ zIvu)zuDWiT?t48BJx{$C`Y?Sx{XG3013`m8gR#40cOTrXx%<;l!7#~i)rix`$7s-) z$k@`j+W5>w$t2Ze-IU)n$n>=tjhT~Kr#al**u2vG)I!-J!(!V~#4^%y=^n>D|9cbn zY3{q+?|DGbTKwXSuk^{I`zO}@=BN)eTX`eG|*n{K;jCu5gt_t{>? zKGlBLLB=7?Vb4+4G1KwDNzp0S=>)BYE<&F<>pDMkzIHKjX>`SQwQ}upBXx6j8+K=K z_jjN3;PHs`Sof6l%s&GY@`XYAMFPvY<9 zKlzCJQOu*yk5wL51YiZA0tN$_0z(2fg5-lrf=icQ}3c)9|eb)ri_i!bq>kFhlbrKA*D-f7Pa&@{pDsTp|E$2aV7^eUu%U>)D6Z(D*s1t!iE2rE zDSK&V8LZ5=Y^U6~e5^veqPmi%GOqIKncK7VDub%wYO(6-8oHXKT5zpT?OvT_-E6&5 zeP;u2LrEh=W88D#xzF?cCab2UX3gfo7V(ybR<_psHnO(Zc2N7H_Tvu6j`dEn&bcm) zuEB1p?zR_vFP^<*dYRWl-jmde+Z*0{-S@cfwBMuuV8CHuYw*F~>X7Ns!m$4E^oZuj z_^8V0(3t#K-?+^9%L&Peu2713A?VgjK>z$XK zA9$nmW^_S);q{`<;@pza(%WUr<@L9=Z+G9hygOO(UAbI+x&~c~eNX&8^8?+7(vMsp z8`nkGdp49dCO7YHu5O{WzI=N4>GyW{4#7_5XU5OfyF$A!_f+;~_bv9fzj%DPJc#^C z@-_dE^RV?u?r8GZ^mzNk>*V@d{CDc_&wdF17(CTG{qWQE=jB=KFX~@a=i=vM7bX{< zFa3VQe`jBDU3LG_{IhoLdVPJp3WNh-2n2!!fnpL2j=6BKF$sqd7Y}n0k`oXSU_Rue z6r?1WFB1(VB@Gihk^#xi$Hm1bAu6G%X$SiMy}|V$K!OeQ1N{&XGXN$5K}bN?!nGVjtEyx z$U?-s$1u3t^)Hg>lFW(9Z_g-~t1fhnSF6gu~ zE_5FASCzjk7Z0<1-%$`El>F|TDcz-({!@KgDF^E^IgeJ|n*N(C_3tZur6J>jG3+!4 zwOV{zNP!MZI+%qZwIX?Q`hI}VgFWJ-^P`Ws1(p$WRQ{t27t7!=mgaU)X1_$-@~6Ud zinzYI=+<(*-u};BCte<*WyD&kOkUj^OF`T&wU6Dq(x5pS>)rOGd2 z9)vM{DZLW<^^4(wz}}RVl?3}bFGu>!cls2uS}NMj^UqSgTmu6cf-R!h^;fxuP4|D) zrz|`ON*kGwf8o*4=HJ;%dGu2#_9GK5+H?hf`T5cp5mont?NguM?&Or#=D#7|{7SZM zG^jsP6XwvU98qxB@2`aIu^2=xc7Msw$@C)JNluvxNWlBm!Su3h=^D^XF)pb@>0MCU zZSS7v3C)t#_#BQX4WC4|&`rx`2)utSt!O`#r`9Pc@Ct7s;d_S2B-h!-UEIPf_Pzd| zcilbZ5(V1aZ&#Q`+8=F1eXbE!BGOu#m3Xm2( z;>XpbGc0UbQ}LrHu`+s-b-cNio;^niWX|%YL9+2QQhxKT+d*2c|_jRdW>Qu_4_w*?uSJ5xOg&TY9|{y=oZ$ZK>b7|)8ztU9#x0OIpGnMYwvK(z$8ayF_Q8@Uw`&?G~1 zB>kAfDpGQ9I9(1`wvAt_GTNCiXt!WC;j0d{YxSMUAo|IK$~qUe*}&d*GnttE&XoRL z5!5KyIggyD*5&1cfnSewwo6_O<3P&WcP(1`EoSk2c*0(CM<U?OLxU=bw7_$)TY%Rx{e z!64gckdrvY!^%o=AZCeGX^)wmRm(IwyGhW-W@sep1A=uZJT6gw47rsoI8OBgKNx-x zjgvHBNyXS8H@a~RSVtDH(p~rq3Nml1&eX5ApR=fD>I~OErduf-duCcXqb&&CND6>U-KPYF1VM+8gxB73B37^$^?nL5^ zL~*KJc4?|#?4l2znnFWOi@wwI6R){%8P2uW|MzXU8kbjf%BY2XR{vd|8HeR_XMLXtK*FnW{-`Mg3b(23+wA+eURU{6qDvjErR z&fVsf-(j&eGvkixao?DG>Y`?jw`GyJ!CMLbn|66~ZA;9`@5HpZ3&sOWO4XA^MZVEI z0_FqVhEm@l?S=d9v3LJ!{xki$&O*%HW%^0_gHQLetguqLT!IJ(Y$CaPaGzOKht03o zT3L;yczoBlUUQMGl9DxA4sDKd?X8c4T?3x=E{An(0hByMR<(~~FSMe4EnKkc4=>%i zoC&f5H(6Wl-j^JqM#IZ_`O~;ViIP)jenj`I*I0>n9o)|tdBxdhqYB5da9N4plVYE; zUr7kwRz|VaHb|SYQ0heN&F%7LD)smzZ4WoAC_1azg9!9 z*4|(sLzkjnpdwH_SPQEVdtS({SS_zbHsmyNXNaPyYPiy}f=C_~WEH{^;Uvd^N?aZT^`X!u(19yHzkJqZ~ z>x>Q^R;3H70UGPuL=!bXW>{YtU}bpKSJQ)XbGxfIUAE9UA6R0N^L)lSXOcv}aAvOX ztx1P~Yv}6JGTThab3c>aQb(-6P2-dLqMfVIV8Kk*l1ENYX{Jtk=4_=h^P|YXRJ&uv zk(gpIhGy@vk{KU|({g?5&~NQ0Dot6reRz%+1rO*0)4GbfKB&eo3+fZAewIzieNF10 z7w@KlUsLPLdfT02DXV36$^Sf!e1cnJ8eNm>(h{`$K39W?X8TWd@miZ%Kr>Hd`?yOEhJjCWoqC*n$J|Tasy$9hu6I#U{#t!?=h24^f)46^u!Ehp z#5%3Ss{?g=H}S8JvK8vk$oKEa!@KMY+u!NgOz2zq^#Mkz*#%5%Yo@7x3XsYrfr}>h zx5lhb#KvUOzB+zn*~_TyOS%MJy*Zzbv2vpkoi`42n8a>dn^H!)v|cD{xG6P-8`0uT zGl@=mbCmVFk2$9|ZRRDBg)gUPPaH_ha5Mc(cplB~vhat0W@X~5%`kb^8qra+!En<$ z18Ai$=DkFcrDg$RD8B~b){L-|nE6Hkv!CTT)?iPQW8jO@*^{3`<(We-kG}bJQk-m; zG*PE>U@f*c+64VWH$ZZ{8*vj}*FHp!)K z{A;a;$pUL8yIvT16xTm2P1u?$jlG<9SUY7l54_dK_MEbuAb<1QdGwCJ5N*8DP5p_ zZR@X|vQAtj1amYW&fBdTc;=aO`hLvcFd2XHExk2f4oT;45bkA}jB^ciKcaag@kOq4eBx|0hpalCtX$J3 zW9}pi`pHh3yK%n>uS^}4(gK7i?j(MY$PF=bp1{`u3Xg$@rt;tQ~uCUf1ZH74k@~^&RcDh}Rv;N(9 zr;EI)A)ur2anJr)M8IIr#{J_)e|?E5OW!~*5-hLh>D8b32DpePo|364_q%p)y=PD; z^HreK8HWJ~80RwmR2!v!kMBblFYlr>+>RnvY&J&=_&0rhkFM{+fuWN0!)({+i3NujaFC5E{H#kKjE{#GRJgP5A zOrkx*WKa3f_k;xCD?2@L)h3@7lSW^k)x}K^I5~$ZqlMao^d6lss6-{|N~bI=Ts1v( zNeiZUrEVRmz03mgF{Jc(cfokVoc7Z6j4m+)%g$p&9-T^|cMfNf3SMOEDaf}*T-tDY zm3RZIBWf>`FOjwT@M0+0T)>xQ(m{lJ04wOgA8f(mfb7m0|ezIvvp+V@}Y*6smeY4{d;i%))3?IU44O#(y_O(?N6S@&iL{?x%>E}K1)_IOMk#; z?YG2ce0&K0`e1ICtKo@-?Eu@b*X^|P=z#|nPQp%MNahYpgA3N`3<=_V!})ZZd0UCk z$RAUpUMtyMxBNJAgWAFJXEA6}-#<22(N(>2XAl2`au|bCUO5OsV+gnC(8AwJ-mvON zs_ee*MXyLji7v-*GsXN`&Zp-X6Q=jK&Rv`PkjL9u<)$U4jAS!CNoyg~(a3WDC@$Wh?}0-|if6-M_n4 z{wZ*pQ?pQGzH8%y*kyxko5ENb(Z@cjV%_@MRFEwCmXK{H;Hr+!^`!4iVsOaKcuD=* z#I7sIV1Q_&;!6#`eZ{^uU$ku=L8p9LY=U;#=&;KhJ9&{(Z=-~>^#;`_5t<8L?57)D zrA^O~4R0%{#a$rZ%2H*vXOEjjfigZ9D><_(_UNoXtnAQqs~=jyahR;Kfty%wVgRU<)HqB{$>md}T!*si) zby&xwVf#vlfCGc=Vl$yN8`?=)n6si@u<6Z&xvPA2*z*a*CZV*pAnPf6P86#iz1hs1 zQ&iE_0)HWup35&ke`9IK-J?XrQJ}JEh zYC?87k2Z8u3>floH#78XoBGdlr)4MYjP!TVcNu~ZD224GgqVvoT}Ew@!UR1vv7d4> zJ1zS_abfHHm>p9>(n}TSJzu4!J%8RlQm}u8qTBie_qYjFm0ep%7Cu&;VMyjWZcNJ` zn*(P^g}{gi!|qOc0L$vuR~62#GZ+y$LNPvmsd14>gd@AQ!uXy?{`wpP@=z+l{UiCv z>X?Jebg{bF$eh#BHoTQDlh<*6OI~~iQ4r^ea&~q3*2>AeU-^RlNN!}t&Qj@vKXPGd z>>Sm`1_V94S0+@o2qao3g)cn+Z1vDW^jE3;O18dm)hY+yW6qsIEyqmPo<0UUs$7;;*}4^=}1L4hzWn28ZG43BHK-qKq+pmCGmsw`NT5X z7?a-`hpcG!VA0YiP*5y}M|_lKay$RH{)nwgC~tu*td32oh&+sMDnP z?P&a&jIQEOr{@#S{mnwsFpXmTZQ-dc&M-2#^8$9dD)ckxx4!x{K=H=pZbFO9%Tvmv zidLR81XHp#sxK49pd7n6l5(b!gSrD{^i(2Df4G#s2a8Jyx9SHeLFaq%b{ri-?q?s= zyaJ9qpD$?;`d}Bq9;v)-So5mk7rNAp2LH8>u)VM{s#Q_>MC3XeZ%i3 zJ;5RKsjW7&SCgp`jrXMg#3p!BxmohkXfm1R58J_WKVsgsCn60KUlFwTeGT1T^tSnd0e!9vY$XC=A5^Q^Be zyegJe9iBVO5{n4|`AC492oUzK`0pkJ#GLZngn&>oW-?5YaS}=vWS0d(l0M0d5 zr6QQ+%)eKY(1BZMAWpW4}9bG__xmK4K9gSK|b@MF7LT+z|-eyrVWzO92YzR>|Qi_zyR zVhkZ27I#8vE^;H))R>o;40=vp1sRg2PNOr`6O5V;!^3yW+V2;0KJDlZ3e(Fe>6Jnk zD{5^mB+dT{?8zt@OFJa0vd}5|wW=Ziv+Es^_r%6J>`#e2mi-bp)r#dNnR332g6))Z zoURXR0je9NV*HDV*dXj^jVY_>v!6=h;<+DM zkq24q)Fl?OjK3dIS%oFOs>l&0mO!k76gGOnnL;p?t3hwfJ4pa4j87)mPZ;MiOK|S7 zUmP95rgF&q_=kmP7H!W*i>aA)r2ioEhBk44dlKuK_C4TBv+^Ld0Pl`5zE36IU|af7cWEx3>L#NiCz4nM5-zVcub zb=h@XI152OqezJyeO5lRzd*{^DJXYv33#6V^AYrHJ7J7}wUfku@fxVoVRQ|!h^k-IaugA69M`aZk^vun zz2xJ9n?Cf2j52q9-vGbU&iqzgKj|}SL2mzrpFmK>{9b1u!-@H`>b^B{^hG=iW9d>h z`o+%=9$wc#vq2h3lkdmxbR*``m6je~&G5ju7~MO zt-n=ZB8xgQQuTV9m9^;N>Ldxsiz7#k&<{BGODd+MeA=f~b>@565JNU5(@7z+W~L0b z@|_9lZ3@=;q#?Cl(ot9W=-fU6&-Wg0BGw-M3KBh}gm1T&C}p{t{+xU%;#BgA4>+*< zI;D+GIGn#JwXgDv+C0XpPAWRKe{4B_NHSk{C>^dpi{F%QypC5hQFJo;VSO5VGLus^ zFKF9D)iNQzud6&--F2t3(bC@VIFn)2oOt0?;iwQaBPm{mw!b`QsL}P{z=`9Gn|`=U zD|azxG|*s1uDR@)XF1iPH2eJotx=B`!X)Ej7)LMc?kHp({in*%)&;)7m#H!wT z;Ga+tlKzo3enVH+$A{5fyT#IM?s~7jy#l>Hq)$ literal 0 HcmV?d00001 diff --git a/frontend/assets/images/person_priya.jpg b/frontend/assets/images/person_priya.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b89f413000e43174d96516d18b4e2811ebb10fc9 GIT binary patch literal 10182 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 F{0{?w@v8s; literal 0 HcmV?d00001 diff --git a/frontend/assets/images/person_sam.jpg b/frontend/assets/images/person_sam.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ea4dbf2cd3c1efe2bc03494e4493ef9148bb0779 GIT binary patch literal 10224 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 o1WWDmAeMi;WQu66wspK5p40LKml;s5{u literal 0 HcmV?d00001 diff --git a/frontend/assets/images/person_tom.jpg b/frontend/assets/images/person_tom.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f02229db16a7f6a3fa22c97daa6aabf930492b0e GIT binary patch literal 9903 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 data = {}; data['id'] = id; data['name'] = name; + data['photo'] = photo; data['role'] = role; data['keptRate'] = keptRate; data['streak'] = streak; diff --git a/frontend/lib/Grounded/designs/Checkbox.dart b/frontend/lib/Grounded/designs/Checkbox.dart index 8bd50d6..09d3863 100644 --- a/frontend/lib/Grounded/designs/Checkbox.dart +++ b/frontend/lib/Grounded/designs/Checkbox.dart @@ -137,7 +137,8 @@ class TickRow extends StatelessWidget { duration: const Duration(milliseconds: 160), style: TextStyle( fontSize: 15, - fontFamily: struck ? "GroundedRegular" : "GroundedMedium", + fontFamily: + struck ? "GroundedRegular" : "GroundedMedium", color: struck ? colorGrey2 : colorPrimaryDark, decoration: struck ? TextDecoration.lineThrough diff --git a/frontend/lib/Grounded/designs/DashedOutline.dart b/frontend/lib/Grounded/designs/DashedOutline.dart index d3c3184..207f690 100644 --- a/frontend/lib/Grounded/designs/DashedOutline.dart +++ b/frontend/lib/Grounded/designs/DashedOutline.dart @@ -19,16 +19,15 @@ class DashedOutline extends CustomPainter { ..strokeWidth = 1.2; final Path outline = Path() - ..addRRect(RRect.fromRectAndRadius( - Offset.zero & size, Radius.circular(radius))); + ..addRRect( + RRect.fromRectAndRadius(Offset.zero & size, Radius.circular(radius))); const double dash = 4; const double gap = 3; for (final metric in outline.computeMetrics()) { double distance = 0; while (distance < metric.length) { - canvas.drawPath( - metric.extractPath(distance, distance + dash), paint); + canvas.drawPath(metric.extractPath(distance, distance + dash), paint); distance = distance + dash + gap; } } diff --git a/frontend/lib/Grounded/designs/DotDigits.dart b/frontend/lib/Grounded/designs/DotDigits.dart index 47dee5c..8d91155 100644 --- a/frontend/lib/Grounded/designs/DotDigits.dart +++ b/frontend/lib/Grounded/designs/DotDigits.dart @@ -82,11 +82,10 @@ class _DotDigitsPainter extends CustomPainter { final Path? outline = _glyph(glyph); if (outline != null) { - final Path placed = outline.transform( - (Matrix4.identity() - ..translateByDouble(x, 0, 0, 1) - ..scaleByDouble(_scale, _scale, 1, 1)) - .storage); + final Path placed = outline.transform((Matrix4.identity() + ..translateByDouble(x, 0, 0, 1) + ..scaleByDouble(_scale, _scale, 1, 1)) + .storage); for (PathMetric metric in placed.computeMetrics()) { final int count = (metric.length / gap).floor().clamp(1, 400); diff --git a/frontend/lib/Grounded/designs/MemberAvatar.dart b/frontend/lib/Grounded/designs/MemberAvatar.dart index 8a2cd86..239b965 100644 --- a/frontend/lib/Grounded/designs/MemberAvatar.dart +++ b/frontend/lib/Grounded/designs/MemberAvatar.dart @@ -2,47 +2,19 @@ import 'package:flutter/material.dart'; import '../about/internal/application/TextType.dart'; import '../utils/Colors.dart'; -import '../utils/CommonUtils.dart'; -import 'scene/SceneArt.dart'; +import 'Portrait.dart'; import 'text/Text.dart'; -/// A member's round portrait: a landscape picked from their name with the -/// initial on it, so everyone is recognisable without a photo. -Widget memberAvatar(String name, double size, {Color? border}) { - const List> palettes = >[ - sceneWorkout, - sceneMeditation, - sceneMustDo, - ]; - final int seed = sceneSeed(name.isEmpty ? "?" : name); - final String initial = - name.isEmpty ? "?" : name.substring(0, 1).toUpperCase(); - - return Container( - width: size, - height: size, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: border ?? colorWhite, width: 2), - ), - child: ClipOval( - child: Stack( - fit: StackFit.expand, - children: [ - SceneArt(palette: palettes[seed % palettes.length], seed: seed), - Center( - child: - text(initial, size * 0.42, TextType.Medium, color: colorWhite), - ), - ], - ), - ), - ); +/// A member's round photo, or their initial when they have none. +Widget memberAvatar(String name, double size, + {String photo = "", Color? border}) { + return portrait(photo, name, size, border: border); } /// Several portraits overlapping, with "+N" once there are more than fit. +/// [photos] lines up with [names]; missing entries fall back to initials. Widget avatarStack(List names, double size, - {int max = 4, Color? border}) { + {List photos = const [], int max = 4, Color? border}) { final List shown = names.take(max).toList(); final int extra = names.length - shown.length; final double step = size * 0.68; @@ -56,7 +28,9 @@ Widget avatarStack(List names, double size, for (int index = 0; index < shown.length; index++) Positioned( left: step * index, - child: memberAvatar(shown[index], size, border: border), + child: memberAvatar(shown[index], size, + photo: index < photos.length ? photos[index] : "", + border: border), ), if (extra > 0) Positioned( diff --git a/frontend/lib/Grounded/designs/Portrait.dart b/frontend/lib/Grounded/designs/Portrait.dart new file mode 100644 index 0000000..6b0f6c7 --- /dev/null +++ b/frontend/lib/Grounded/designs/Portrait.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +import '../about/internal/application/TextType.dart'; +import '../utils/Colors.dart'; +import 'text/Text.dart'; + +/// Where a portrait comes from. A stored `pic` is one of: +/// - `data:image/...;base64,...` — a photo picked on this device +/// - `http(s)://...` — a photo the server holds +/// - `asset:assets/images/...` — a bundled sample (mock mode) +/// Anything else, or empty, has no photo. +ImageProvider? portraitImage(String pic) { + if (pic.startsWith("data:image")) { + final int comma = pic.indexOf(","); + if (comma < 0) { + return null; + } + return MemoryImage(base64Decode(pic.substring(comma + 1))); + } + if (pic.startsWith("http://") || pic.startsWith("https://")) { + return NetworkImage(pic); + } + if (pic.startsWith("asset:")) { + return AssetImage(pic.substring("asset:".length)); + } + return null; +} + +/// A round photo of a person. Without a photo, their initial on a soft +/// neutral disc — never a landscape: a person should look like a person. +Widget portrait(String pic, String name, double size, + {Color? border, double borderWidth = 2}) { + final ImageProvider? image = portraitImage(pic); + final String initial = + name.trim().isEmpty ? "?" : name.trim().substring(0, 1).toUpperCase(); + + return Container( + width: size, + height: size, + alignment: Alignment.center, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: colorMuted, + border: Border.all(color: border ?? colorWhite, width: borderWidth), + image: image == null + ? null + : DecorationImage(image: image, fit: BoxFit.cover), + ), + child: image == null + ? text(initial, size * 0.4, TextType.Medium, color: colorInkSoft) + : null, + ); +} diff --git a/frontend/lib/Grounded/designs/ProgressRing.dart b/frontend/lib/Grounded/designs/ProgressRing.dart index 61a1d4f..d88077d 100644 --- a/frontend/lib/Grounded/designs/ProgressRing.dart +++ b/frontend/lib/Grounded/designs/ProgressRing.dart @@ -53,8 +53,8 @@ class ProgressRing extends StatelessWidget { ), child: Center( child: child ?? - text("${(clamped * 100).round()}%", size * 0.22, - TextType.Bold, + text( + "${(clamped * 100).round()}%", size * 0.22, TextType.Bold, color: colorPrimaryDark), ), ); diff --git a/frontend/lib/Grounded/designs/input/CountStepper.dart b/frontend/lib/Grounded/designs/input/CountStepper.dart index b14577c..b01f432 100644 --- a/frontend/lib/Grounded/designs/input/CountStepper.dart +++ b/frontend/lib/Grounded/designs/input/CountStepper.dart @@ -56,8 +56,8 @@ Widget _stepButton(IconData icon, VoidCallback? onTap) { color: enabled ? colorSecondary : colorInset, borderRadius: BorderRadius.circular(10), ), - child: Icon(icon, - size: 14, color: enabled ? colorPrimaryDark : colorGrey), + child: + Icon(icon, size: 14, color: enabled ? colorPrimaryDark : colorGrey), ), ); } diff --git a/frontend/lib/Grounded/designs/input/InputFields.dart b/frontend/lib/Grounded/designs/input/InputFields.dart index 7753708..126875a 100644 --- a/frontend/lib/Grounded/designs/input/InputFields.dart +++ b/frontend/lib/Grounded/designs/input/InputFields.dart @@ -46,9 +46,8 @@ Widget inputField( fontSize: 13, color: colorGrey, ), - prefixIcon: icon == null - ? null - : Icon(icon, size: 18, color: colorGrey2), + prefixIcon: + icon == null ? null : Icon(icon, size: 18, color: colorGrey2), filled: true, fillColor: colorWhite, contentPadding: diff --git a/frontend/lib/Grounded/designs/text/Text.dart b/frontend/lib/Grounded/designs/text/Text.dart index e3720c1..7c8743d 100644 --- a/frontend/lib/Grounded/designs/text/Text.dart +++ b/frontend/lib/Grounded/designs/text/Text.dart @@ -55,8 +55,8 @@ Widget markedText( TextSpan( text: parts[index], style: TextStyle( - fontFamily: getTextType( - index.isOdd ? TextType.Medium : TextType.Regular), + fontFamily: + getTextType(index.isOdd ? TextType.Medium : TextType.Regular), ), ), ], diff --git a/frontend/lib/Grounded/mock/MockComms.dart b/frontend/lib/Grounded/mock/MockComms.dart index d13a84f..ad326e2 100644 --- a/frontend/lib/Grounded/mock/MockComms.dart +++ b/frontend/lib/Grounded/mock/MockComms.dart @@ -46,7 +46,8 @@ class MockComms implements ConnectComms { Future _done() => _ok({'success': true}); - Future _emptyPage() => _ok(MockStore.page(>[])); + Future _emptyPage() => + _ok(MockStore.page(>[])); // ── Auth ────────────────────────────────────────────────────────────────── @@ -72,7 +73,8 @@ class MockComms implements ConnectComms { Future logout() => _done(); @override - Future me() => _ok({'id': 'mock-user', 'name': 'Alex'}); + Future me() => + _ok({'id': 'mock-user', 'name': 'Alex'}); // ── Goals ───────────────────────────────────────────────────────────────── @@ -152,7 +154,8 @@ class MockComms implements ConnectComms { @override Future completeCommitmentEntry(CompletionRequest request) { - final Map? item = MockStore.commitment(request.commitmentId); + final Map? item = + MockStore.commitment(request.commitmentId); if (item != null) { final DateTime? end = DateTime.tryParse(item['dueEnd'] ?? ''); item['status'] = end != null && end.isBefore(DateTime.now()) @@ -164,7 +167,8 @@ class MockComms implements ConnectComms { @override Future deferCommitmentEntry(DeferralRequest request) { - final Map? item = MockStore.commitment(request.commitmentId); + final Map? item = + MockStore.commitment(request.commitmentId); if (item != null) { final DateTime now = DateTime.now(); item['dueStart'] = request.newDueStart.isNotEmpty @@ -181,7 +185,8 @@ class MockComms implements ConnectComms { @override Future abandonCommitmentEntry(AbandonRequest request) { - final Map? item = MockStore.commitment(request.commitmentId); + final Map? item = + MockStore.commitment(request.commitmentId); if (item != null) { item['status'] = 'Abandoned'; } @@ -271,7 +276,8 @@ class MockComms implements ConnectComms { Future updateSickMode(SickModeRequest request) => _done(); @override - Future checkDistress() => _ok({'distressed': false}); + Future checkDistress() => + _ok({'distressed': false}); @override Future updateTone(ToneRequest request) => _done(); @@ -291,8 +297,8 @@ class MockComms implements ConnectComms { Future getKeystoneHabits() => _ok([]); @override - Future getMyRoutines(HistoryRequest request) => _ok( - MockStore.routines.map((routine) => routine.toJson()).toList()); + Future getMyRoutines(HistoryRequest request) => + _ok(MockStore.routines.map((routine) => routine.toJson()).toList()); @override Future saveRoutineEntry(Map request) { @@ -385,13 +391,16 @@ class MockComms implements ConnectComms { Future getSessionHistory(HistoryRequest request) => _emptyPage(); @override - Future getLastSessionFor(IdRequest request) => _ok({}); + Future getLastSessionFor(IdRequest request) => + _ok({}); @override - Future getWeeklyVolume(ReportCardRequest request) => _ok({}); + Future getWeeklyVolume(ReportCardRequest request) => + _ok({}); @override - Future getContactVolume(ReportCardRequest request) => _ok({}); + Future getContactVolume(ReportCardRequest request) => + _ok({}); @override Future getPersonalRecords(HistoryRequest request) => _emptyPage(); @@ -399,10 +408,12 @@ class MockComms implements ConnectComms { // ── Report card ─────────────────────────────────────────────────────────── @override - Future getWeeklyReportCard(ReportCardRequest request) => _ok(_report()); + Future getWeeklyReportCard(ReportCardRequest request) => + _ok(_report()); @override - Future getMonthlyReportCard(ReportCardRequest request) => _ok(_report()); + Future getMonthlyReportCard(ReportCardRequest request) => + _ok(_report()); Map _report() { final DateTime now = DateTime.now(); @@ -421,13 +432,27 @@ class MockComms implements ConnectComms { 'Health': 0.79, }, 'completionByWeekday': { - '1': 0.9, '2': 0.8, '3': 0.75, '4': 0.45, '5': 0.7, '6': 0.6, '7': 0.85, + '1': 0.9, + '2': 0.8, + '3': 0.75, + '4': 0.45, + '5': 0.7, + '6': 0.6, + '7': 0.85, }, 'worstHour': 19, 'debtTrend': [8, 9.5, 11, 10.2, 7.4, 6.8, 5.9], 'deferralLeaderboard': >[ - {'commitmentId': 'c-email', 'title': 'Email supervisor the outline', 'count': 2}, - {'commitmentId': 'c-kitchen', 'title': 'Deep clean the kitchen', 'count': 1}, + { + 'commitmentId': 'c-email', + 'title': 'Email supervisor the outline', + 'count': 2 + }, + { + 'commitmentId': 'c-kitchen', + 'title': 'Deep clean the kitchen', + 'count': 1 + }, ], 'excuseTaxonomy': [], 'estimationAccuracy': {'Deep work': 1.6, 'Admin': 1.1}, @@ -451,32 +476,42 @@ class MockComms implements ConnectComms { // ── Groups ──────────────────────────────────────────────────────────────── @override - Future getMyGroups(HistoryRequest request) => _ok(MockGroups.mine()); + Future getMyGroups(HistoryRequest request) => + _ok(MockGroups.mine()); @override - Future getGroupEntry(IdRequest request) => _ok(MockGroups.one(request.id)); + Future getGroupEntry(IdRequest request) => + _ok(MockGroups.one(request.id)); @override - Future saveGroupEntry(GroupRequest request) => _ok(MockGroups.save(request)); + Future saveGroupEntry(GroupRequest request) => + _ok(MockGroups.save(request)); @override - Future joinGroupEntry(JoinGroupRequest request) => _ok(MockGroups.join(request)); + Future joinGroupEntry(JoinGroupRequest request) => + _ok(MockGroups.join(request)); @override - Future leaveGroupEntry(IdRequest request) => _ok(MockGroups.leave(request.id)); + Future leaveGroupEntry(IdRequest request) => + _ok(MockGroups.leave(request.id)); @override - Future saveGroupGoalEntry(GroupGoalRequest request) => _ok(MockGroups.saveGoal(request)); + Future saveGroupGoalEntry(GroupGoalRequest request) => + _ok(MockGroups.saveGoal(request)); @override - Future logObjectiveProgress(ObjectiveProgressRequest request) => _ok(MockGroups.logProgress(request)); + Future logObjectiveProgress(ObjectiveProgressRequest request) => + _ok(MockGroups.logProgress(request)); @override - Future getGroupMessages(IdRequest request) => _ok(MockGroups.messages(request.id)); + Future getGroupMessages(IdRequest request) => + _ok(MockGroups.messages(request.id)); @override - Future sendGroupMessage(GroupMessageRequest request) => _ok(MockGroups.send(request)); + Future sendGroupMessage(GroupMessageRequest request) => + _ok(MockGroups.send(request)); @override - Future nudgeGroupMember(NudgeRequest request) => _ok(MockGroups.nudge(request)); + Future nudgeGroupMember(NudgeRequest request) => + _ok(MockGroups.nudge(request)); } diff --git a/frontend/lib/Grounded/mock/MockGroups.dart b/frontend/lib/Grounded/mock/MockGroups.dart index 271b469..19d7ed0 100644 --- a/frontend/lib/Grounded/mock/MockGroups.dart +++ b/frontend/lib/Grounded/mock/MockGroups.dart @@ -35,6 +35,18 @@ class MockGroups { static final Random _random = Random(7); + /// Sample people with a bundled portrait (assets/images/person_*.jpg). + static const List _photos = [ + 'alex', + 'sam', + 'priya', + 'jordan', + 'mei', + 'tom', + 'ade', + 'lena' + ]; + static Map _member(String id, String name, {String role = "Member", double kept = 0.7, @@ -44,6 +56,9 @@ class MockGroups { return { 'id': id, 'name': name, + 'photo': _photos.contains(name.toLowerCase()) + ? 'asset:assets/images/person_${name.toLowerCase()}.jpg' + : '', 'role': role, 'keptRate': kept, 'streak': streak, @@ -55,9 +70,8 @@ class MockGroups { }; } - static Map _objective( - String id, String title, String unit, double target, double current, - double mine) { + static Map _objective(String id, String title, String unit, + double target, double current, double mine) { return { 'id': id, 'title': title, @@ -105,7 +119,8 @@ class MockGroups { _member('m-sam', 'Sam', role: 'Owner', kept: 0.92, streak: 11, checkedInHoursAgo: 2), _member(_me, _myName, kept: 0.78, streak: 4, checkedInHoursAgo: 4), - _member('m-priya', 'Priya', kept: 0.85, streak: 7, checkedInHoursAgo: 3), + _member('m-priya', 'Priya', + kept: 0.85, streak: 7, checkedInHoursAgo: 3), _member('m-jordan', 'Jordan', kept: 0.41, streak: 0, standing: 'Warned', checkedInHoursAgo: 52), ], @@ -114,7 +129,8 @@ class MockGroups { 'id': 'gg-run', 'title': 'Run 200 km this month', 'description': 'Every kilometre counts, treadmill included.', - 'targetDate': DateTime(now.year, now.month + 1, 0).toIso8601String(), + 'targetDate': + DateTime(now.year, now.month + 1, 0).toIso8601String(), 'colourHex': colorToHex(goalAccents[1]), 'objectives': >[ _objective('ob-km', 'Distance run', 'km', 200, 86, 22), @@ -151,11 +167,14 @@ class MockGroups { 'id': 'gg-drafts', 'title': 'Full drafts submitted', 'description': 'Three theses, one deadline.', - 'targetDate': DateTime(now.year, now.month + 1, 15).toIso8601String(), + 'targetDate': + DateTime(now.year, now.month + 1, 15).toIso8601String(), 'colourHex': colorToHex(goalAccents[3]), 'objectives': >[ - _objective('ob-words', 'Words written', 'words', 30000, 18400, 7200), - _objective('ob-chapters', 'Chapters reviewed', 'chapters', 9, 3, 1), + _objective( + 'ob-words', 'Words written', 'words', 30000, 18400, 7200), + _objective( + 'ob-chapters', 'Chapters reviewed', 'chapters', 9, 3, 1), _objective('ob-hours', 'Deep-work hours', 'hours', 120, 64, 23), ], }, @@ -191,15 +210,17 @@ class MockGroups { _messages['group-run'] = >[ _message('group-run', 'm-sam', 'Sam', 'Same route tomorrow? Gate at 6:15.', 60 * 20), - _message('group-run', _me, _myName, "I'm in. Bringing Priya's spare torch.", - 60 * 19 + 40), + _message('group-run', _me, _myName, + "I'm in. Bringing Priya's spare torch.", 60 * 19 + 40), _message('group-run', 'm-priya', 'Priya', 'logged 6 km on Distance run', - 60 * 3, kind: 'CheckIn'), - _message('group-run', 'm-sam', 'Sam', 'logged 8 km on Distance run', - 60 * 2, kind: 'CheckIn'), + 60 * 3, + kind: 'CheckIn'), + _message( + 'group-run', 'm-sam', 'Sam', 'logged 8 km on Distance run', 60 * 2, + kind: 'CheckIn'), _message('group-run', 'm-sam', 'Sam', - 'nudged Jordan: two days without a run, we miss you out there', - 95, kind: 'Nudge'), + 'nudged Jordan: two days without a run, we miss you out there', 95, + kind: 'Nudge'), _message('group-run', 'm-priya', 'Priya', '86 km down. We need about 9 a day to make it, doable.', 40), ]; @@ -208,10 +229,15 @@ class MockGroups { kind: 'System'), _message('group-thesis', 'm-mei', 'Mei', 'Chapter 3 is with my supervisor. Terrified.', 60 * 26), - _message('group-thesis', _me, _myName, 'logged 1,200 words on Words written', - 60 * 22, kind: 'CheckIn'), - _message('group-thesis', 'm-tom', 'Tom', - 'Library 9 to 1 tomorrow if anyone wants a co-working block.', 60 * 7), + _message('group-thesis', _me, _myName, + 'logged 1,200 words on Words written', 60 * 22, + kind: 'CheckIn'), + _message( + 'group-thesis', + 'm-tom', + 'Tom', + 'Library 9 to 1 tomorrow if anyone wants a co-working block.', + 60 * 7), ]; _messages['group-walk'] = >[ _message('group-walk', 'm-ade', 'Ade', @@ -236,7 +262,8 @@ class MockGroups { .toList(); for (Map message in due) { _pending.remove(message); - _messages.putIfAbsent(message['groupId'], () => >[]) + _messages + .putIfAbsent(message['groupId'], () => >[]) .add(message); final Map? group = _find(message['groupId']); if (group != null) { @@ -259,8 +286,9 @@ class MockGroups { } static void _post(String groupId, String body, String kind) { - _messages.putIfAbsent(groupId, () => >[]).add( - _message(groupId, _me, _myName, body, 0, kind: kind)); + _messages + .putIfAbsent(groupId, () => >[]) + .add(_message(groupId, _me, _myName, body, 0, kind: kind)); } /// Someone answers a few seconds later, so the chat feels inhabited. @@ -331,8 +359,8 @@ class MockGroups { 'inviteCode': _code(), 'unread': 0, 'members': >[ - _member(_me, _myName, role: 'Owner', kept: 0.78, streak: 4, - checkedInHoursAgo: 0), + _member(_me, _myName, + role: 'Owner', kept: 0.78, streak: 4, checkedInHoursAgo: 0), ], 'goals': >[], }; @@ -366,8 +394,8 @@ class MockGroups { 'members': >[], 'goals': >[], }); - (group['members'] as List).add(_member(_me, _myName, - kept: 0.78, streak: 4, checkedInHoursAgo: 0)); + (group['members'] as List).add( + _member(_me, _myName, kept: 0.78, streak: 4, checkedInHoursAgo: 0)); _groups.insert(0, group); _post(group['id'], 'joined the group', 'System'); _queueReply(group['id']); @@ -422,7 +450,7 @@ class MockGroups { _post( group['id'], 'logged $amount ${objective['unit']} on ${objective['title']}' - '${request.note.isEmpty ? "" : " — ${request.note}"}', + '${request.note.isEmpty ? "" : " — ${request.note}"}', 'CheckIn'); _queueReply(group['id']); } @@ -465,8 +493,8 @@ class MockGroups { who = member['name']; } } - _post(request.groupId, 'nudged $who: today still counts, go get it', - 'Nudge'); + _post( + request.groupId, 'nudged $who: today still counts, go get it', 'Nudge'); return {'success': true}; } } diff --git a/frontend/lib/Grounded/mock/MockMemory.dart b/frontend/lib/Grounded/mock/MockMemory.dart index a5d905d..14cb12a 100644 --- a/frontend/lib/Grounded/mock/MockMemory.dart +++ b/frontend/lib/Grounded/mock/MockMemory.dart @@ -21,7 +21,8 @@ class MockMemory extends InternalMemory { _seeded = true; setMyDescription( MeDescription(id: "mock-user", name: "Alex", token: "mock-token")); - setUserDetails(UserDetails(pic: "", name: "Alex")); + setUserDetails(UserDetails( + pic: "asset:assets/images/person_alex.jpg", name: "Alex")); setTodos([ TodoItem(id: "t1", title: "Pick up gym kit"), TodoItem(id: "t2", title: "Book physio for Thursday"), @@ -51,8 +52,9 @@ class MockMemory extends InternalMemory { title: title, kind: kind, startedAt: at, - seconds: (seconds * (held ? 1 : 0.5) * (0.85 + random.nextDouble() * 0.3)) - .round(), + seconds: + (seconds * (held ? 1 : 0.5) * (0.85 + random.nextDouble() * 0.3)) + .round(), completedSteps: held ? steps : 1 + random.nextInt(steps - 1), totalSteps: steps, )); diff --git a/frontend/lib/Grounded/mock/MockStore.dart b/frontend/lib/Grounded/mock/MockStore.dart index 3c8eaef..c8ada8c 100644 --- a/frontend/lib/Grounded/mock/MockStore.dart +++ b/frontend/lib/Grounded/mock/MockStore.dart @@ -43,7 +43,8 @@ class MockStore { { 'id': 'goal-strong', 'title': 'Get strong', - 'description': 'Deadlift my bodyweight by spring, without getting hurt.', + 'description': + 'Deadlift my bodyweight by spring, without getting hurt.', 'category': 'Health', 'defaultClass': 'Standard', 'startDate': at(-30, 0).toIso8601String(), @@ -145,10 +146,7 @@ class MockStore { minutes: 60, category: 'Deep work'), task('c-meal', 'Meal prep for the week', - goal: 'goal-strong', - start: at(1, 17), - end: at(1, 20), - minutes: 90), + goal: 'goal-strong', start: at(1, 17), end: at(1, 20), minutes: 90), // Two things already past their window, so the overdue queue and debt // have something to show. task('c-email', 'Email supervisor the outline', @@ -159,10 +157,7 @@ class MockStore { status: 'Overdue', deferrals: 1), task('c-kitchen', 'Deep clean the kitchen', - start: at(-1, 18), - end: at(-1, 21), - minutes: 45, - status: 'Overdue'), + start: at(-1, 18), end: at(-1, 21), minutes: 45, status: 'Overdue'), ]); // ── Routines ─────────────────────────────────────────────────────────── @@ -175,7 +170,10 @@ class MockStore { final RoutineChain push = RoutineTemplates.upperPush() ..id = 'routine-push' ..goalId = 'goal-strong' - ..days = [1, 2, 3, 4, 5, 6, 7].where((day) => day != weekday).take(2).toList(); + ..days = [1, 2, 3, 4, 5, 6, 7] + .where((day) => day != weekday) + .take(2) + .toList(); final RoutineChain breathing = RoutineTemplates.boxBreathing() ..id = 'routine-breathing' @@ -262,12 +260,14 @@ class MockStore { final List> tasks = goalTasks(goal['id']); final int completed = tasks .where((item) => - item['status'] == 'Completed' || item['status'] == 'LateCompleted') + item['status'] == 'Completed' || + item['status'] == 'LateCompleted') .length; final int late = tasks.where((item) => !_settled(item) && _windowClosed(item)).length; return Map.from(goal) - ..['totalTasks'] = tasks.where((item) => item['status'] != 'Abandoned').length + ..['totalTasks'] = + tasks.where((item) => item['status'] != 'Abandoned').length ..['completedTasks'] = completed ..['overdueTasks'] = late ..['debtContribution'] = late * 2.0; diff --git a/frontend/lib/Grounded/see/group/GroupChatState.dart b/frontend/lib/Grounded/see/group/GroupChatState.dart index f0fc8cf..0d76996 100644 --- a/frontend/lib/Grounded/see/group/GroupChatState.dart +++ b/frontend/lib/Grounded/see/group/GroupChatState.dart @@ -98,6 +98,16 @@ class GroupChatState extends State implements ConnectGroupChat { _onSend(reply); } + /// A message author's photo, looked up among the group's members. + String _photoOf(String authorId) { + for (final member in widget.group.members) { + if (member.id == authorId) { + return member.photo; + } + } + return ""; + } + // ── Views ───────────────────────────────────────────────────────────────── Widget _mobileView(BoxConstraints constraints) { @@ -145,8 +155,7 @@ class GroupChatState extends State implements ConnectGroupChat { return Container( decoration: BoxDecoration( - borderRadius: - const BorderRadius.vertical(bottom: Radius.circular(20)), + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(20)), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -188,7 +197,10 @@ class GroupChatState extends State implements ConnectGroupChat { const SizedBox(width: 10), avatarStack( widget.group.members.map((member) => member.name).toList(), 30, - max: 3, border: colorNight), + photos: + widget.group.members.map((member) => member.photo).toList(), + max: 3, + border: colorNight), ], ), ); @@ -275,7 +287,8 @@ class GroupChatState extends State implements ConnectGroupChat { children: [ grouped ? const SizedBox(width: 34) - : memberAvatar(message.authorName, 34), + : memberAvatar(message.authorName, 34, + photo: _photoOf(message.authorId)), const SizedBox(width: 8), Flexible( child: Container( @@ -328,7 +341,8 @@ class GroupChatState extends State implements ConnectGroupChat { ), child: Row( children: [ - memberAvatar(message.authorName, 34), + memberAvatar(message.authorName, 34, + photo: _photoOf(message.authorId)), const SizedBox(width: 12), Expanded( child: Column( diff --git a/frontend/lib/Grounded/see/group/GroupDetailState.dart b/frontend/lib/Grounded/see/group/GroupDetailState.dart index a888cfd..5db34bf 100644 --- a/frontend/lib/Grounded/see/group/GroupDetailState.dart +++ b/frontend/lib/Grounded/see/group/GroupDetailState.dart @@ -329,7 +329,8 @@ class GroupDetailState extends State horizontal: 7, vertical: 3), decoration: BoxDecoration( color: colorLime, - borderRadius: const BorderRadius.all(Radius.circular(8)), + borderRadius: + const BorderRadius.all(Radius.circular(8)), ), child: text( "${_group.unread} new", 10, TextType.Medium, @@ -519,7 +520,7 @@ class GroupDetailState extends State child: text("$rank", 16, TextType.Medium, color: rank == 1 ? colorInk : colorGrey2), ), - memberAvatar(member.name, 46), + memberAvatar(member.name, 46, photo: member.photo), const SizedBox(width: 12), Expanded( child: Column( @@ -632,7 +633,8 @@ class GroupDetailState extends State alignment: Alignment.center, decoration: BoxDecoration( color: colorAmbientMid, - borderRadius: const BorderRadius.all(Radius.circular(12)), + borderRadius: + const BorderRadius.all(Radius.circular(12)), ), child: text( "+${formatAmount(preset)}", 15, TextType.Medium, diff --git a/frontend/lib/Grounded/see/group/GroupEditorState.dart b/frontend/lib/Grounded/see/group/GroupEditorState.dart index 7878809..0319bc6 100644 --- a/frontend/lib/Grounded/see/group/GroupEditorState.dart +++ b/frontend/lib/Grounded/see/group/GroupEditorState.dart @@ -245,7 +245,7 @@ class GroupEditorState extends State text(name, 26, TextType.Light, color: colorWhite, maxLines: 2, overflow: TextOverflow.ellipsis), const SizedBox(height: 16), - avatarStack(const ["Alex"], 34), + avatarStack(const ["You"], 34), ], ), ); diff --git a/frontend/lib/Grounded/see/group/GroupsState.dart b/frontend/lib/Grounded/see/group/GroupsState.dart index 5913732..a5c4405 100644 --- a/frontend/lib/Grounded/see/group/GroupsState.dart +++ b/frontend/lib/Grounded/see/group/GroupsState.dart @@ -180,7 +180,8 @@ class GroupsState extends State implements ConnectGroups { horizontal: 10, vertical: 6), decoration: BoxDecoration( color: colorLime, - borderRadius: const BorderRadius.all(Radius.circular(8)), + borderRadius: + const BorderRadius.all(Radius.circular(8)), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -218,7 +219,10 @@ class GroupsState extends State implements ConnectGroups { children: [ avatarStack( group.members.map((member) => member.name).toList(), - 32), + 32, + photos: group.members + .map((member) => member.photo) + .toList()), const Spacer(), text("${(group.progress * 100).round()}%", 22, TextType.Light, diff --git a/frontend/lib/Grounded/see/home/HomeState.dart b/frontend/lib/Grounded/see/home/HomeState.dart index fe9d335..cfd58ef 100644 --- a/frontend/lib/Grounded/see/home/HomeState.dart +++ b/frontend/lib/Grounded/see/home/HomeState.dart @@ -27,6 +27,7 @@ import '../../designs/Component.dart'; import '../../designs/FloatingNav.dart'; import '../../designs/GlassSurface.dart'; import '../../designs/MemberAvatar.dart'; +import '../../designs/Portrait.dart'; import '../../designs/ProgressRing.dart'; import '../../designs/Responsive.dart'; import '../../designs/Shell.dart'; @@ -35,6 +36,7 @@ import '../../designs/input/InputFields.dart'; import '../../designs/scene/SceneArt.dart'; import '../../designs/text/Text.dart'; import '../../utils/Colors.dart'; +import '../../utils/PhotoPicker.dart'; import '../../utils/CommonUtils.dart'; import '../../utils/StandingEngine.dart'; import '../../utils/Thresholds.dart'; @@ -247,6 +249,13 @@ class HomeState extends State implements ConnectHome { } } + void _onChangePhoto() async { + final String? pic = await PhotoPicker.pickPortrait(); + if (pic != null) { + _model?.savePhoto(pic); + } + } + void _onOpenSettings() { GroundedNavigation() .navigateToPage(NavigatorType.justOpen, const Settings(), context); @@ -1194,28 +1203,11 @@ class HomeState extends State implements ConnectHome { // ── Header helpers ──────────────────────────────────────────────────────── /// A portrait cut from the painted landscape, with the initial on it. + /// Your photo. Tapping it picks a new one. Widget _avatar(double size) { - final String initial = - _user.name.isEmpty ? "G" : _user.name.substring(0, 1).toUpperCase(); - - return Container( - width: size, - height: size, - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: colorWhite, width: 2), - ), - child: Stack( - fit: StackFit.expand, - children: [ - SceneArt(palette: sceneWorkout, seed: sceneSeed(_user.name)), - Center( - child: - text(initial, size * 0.4, TextType.Medium, color: colorWhite), - ), - ], - ), + return GestureDetector( + onTap: _onChangePhoto, + child: portrait(_user.pic, _user.name, size), ); } @@ -1705,7 +1697,10 @@ class HomeState extends State implements ConnectHome { children: [ avatarStack( group.members.map((member) => member.name).toList(), - 26), + 26, + photos: group.members + .map((member) => member.photo) + .toList()), const Spacer(), Icon(CupertinoIcons.chat_bubble_2_fill, size: 16, color: colorInkSoft), diff --git a/frontend/lib/Grounded/see/home/ViewHome.dart b/frontend/lib/Grounded/see/home/ViewHome.dart index 739f67a..e8cd172 100644 --- a/frontend/lib/Grounded/see/home/ViewHome.dart +++ b/frontend/lib/Grounded/see/home/ViewHome.dart @@ -80,6 +80,15 @@ class ViewHome extends ParentViewModel { } } + /// A new profile photo, kept on the device with the rest of the user's + /// details. There is no upload endpoint yet, so it does not leave the phone. + void savePhoto(String pic) async { + final UserDetails details = await getDataManager().getUserDetails(); + details.pic = pic; + await getDataManager().setUserDetails(details); + connection.onUserLoaded(details); + } + void loadTodos() async { connection.onTodosLoaded(await getDataManager().getTodos()); } @@ -140,8 +149,7 @@ class ViewHome extends ParentViewModel { /// Reminders are one-shots for the coming week, so they are re-placed /// whenever the schedule is read. void _placeReminders(List routines) async { - final ProgressTargets targets = - await getDataManager().getProgressTargets(); + final ProgressTargets targets = await getDataManager().getProgressTargets(); if (targets.reminders) { await LocalNotificationEngine.scheduleRoutineReminders( routines, targets.reminderLead); diff --git a/frontend/lib/Grounded/see/settings/SettingsState.dart b/frontend/lib/Grounded/see/settings/SettingsState.dart index 3385c1b..ae0a68d 100644 --- a/frontend/lib/Grounded/see/settings/SettingsState.dart +++ b/frontend/lib/Grounded/see/settings/SettingsState.dart @@ -8,12 +8,14 @@ import '../../about/internal/application/ToneLevel.dart'; import '../../about/internal/application/UserDetails.dart'; import '../../configs/Navigator.dart'; import '../../designs/Component.dart'; +import '../../designs/Portrait.dart'; import '../../designs/Responsive.dart'; import '../../designs/Shell.dart'; import '../../designs/buttons/Buttons.dart'; import '../../designs/input/InputFields.dart'; import '../../designs/text/Text.dart'; import '../../utils/Colors.dart'; +import '../../utils/PhotoPicker.dart'; import '../../utils/Thresholds.dart'; import '../../utils/Validators.dart'; import '../login/Login.dart'; @@ -59,9 +61,19 @@ class SettingsState extends State implements ConnectSettings { Navigator.pop(context); } + void _onChangePhoto() async { + final String? pic = await PhotoPicker.pickPortrait(); + if (pic != null) { + _model?.savePhoto(pic); + } + } + + void _onRemovePhoto() { + _model?.savePhoto(""); + } + void _onOpenTargets() { - GroundedNavigation() - .navigateToPageWithData(const TargetSetup(), context); + GroundedNavigation().navigateToPageWithData(const TargetSetup(), context); } void _onToneSelected(ToneLevel tone) { @@ -184,6 +196,50 @@ class SettingsState extends State implements ConnectSettings { crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ + sectionBreak("Profile photo", caption: "kept on this phone"), + card( + child: Row( + children: [ + portrait(_user.pic, _user.name, 64), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + text(_user.name.isEmpty ? "You" : _user.name, 16, + TextType.Medium, + color: colorPrimaryDark), + const SizedBox(height: 8), + Row( + children: [ + GestureDetector( + onTap: _onChangePhoto, + child: text( + _user.pic.isEmpty + ? "Add a photo" + : "Change photo", + 13, + TextType.Medium, + color: colorAccent), + ), + if (_user.pic.isNotEmpty) ...[ + const SizedBox(width: 18), + GestureDetector( + onTap: _onRemovePhoto, + child: text("Remove", 13, TextType.Regular, + color: colorGrey2), + ), + ], + ], + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 28), sectionBreak("Tone", caption: "how it speaks to you"), segmentedSelector( options: ToneLevel.values, @@ -270,8 +326,7 @@ class SettingsState extends State implements ConnectSettings { ], ), ), - Icon(CupertinoIcons.chevron_right, - size: 18, color: colorGrey2), + Icon(CupertinoIcons.chevron_right, size: 18, color: colorGrey2), ], ), ), @@ -327,8 +382,8 @@ class SettingsState extends State implements ConnectSettings { ), const SizedBox(height: 20), Center( - child: text("Grounded 1.0.0", 11, TextType.Regular, - color: colorGrey), + child: + text("Grounded 1.0.0", 11, TextType.Regular, color: colorGrey), ), ], ), diff --git a/frontend/lib/Grounded/see/settings/ViewSettings.dart b/frontend/lib/Grounded/see/settings/ViewSettings.dart index 40d07c8..7294ea4 100644 --- a/frontend/lib/Grounded/see/settings/ViewSettings.dart +++ b/frontend/lib/Grounded/see/settings/ViewSettings.dart @@ -24,6 +24,14 @@ class ViewSettings extends ParentViewModel { ); } + /// A new profile photo (or "" to remove it), kept on the device. + void savePhoto(String pic) async { + final UserDetails details = await getDataManager().getUserDetails(); + details.pic = pic; + await getDataManager().setUserDetails(details); + initialise(); + } + /// The tone slider is capped: it changes register, never cruelty. void changeTone(ToneLevel tone) async { if (!await hasNetwork(() => changeTone(tone))) return; @@ -66,8 +74,8 @@ class ViewSettings extends ParentViewModel { connection.onSickModeChanged(details); } catch (e) { - handleError(e, () => setSickMode(enabled, reason), () => dismissError(), - "Retry"); + handleError( + e, () => setSickMode(enabled, reason), () => dismissError(), "Retry"); } } diff --git a/frontend/lib/Grounded/utils/PhotoPicker.dart b/frontend/lib/Grounded/utils/PhotoPicker.dart new file mode 100644 index 0000000..509527e --- /dev/null +++ b/frontend/lib/Grounded/utils/PhotoPicker.dart @@ -0,0 +1,39 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:file_picker/file_picker.dart'; + +/// Picks a photo from the device and returns it as a small PNG data URI, +/// ready to store as a profile `pic`. Null when nothing was picked or the +/// file was not an image. +class PhotoPicker { + /// Long edge of the stored portrait. Big enough for the largest avatar on + /// a 3x screen, small enough to keep in local storage. + static const int _edge = 256; + + static Future pickPortrait() async { + final FilePickerResult? result = + await FilePicker.pickFiles(type: FileType.image, withData: true); + final Uint8List? bytes = result == null || result.files.isEmpty + ? null + : result.files.first.bytes; + if (bytes == null) { + return null; + } + + try { + final ui.Codec codec = await ui.instantiateImageCodec(bytes, + targetWidth: _edge, targetHeight: null); + final ui.FrameInfo frame = await codec.getNextFrame(); + final ByteData? png = + await frame.image.toByteData(format: ui.ImageByteFormat.png); + if (png == null) { + return null; + } + return "data:image/png;base64,${base64Encode(png.buffer.asUint8List())}"; + } catch (e) { + return null; + } + } +}