From 697348c780e0d7c93be60bdf5fedcc89bd37bef7 Mon Sep 17 00:00:00 2001 From: Rainxch Zed Date: Mon, 5 Jan 2026 15:27:45 +0500 Subject: [PATCH 01/10] feat(details): Add favorite button and feedback Adds a favorite/unfavorite button to the details screen TopAppBar, allowing users to add or remove a repository from their favorites. A snackbar message is now shown to confirm the action, with localized strings for "added to favorites" and "removed from favorites". The `isFavorite` state is now fetched earlier to improve UI responsiveness. --- .../baselineProfiles/0/composeApp-release.dm | Bin 14153 -> 14172 bytes .../baselineProfiles/1/composeApp-release.dm | Bin 14142 -> 14152 bytes .../composeResources/values-bn/strings-bn.xml | 3 + .../composeResources/values-es/strings-es.xml | 3 + .../composeResources/values-fr/strings-fr.xml | 3 + .../composeResources/values-it/strings-it.xml | 6 +- .../composeResources/values-ja/strings-ja.xml | 3 + .../composeResources/values-kr/strings-kr.xml | 3 + .../composeResources/values-ru/strings-ru.xml | 3 + .../values-zh-rCN/strings-zh-rCN.xml | 3 + .../composeResources/values/strings.xml | 3 + .../details/presentation/DetailsAction.kt | 2 +- .../details/presentation/DetailsRoot.kt | 136 ++++++++++++------ .../details/presentation/DetailsViewModel.kt | 81 +++++++---- 14 files changed, 169 insertions(+), 80 deletions(-) diff --git a/composeApp/release/baselineProfiles/0/composeApp-release.dm b/composeApp/release/baselineProfiles/0/composeApp-release.dm index 43c318579c6423161c6aca9dbc27c11f656841cf..170df483740c7b84e6717135270fd1a56ef7ef3f 100644 GIT binary patch literal 14172 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 Pgofhr*PY_|C-lDn4XQav literal 14153 zcmaKT1yo$kwk8%Df&~Zxf^=|qZL9+X4-niv1oz<5I3c*Zy9aj*5Zv9hk;b)gCjWi! zt$Sx?%~Y*gwNKTqBfEC(@0>d8`yz*o@)iLN4Glp}F-r&GKN99&+1}B@#>ml~)!xzW z8zPv!Yy7W(z|O&gfc)QzLVs(X|CW*d)((G5?JEQXUq*y~=}@C0Advi(?dK5@kPs?E z!UI8nkMf@p5HJu-tc{$UOr2Q2nz}KHozDwVA|QBnBOqY@PY35eJ#L7{+H#Rp5^u2P z`@b(QOi!;)i1`NNMxbJeeHcXw?PJzN5-Z0AQaB)}moHTBpq6j4TJt00(NXAkctnmk z64H_UvS4aS)|RQA)@C*g zRWrR(IuE2CGH1)h5ex?ucP{nxp7w{E|HN_rV)bMM=>mU=Jb0$cZYl#TfW>`$Rb2B* z(+R|p9TWXIe3fWK~|CBwak9uARV?$f0^1&tQ-GD6+`eKWm@c9rw?f=8t-g z82-Guv=L6FN$~0@(DJd0a@uCks}Iuv9=|D&S|#x!^b9Gk6=}HFUt?CXv}Q7}jGA}p zALY5cXku|}h-pVZ?Ayt@RY1zbkwB`WxX-EM_EM3#dvc?HebZ4f&-p-)v|+PWJ%cFyCaS z%8tqL(qUusrcPk^L$IGp5%J)uSyXqK1I)U?54ktX}bH#LuZ88Nvu| z??{`=_!y`EMUzpZ4X>W9HS2!)#s?;xJw1G;M&p5g`l~FfkXJTSWISxzYh5LMr$;uc ztfjo9!*gspYe{A6ald2dAVO6I+yz*Z%?0{(o$C~|R-TiD+IWMjqcGSUqp<6RW+eMq zqMT*&-ks1cRuaik0Bp_~g5E3iwa8Nv@JX5a>zkM63w@nuXm%bFKK?a0`9x$nrB2>- zNHg%pk8UH{w*^WutCzxoHJQ?hJS%G7dSb3TFHy&v zh>-i`BtwOfeDr}*9!elfJ#gp4&4k#qC!7~a=@|XBoXJ)HYWQ#6-z%>zW`5ai`f%*8 z;G%)o7;N}}eMXTlYUk!SQL|5z=Q71fv@MJr3zjJAyjdf|&o8~Bg6M=BF4c$TR9UT5)TeTQc4a6BSD)s1W8+9aE z$T!*A6{7)t={(iBd}8-AnI20t?Q+W4g8cI6AvW3lytSg{-jQm3`IwDmBBI;h1=m%t z&g2oN&Po`hVgP*5BwMVfAo3>I1l^6|bz@Th*`Ix*!v_N8i3{b$X^>elX=^MSMwY$$ zGpLsPXrAwedGL|F$K7*RDc$v4JpbHIE^W=MUJ4}j5qyn*TLI^?eSOEGT$uxZQ>UO| z$9pp;UL-7+x3eU-uVU`#^^sh-OMgPxzXNGo;l?=$QF*)LIbq0q{SPbw!ocn-ccqs| zftA|vrG$*IE$(d?9)?K(pJZ~JSsCB8#c$nvwzYoq9xX;aH!pKV1FeiAHoXZ)O({q{)|+br$PqScYs9rN44k5POk(eDyKI)ha( zbM(%7sbs;}M=yDi$*MLiFpc6NYpljiO43BlP~NqT7$V-Wd8Wy(#?4G+997OZ+9~GU0dH2C~j}G}>>Y_^((*`{Q#`1k{WVgrDTu7ygE9Z;W`- zK6l&e5p!2+wk0DPLqsjE*f-3luNXf3GSvBCea`-l2pD43|DGN7XglPG`@k+o1pw{A zF^06i0TYFVG!myf&^P1O-q@h*6}3I7^#Ls(?-=OQu*#clb5R$UAob z`c;6XSj3Q4fm&r60kvrw2fXy4Jy%BOyIJ`|1Y*9QOrjAimy!I?n zwyyp31>Sbbs{<6CQJ@!`y|EU9CdgjtmaKVLFESYwp{5YP?1!_R)l=0V8Xx)(9rZ6$ z82BpDUW!Yw8n!?eo{fb&HDTPXpme)a|Y-mAh?Q0t+-gAtza(*&A zn)YqED#4$0-#iOT_)F)&pASy1;`pAPgz-j8Rq`8tqJT%VDdV4LXmCfOEkyzY#mB3z zhjj_{pXyeJk&q;gDP$SOnH+csateNLWiOD3O_1GxK0x(hoCMI=35P_o#le!L2A^7h zZuA7B_6hi!eAH;=hPuc{XiLf#f{JzuB@;Y? z>lr1zpBye{MGO=j2RK)jx3n_pkf4iNQ;^<;L4Te%0_JuW+oC2 zu^Ndt7sc%RrWn%?u5LOtmS3(<(377A=wX;!pPFXUGBNzE-8%t7Qg8vm!2J2c3k#%d zuM?KUpFs%{lM)m5K@@9`2bbz|cQb31;FA`b)~1c)r3t>}`Ydw?-WlkQO(vaUdK2$( z&xg#W$Dc3x1c_}}Q-%P7>KsVct@X!d5?(@7s7&#ZWyFm0*Uj#^KGAmj5PFBy0!pf0 zScHftqieU7N>uckkd=fp-KfEnu44t-0Yv@Nacz8A8OE?D)P3>rf$gwJ(d)yHhqH6{ zUscWhhv&=qJv^j8rg1{1Vzrp%ecNP#nDdpATXBpkC*gEs${x!qA*~=HB!M6zeM|;nn za+2YRf)_O)6%f50+YL1+XisKbn)a_nZgX0-J~!*1!*T9(+zI8p(~qtl`yX0xFsa&_ae~b&FXoy4@$>n#i1B;4$xrr*=)-ijxSwAP=VB9wc;$zq z+D{GY1%HX$j#y$oEk71|q?O6c?#uO^u(f2FtIL>XWvUC8a-ejig3;kHv`grLfKdM;oVzey3kMXN+y$;h40^#RGeap1!u0_th%^;H}YHe!Yh zd0op4&<1F4(v5p+2+^Kck)Ks8w?U5}*KG!`IDhtx(2+LcFi~8Z9xx89>Uik2tY5S% z|CXj6$ft1aVKrM8-t>_E)e*;ju@g(tBtozD@|Fgxg4*^I*p&4R&|0h1C`| zDQJC|DW}`7g1PXaEZ0jv^{kP3Pmd2kj^eXkyn>2sFMBl7LmonyrR&Q;9|!mGRS!G} z`zi`r$`vu5CtMMk20o5fTK}f#a>^9I;`(Tp^czV$pFp+7wMc_s+^n)Xy<%5RP*_Uq zATU8@a0Teutl;F3lSmq#l4RU$4`->lduux9_~Z4Tm_ixaT8i{Jf}1^Xvj14(LY(btYF4HJJ*P$MtV=ge(2K;`-xZS_ z9m81KOB8~xA9{!g?_<5?eGRI6fmP?3_6aZ}{}M~?iMB)l z5a^it;84J8o$LTVy0I{WY+9upb7=tcW~NsBRaAB2hDrcM{EA*qw0o9dky=&u)MYPG zF@fYwkM-o^a7*7w-;!UP?I3kC0O&6H>SGrz<3m83R4 zj#>R&|87BEtnoXpk5W?|TNHv>aw8z|l4GY?V>+X-j(1FS!S+W^!&hCZ_s^` zn-M#1iQBv1;&KFS__4aVVNWi30-isUCI1-vP6O)GPgt#r7BBItR%L8566|r-j~ZZz z|HWw#;zmX*m+db^c&|a8rP{yJk9n7!?}a;^>pyRuU4Il?(OGGX|8u9%Q0PDNgd5JI z)FNZPY>-vTpR_OW#eX4+i1QnLC6ZoQV_maV<4i?W-6QE}Pg>!-N}x4Xy9w5}9y$po zA;U$IgVgG^pUj}51*Zt6c;Q!8BJ>0yrBz_t7Ta^EGnQ8HuvT+=`LF|5W(G~D6>^xi zgEpmxhMW1sX*Ee~6)m8~LR8E3wusrYHbCp;^TujMBs?N zKI{u7HApoz<1Fe{FUMO3?mkQyMJ%SOJ_S7dN$-CaqlOB77ji!=*FQYygo~W4PBuEb zX7M#yCe2>lv#GwU&IV$3hnD1Uc(yTTHvP_y=TOH~bf8zjt-d@uik6aWRKvPDU1U%c zgKp(sa~_ei2^B4eHbwKzNfh5gmZN{@wrzALOw9pYJC*(bE6UAv8k$tfwU4RX0<{$C zCH}s){H|P7d_Dn+@pM1z3J3XzPm9(SrRNl;HWeNX2@xKoavnWbe!9;ue8-b19sIdj z#p6V-6kcrBPPfXojJSz;ZuycZ#cMrsW1DGQas#g!(h^d`o~asazuZrnUYX7#$&&e2 zIH2-G7*#8QcmsbF4qWoIMDwYuWwsJg{N#*n1;Nql2$a-Ucj3e!ZarIdNxFwqe=>xlks^EzvEgAm#Q&=Bd-}Y96vWmOH~RI?S8! z2-k2Xsz`uC`?Kbt^YcK*nyJ?d%-c%(V+Sj(cLNtrOsfp~Zs9IpRi}Pk8*qEy(^{*N z)vA}sy_}R>_s>rOJ5)XDCr-x`v>LyltxFM0GNf-wW6Cb)cTR-cbCt~-&`6KJQ=FuR zz4iz8hCl{4$-@d(VqE3C93;L#81T^WZJLnVX^A-S|Ae6fB93xeFAn}i>}SwHq}IGlPcR+R|~w9k33?e3n~eM5b|m1&0BE)&3Ci*<#W z#GS$Va%fmr>Lw4nQ`+ieT00!(PF>5Mmr-v#z#W?-2QXpGtqmCZwtivA7F5wq-Htaq z_O3%>^u2VZT-$oNy=w;hY~Xn{k3IWWY!uW1418rLVQ5-%Q2*OMZ*M)|kM`Xnb{h7Q z2lpu3&oQHeUL{(S%y8?M5exMz^%bb%dq=*{H=PUmZ4%O#zhgqsCm2w;vS`^Wi4>L$ z$yQCXqUD4%G&?U>U+NVy+~TxT^eVOQrD!?F#yI>c(y?$)b1?_1j;*3JQa z_>%R^++j?#`-!XfXN;ayJaWZ}@VkKSDAl%0V?DZZ8CQeDAu)EZ_FZ4iHHySgAG1Xa z#nfcWzNt6i=VvXSDYthxEoBsGY7*60?*u$+n6ZP)dO^4Q)^t{<&~zX7HZ zvFUb(g3mQOGO_LB;9PCUpS|3J}54ij4P1G215=uQL0UYeWggnEya40=MNK)4^^7Y-K{;# zxp4uUPp>=Bgye%TQpn}T3cYMOrm#<(J3YkpT?PFwv%qmideACK2`f>9wx(A4M=s^A zE+PwmQ7UMLxflOwSzlqb<)u>2bNfY~{mOaC9_c9SRhba=t(?-*&!m=^Fa5%#AP*AI z&ZBd(iM%wC(SeGd3hVSGz7=XN}VZgaLB$)kdf_0(`dnZ1E z^ZX2F$ehV&W|@|Kf^!mkF6NN^tUJk9>750l6frMC$jt%wMh5|Gr<1utK$ni0>)j?1#9har)(3ybpFWP1&9xVx{*b3?o|?`?Az zzaDXT8yk1^`i3InBLhq@?f>p0E)#~IJx4fGLKO>nmY-^F3swiNUEW02Gt5{0{XVNU zT5kT9K{D4|NW*fNy?sy2U*72_$TOmZipf8PC<(gW*~ruLAfd-$FF&>bWYO$~ux9`o ze4`jF0nUh#$mCmt^OV28h)~3fF1nF_z%r!@{t_%)hYs40MvmhA?u*^*_0*(^is?2` zNH%b;i?mSw1{+kojTZ3Tx7ylq8v`Gi7WFe`G7&q6;s4zBKMkbm99^~DmQUk*OZ@Le z%KmJYp7%}8BMH)6Fb{hNX}gKyo7r8ZXeaeK>HW4mC$zvR6dJ4Ycxc|s>CyZnm(Hv) z1Xf3ur_o0Su64$ZN@WoQ7&+B9S$;~(-G*>;Rx6_U(2X3%IG4N$cs`1T!`^>ouJS7+ zC&5`}Yw@{n=xm2{;mpo?CwK+&9E~_o?MSjE%z>l&~+s8lI}mex*e@92EwkD;vqt?*)@w+p2CQ2 zqr6EF_guJQxZ?k1|M$emg3#{ln2T`cZNQCR#(3b!5<~8j|`gpL%xnoUUoBoDR}C@yZ?B+)P^f1m|}SSh&uGi zE=eQ{$L!Do3)X}DMEc&RcOjA_>pETP()A$ZFllm&X^~sMvtHxyQOl0>pmPwco#h9v zgfM6THR{JM>l@Mpa@}d3Ejw%RL#Az_V97_=FlQ<(va3oire;$?80GfiJx%@0_q=+GVOXGW{i?v`138^7|<;l(~oNX8Qx{yYTh? zwNpS%)V1?>VfYR5B3+@)x6dj6^37}xjx){MBe%qztTdZXfQy6PP_m zL=@KEQXQ>j<@sK8p8f%+(0fnvSe_^M<&U^zyA{dPolavTk1p52uP3ds2r=-Bc+f)_ zmn4N1q(N(bwzwK6B_h6HnLqVHdfnuQ)o|WJ34~bDqjh_X-TCn=WB=W|o6F9!KQ$~p zcjxejlBoWg2+0O59AG(zZ!Oi)DV0&CD7jvl{c{N%B5ZZd0PUaD&<6BPM!r)&CS^Gx z=hF5LULO&R2=}u*Cn+kLal<6w9QO^9s8=_n8 zH#VuHOMaKYp}XvOV#4@t(yOk`5$n`^UKx;#?>LUrh6UXQKN}L}3rp^>DDn5`zFNCX z7ExZe4?xb_XDq{Fu9&FP8QjS9&Pfi%p_$>@#$wV89MdI9fteIvI#SaG*>xnJnqUCV zy4sAp=A5b6yCr1O!{O=AvC16pA8Z#esH_i)4Nd1k^`{gEP`^EC>={Sp_c=H+~Mp@*q_Ko;6j)pgx7%h4Pw2 zN)TU3chzFLXT*Njp}?^BwnYW+^svzfpqdULhC7IpI&M+Cyp^`s_ zoxjfI8xq+=6C*XgJtr;T14+7jCOno%GelnU)wT2FS(YA8cN|c3;~_>R1adtLvuPr$ zO{pD#85TPeZfTJob&OH^X^wJ6Cqb|6X_rR#G{W(F*|&)ri8f7A5 zpGCd)z3AucZ{tn8U>e+As0u8?VcP5`(GOc+=xnKQ_@xqv!0lAuv2pqq*7lGWzL%N) z4e(uZo?c0iG2($?(Pe)|b5WmAVeB*?qfswe@=PosBdIpka`Q?Rqwp-(ciJR7t>Ol(@{$nJJIKJCyV zSOV)VTUCd7Dkqprnt}9QS^;O8To=CP`w_X1~Yxc`e#3Nh>FqtRQb*37zL82E(+hG56 zYlh0INx0VIYeb@YR94JMW8@ra&w5jG`Fzs#m3#Z?b<{VopzTXW^-%hD@tp9khqu#* z>8uG~uGrAstdyR*!QR2!Q7(KI^>AU@ceL7=Z7gh7QZ^p42ujH%Bw7E)yZ?wL_U!## z862MA6}t3rpTWL5GwS2w>x{*i0bZeq3@1x(#Ye*n&lOOY-<(gT$>%zkW42aWWuW4m zrzF)luVq5tdWQL@l`+eSr%K!g)mVCZN;;#_zwosqGIn5CA0DY>rVac z$+D}AwwBseeSNoGdt)FBBirU!)?48jbbqR|uAC2_Bhf3XVYACWoITqG;Dk?ZH~r6+ zd6QfYyX4b^g>3&9-2OisHVh;E=%nl=;lBJwhBqk|tp||*-vsJllCnKu-(ur+U<{p7 zN_aPRcF?__)sfSC>Exeai&4ce;7Qf)0LmDg>)l#jVA@`Jb543|T7;`Uux`3FFr0Lf z-+3!N7Qx*X)CWWAOd&q=@lSso6i6&bae#C{Y_s*}N86s~`?1C~TCWhDPC86Nm7Y7M zpYzTV4%=EGdeT?!1)iF3BToZVFLCW#z=YCd+h?pYL?pLt3Om_%(bTJebVc*H$TT$5 zM}T|9le>?!;ELTB#Xy&cncy(WCq8A+j-cAkt8W2L;CfSBZY|eQsU<%GtR94?qj!7B=&z)96;ofTbh{)!&se>a8l0!3AIM5;TU? zha5OdEpmpwE+yO-COBQ(44KT{^!8+wlno6g{H}U8aP7a|U@F>|LOE{dZ;xlnqQH;X zvSlXW#X2PRH_(_k8|mpC%WfJKxg8`j$MHOk1Tv2#X$?)%U$|(m7-Ja74gxZqp1wmr zJI`{+k43#JAj9y~mgr<;Dwt}RD1*vtdSSXQPAix85RYAL=5wRi5f*G%2zfNyG$*O* zmw@RPF=QV6l7~S_cOnjFi%OQCh)k4lvC8bCr13^`hOiUTOP9tQ@MI;$u4;HbDlp}l zKU`;{hpyizw7qEE#Hg^9^vJ%UFo1}c{9IrN=&=q*&f`WYZ$Q94gnFsGLD3xBSq zJk=E3_Gyt&J!crPQ5wp;9y7ceuFdLekH){sI6CR8$)@KA2x!sAW;kM3L}|*o)cTS2 zX>mQXjGdJl#f&+2s;u+jtK##0WPz2sfkWu}PpPQ1LPxf-RR2Z8?T|*c42~C+gB@GG z^Sr7m)#f6*R8ODN_G1#o`{4?>R;h|P`Ex;m0z~nVOWeos7%&f7EQ01N>v2{6>hEB` zYrhove#>U?qxnu|o?4GpVXb4QI*%x&R2J9C1QRY=k-uuD6?ORPRWhu6f6^j zim?^d+l<;dMr7FYwP~)-NyHS=!rT;r=mg1(1rQmP)l6HZ8U7M=18;JR>lTNWt z{2}cOw-#rx;h+<`smUq7uIE?fG0qo=eE?`mNuhSt=XsZOI_2!(pybrqC$bQrm%F_5dNo5wqz9i? zhHfXMDvA>iNs*XLjdy<~O=~3V#FlD8gFS2b-8Z0a%o0v! zQg4;@foDQ`UqCgL+K_`_4ZylIalI`cqKo6qFW82k z%TCh560yjqez=pOuG){mfbBkW3U)dg1VHrFhH`q>Ho$9f<7ID`Yz(LL&xH~XpS%Aj zLZt%IGcv&r>Ukh``>}t;C@B46OXY$7erZk6*AXOD+eu1>dIe_}oyhGW%QlII@;Iw^ znq!+Of_wQ681rm`#S70KE7%eIGA zYbPlFtVW9{ZzWX7Sb98!(WcBh#j#OwbYS{uqHN)#s07ta|0{}kcZz5rnZSl)8ac}# z!G#2TUzH(HW>iBYm=pV*xH?w6arFPJ`4+ zUZ2As@k@2OH_T%)*iCzAcbm00k4~2oQo_QQ>sc+&XEu$6P_Ov}o80&zld`<0w2Ebe zQ$@bfNx_+SpCab}#wM-;(7h@2_xsBldvphnU1=IT3Es6abzTw6cG7u|3#(3Vv$GeM zSEQbMPs8FDd?rWPdyrSRfW^@Fg5(-IVUdUyuqA?ZAD5jePMK-;0=s zhxT4*Sf!n#deIDlm4Jx-Jm%hog#2Vf{Pz!zJF?%GiwOK;f(bYO^;>~`2H7sC$8#W_ zBfjq4k8~uBjw6>859m-3HSx#_C(T|`saF`y24Tek-#B{AapF=rvJ_Wk zkd{4b^F-=i=T7t+(c{Ip`fA-{kn`6?iN>qzQscwRdm+Q!?V~AD=qb!?tVk&VIMAIL z<;6MHu^5JWP2uTh`#&|dbvx3(6Xvx(eye)t-8Zb9hSw~<3aQOJ7wxb3EObKn%me~o zo14q*rjF~`86ciXk*<`aX8QD|FgEbd-((`hkW9;|!HQ3EO>0mN)mGP)PledHv`zpw zbtKh3(KO0i+_OIC9y%py_{(aSbK-Gw0xE|-(^rsPQ_gX;1IL(zP8pG%AusT19%rK~ z-8>_cQNdJl&^qRqi#t#6W3|Bhpl3A}pQN1~P0fNzpW(YU$jz|Cg5GYvh8l_|MN!+ z#z~5EEm5#0jv>Vlj)=e{#%Pm$W7ZFkA4hQE)`YYu@#rRGK87iE^}Erp^mpRR4kw`d z0#`K(0h~vZrntZ$BYhfd_qXE$&efHd+2MdC`f%rq4`zZXai}2YefG0x@ay?Qh>GaT z%4_?K#z|9a^^df>B0Fx`=JD9~VsL`tUXFQZyx??GGJ}%xX_$9m57YS+>u$n+uKm(L z`YnHk1N)Se(<=6b#P&NU-MWnYh#_$@{x;d1?cA$k6QNkzpZyvrdF)e?m-VUX^d}#y zO!^SO2OVMtIXKQuk)4|MvyWoh1N@?lv*uDi!)}C~mC2WUo006}5zSJZGhCWD&+B)U z$dp9Lq6jncZYAfPy_FU(Ehlgw+h1R`y$Fhp^L*wZYog11zuBsiY5iTN^!)HU^R_Tj zVhWfpgL&Ed`A{wyS>YXuy|1?@-q1R>Q+f4S<{N8ql8-n-uyDFMV&^s2GqHE};ovhv z!3-yIbFE1 z9ttK#I|s|$*~%1m=g)oRf#UL}Rlr z__MM30-}uf-&r=M&kkLfaUDrrF}OUTJH+#yDI$)*Eg5J@mIx+o&2=BEf=qHGhYySL-7qE54#S)o6AN)_#{C-V*$R$yndjrh_2Jd zvxdcgBJPTVzh%gK7M8JQaySDFNoPd|HX*X$T~W9R=-)a`h?)BoPh zMcTjO*DH}+(usr61_V@rvxy|%?)7kojpz>#nlkaoLK{~`A@P&%u5iq=(>OOSS!JRz z-uDup5z4eEDCKVUgcsAi8-6Bw zN%C6pIolr<+xO*W4gm3!X4xT_Gb!eTj!S`@yB_V>9vMkqb$&HQ0b^3+Q#~%!GZy&n zImnL#=y}Ym`dj=jTl%6|`}fiSqFu2$(P7SGf3HZp4RgQ=ayB9+cRn2^XcwJqjPohV zBc6~Ke~dSm+>8VoBVsAIG4YD5>!3l>=0oIh-e;KM`p3i*6&Q8;)j%i}^OE|$7!o=E zBMRDZGFwgv+;}5qR~iZHIA70~!@)$dD)vV9K0Cik4hicz^8<8TP6EzzdLTfypMp@-uJnFRv@B_`Y){Fnu5y|c?OG#G zv^B;sO`+c>TWBfrnr>)g(qT{LmGeE~WAFAF>is3PgS?E8GwhB105O{W|( zqFj*vc=GRm0f3JT$=c{uSiR*_G=b6Ynj_6T?F}A+sLwuR(4zai682VHib5~{;D<wn?FZp!@SeI}AlHBwNw&H8R;FkWuFpwu49pE19lLr*-5&4+n_`OvL z6^y-0kAEzg)+`wyG@%4^nn11`&a)+x6xjLP8~1$sMPvy(4Kajlz=&^ONm4AuN@xK| z(CqJG`@{5opI4b~=S&2hIQj6SYe7C%EJDu`(p9+YEu!-VQOL-oT?6%sI0e8PL15v> zjUg6##y;T&A^>M+G@-^`&?u{oWI1Ks|5*$mFYV(f-a zJVU@$aR9(MeiQ#5=T|cYDSsD5+7kj8QNdHGPA97waTzDLK;EmXM;1h%m^AjS&A=_u#nF%9xWJm+uY8DDj z*v=W+mNcd^+R$Hqsos3KF>``f|xSKxzvs&+^YvfCMF zCYZr=LYe%!8$d74>xHNfqhNcIXt5G&F`?PJ#63m?pWqI}WaD2bU^sPsloPr#i8NjR z4K&9%k>3#CYEQfHHS^2?3Khvx3hz%SG~n7V__c`aiiYrDo^?&KPEc~#egwRSeIjNb zk|IVI8X%O*Wm~LNL__j)=;|b#^6LOIBm1xySXr8%Vi1_`zpN6RMciytDiI}wc02$r z3Zl2a$N|vY<@Ikru_GXW|CTTRPxb-UUn0SOvJY$!5l~<5>>X_o*x5P%QVH_@(g;re z5)J<02aHCNNW5W<5^iukK9XGf_4xHJ>f@%yhkSZveun|Xx71A^u%(p-UODJQyP@kN zoKvRbrn|j9X+yrg=7W}6z^#u=ZS3Ba@vP3yUeC|Yu5t;paK=cjgN1~>z3$fY?r0`@qA%u;Ac_hTIG0upihabp?D2&TW*i*C6bVJ`&(tVeRv^00qwx0^>rSc&Y0#p4F> z!z?h#0a#t1R>NN#2fsJ!U-D73Ev8Y4$S4t;kN3%SSG2M#N_R01`t_ZUKG5{tn4YgaVr<^QHx%jC$O6+H$#q)yE<_6 zv`r!Y^%UcW!+}HSonj(tyvd2xAfvdkS&X%8?;u+zybgLK!UimO;XA1b zDjWWe9Wqoy)+$tRDfBeK?;Y?9NP~l*@!PI*Z;~;DY$-6PkcEv+!Ikyy$b+Ve&oe~F zzS@eUqvA~O=PL7hv#uE-iU?$Z(8rA9bdYQG{a6DlDD6*Ug}aXk-O{bRPW`m2yO3dw zA+$8{T)uS_YEYhR!|FaW;*b_kXj>O@efW?=$uaUV_Oy+(feMm9uO@*-)(dC{_2 z{inCVWwd1Om9VrG5%UlAk7={bwD4(u>}i?!im(Jj=L1>K3tLMr5KT+%lI)c!sUnYT zjmUVppp5gn=|FAPubvqb!NFc)8pV}&&WYSC%Anotk=J(3>0dNn96P%``n6OMboeHF z)O+iup}5#Kx2%z7!~AMP__r%HcqrKYB%M~gBgc}wI`;mk1N;@-u(rC6UMJOwE`juL z=v8&3`dQph2a@G+Kvu=27bo91K|e%zX}E~3EF-(Jzov}M*^|S=Ar&&|9@hIpLpv`^ zB0i-VL8I}$l5;_Z3g_1iJbY&ECqZz-6Q6VMh^aig`2q_iTbaYMQK7!0F6-X(&9-jl(%)&QXFre?9TY3m?w}Qc zzf>F=YubCB#Xb!^1*bg-uQTPWxA*<8L8ui&b*s}jg z_LW@oYWCrt5LWv}Ub2~q?-oOSFH$+TTw>|A!z#Wm zxz2%i*(*YMiN9^xSl-;krZCOxOq}g{A?-)w2^07P^-L1SHfiRbz$?389Tv)q;BwMd zP!@SfwICR4#$%ScBvFXt5O=UxUCkm(E~VAvoXq(Sm7zTG)7x;f_ox# zaU-el&P|)1#zYn0Lt`>sNmk<6kXKCiq%LBWS5H@Eo{|Sc6XDRr$LR<8bAth@(T9s7 zV+VhS;znk2b>W~Yrd~l!BE_|3W3SITBgaNodt+f z8E#x!h#lp4dLDjAwNbA;}vaNlZ?7T<|cZ;LAxN#fbN+5q0{pSM`h(28uigGUCl{y*BPFBjYz}d zOy+yE64>VUf)L-y^(KVFKfNikYrQK%S%KjPc9z7kW?3C@d=N(lpj{|H1y;LHLw6GI zn8ahKlxPBfI?}M9zVUzMO{QoNkgF~Y?}*L1OJj_%n#x>}=ku9B79z!(i>9isTai_s z8t*!1CDrK;vZJ}siL57^{6VxkghPBq(|H<>M?({lHU$f}niAzJ{?ON-<{TNSWkLhJ z(1}hbGV`bs<{GtQj#!t46;BBq2qxA~aeb>!3jFxWD&qB12p{A5GU*!0{m53t91)#K zr1s`yn#?zv7bJ{#Lg@eCN2#61>sNniWe9)E7db?vw+R2{ul*;s{KH@S-_w8M$^SX> kKhOFP_WV2f{KcvN9BK1K?q8Sx3qnW;&VThfc>huUU(sQ;%m4rY diff --git a/composeApp/release/baselineProfiles/1/composeApp-release.dm b/composeApp/release/baselineProfiles/1/composeApp-release.dm index 953ca2022690d59bfe36fa269dd418466100383e..103cca3111fde09785ccf184a6012c2b572211e0 100644 GIT binary patch literal 14152 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 I?Ekp?e>O-CcK`qY literal 14142 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 F{{vqd1S
অন্ধকার সিস্টেম + রিপোজিটরি প্রিয় তালিকায় যোগ করা হয়েছে + রিপোজিটরি প্রিয় তালিকা থেকে সরানো হয়েছে + diff --git a/composeApp/src/commonMain/composeResources/values-es/strings-es.xml b/composeApp/src/commonMain/composeResources/values-es/strings-es.xml index 66e59383f..fea85d28c 100644 --- a/composeApp/src/commonMain/composeResources/values-es/strings-es.xml +++ b/composeApp/src/commonMain/composeResources/values-es/strings-es.xml @@ -194,4 +194,7 @@ Oscuro Sistema + Repositorio añadido a favoritos + Repositorio eliminado de favoritos + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-fr/strings-fr.xml b/composeApp/src/commonMain/composeResources/values-fr/strings-fr.xml index f00f15d66..065173361 100644 --- a/composeApp/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/composeApp/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -194,4 +194,7 @@ Sombre Système + Dépôt ajouté aux favoris + Dépôt supprimé des favoris + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-it/strings-it.xml b/composeApp/src/commonMain/composeResources/values-it/strings-it.xml index d221b057c..a4f8f0afe 100644 --- a/composeApp/src/commonMain/composeResources/values-it/strings-it.xml +++ b/composeApp/src/commonMain/composeResources/values-it/strings-it.xml @@ -242,4 +242,8 @@ Font del sistema Usa il font di sistema per una migliore leggibilità - + + Repository aggiunto ai preferiti + Repository rimosso dai preferiti + + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-ja/strings-ja.xml b/composeApp/src/commonMain/composeResources/values-ja/strings-ja.xml index 509c269d3..fa55d8305 100644 --- a/composeApp/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/composeApp/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -194,4 +194,7 @@ ダーク システム + リポジトリをお気に入りに追加しました + リポジトリをお気に入りから削除しました + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-kr/strings-kr.xml b/composeApp/src/commonMain/composeResources/values-kr/strings-kr.xml index 92da24e57..0c492d7fb 100644 --- a/composeApp/src/commonMain/composeResources/values-kr/strings-kr.xml +++ b/composeApp/src/commonMain/composeResources/values-kr/strings-kr.xml @@ -245,4 +245,7 @@ 다크 시스템 + 리포지토리를 즐겨찾기에 추가했습니다 + 리포지토리를 즐겨찾기에서 제거했습니다 + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-ru/strings-ru.xml b/composeApp/src/commonMain/composeResources/values-ru/strings-ru.xml index 372643681..844dd50de 100644 --- a/composeApp/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/composeApp/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -212,4 +212,7 @@ Тёмная Системная + Репозиторий добавлен в избранное + Репозиторий удалён из избранного + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/composeApp/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 13b6f950b..236c24c6f 100644 --- a/composeApp/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/composeApp/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -195,4 +195,7 @@ 深色 跟随系统 + 已添加到收藏 + 已从收藏中移除 + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index de8ae0533..c7a569873 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -247,4 +247,7 @@ Dark System + Repository added to favourites + Repository removed from favourites + \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsAction.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsAction.kt index 3f51c47ca..b3c5c4a22 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsAction.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsAction.kt @@ -22,7 +22,7 @@ sealed interface DetailsAction { data object OnNavigateBackClick : DetailsAction // NEW ACTIONS - data object ToggleFavorite : DetailsAction + data object OnToggleFavorite : DetailsAction data object CheckForUpdates : DetailsAction data object UpdateApp : DetailsAction } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt index 0516b9d8e..49863172d 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt @@ -3,6 +3,7 @@ package zed.rainxch.githubstore.feature.details.presentation import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -10,6 +11,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CutCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.material.icons.filled.OpenInBrowser import androidx.compose.material3.CircularWavyProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api @@ -31,11 +34,13 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import githubstore.composeapp.generated.resources.Res import githubstore.composeapp.generated.resources.navigate_back import githubstore.composeapp.generated.resources.open_repository +import io.github.fletchmckee.liquid.LiquidState import io.github.fletchmckee.liquid.liquefiable import io.github.fletchmckee.liquid.liquid import io.github.fletchmckee.liquid.rememberLiquidState @@ -118,53 +123,7 @@ fun DetailsScreen( ) { Scaffold( topBar = { - TopAppBar( - title = { }, - navigationIcon = { - IconButton( - shapes = IconButtonDefaults.shapes(), - onClick = { - onAction(DetailsAction.OnNavigateBackClick) - } - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(Res.string.navigate_back), - modifier = Modifier.size(24.dp) - ) - } - }, - actions = { - state.repository?.htmlUrl?.let { - IconButton( - shapes = IconButtonDefaults.shapes(), - onClick = { - onAction(DetailsAction.OpenRepoInBrowser) - }, - ) { - Icon( - imageVector = Icons.Default.OpenInBrowser, - contentDescription = stringResource(Res.string.open_repository), - modifier = Modifier.size(24.dp) - ) - } - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = Color.Transparent - ), - modifier = Modifier.then( - if (isLiquidTopbarEnabled()) { - Modifier.liquid(liquidTopbarState) { - this.shape = CutCornerShape(0.dp) - this.frost = 8.dp - this.curve = .4f - this.refraction = .1f - this.dispersion = .2f - } - } else Modifier - ) - ) + DetailsTopbar(onAction, state, liquidTopbarState) }, containerColor = MaterialTheme.colorScheme.background, modifier = Modifier.liquefiable(liquidTopbarState), @@ -233,6 +192,89 @@ fun DetailsScreen( } } +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun DetailsTopbar( + onAction: (DetailsAction) -> Unit, + state: DetailsState, + liquidTopbarState: LiquidState +) { + TopAppBar( + title = { }, + navigationIcon = { + IconButton( + shapes = IconButtonDefaults.shapes(), + onClick = { + onAction(DetailsAction.OnNavigateBackClick) + } + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(Res.string.navigate_back), + modifier = Modifier.size(24.dp) + ) + } + }, + actions = { + Row ( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (state.repository != null) { + IconButton( + onClick = { + onAction(DetailsAction.OnToggleFavorite) + }, + shapes = IconButtonDefaults.shapes(), + colors = IconButtonDefaults.iconButtonColors( + contentColor = MaterialTheme.colorScheme.onSurface + ) + ) { + Icon( + imageVector = if(state.isFavorite) { + Icons.Default.Favorite + } else Icons.Default.FavoriteBorder, + contentDescription = null, + ) + } + } + + state.repository?.htmlUrl?.let { + IconButton( + shapes = IconButtonDefaults.shapes(), + onClick = { + onAction(DetailsAction.OpenRepoInBrowser) + }, + colors = IconButtonDefaults.iconButtonColors( + contentColor = MaterialTheme.colorScheme.onSurface + ) + ) { + Icon( + imageVector = Icons.Default.OpenInBrowser, + contentDescription = stringResource(Res.string.open_repository), + modifier = Modifier.size(24.dp) + ) + } + } + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.Transparent + ), + modifier = Modifier.then( + if (isLiquidTopbarEnabled()) { + Modifier.liquid(liquidTopbarState) { + this.shape = CutCornerShape(0.dp) + this.frost = 8.dp + this.curve = .4f + this.refraction = .1f + this.dispersion = .2f + } + } else Modifier + ) + ) +} + @Preview @Composable private fun Preview() { diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt index 029d8e0ba..b4fda21f5 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt @@ -4,7 +4,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger import githubstore.composeapp.generated.resources.Res +import githubstore.composeapp.generated.resources.added_to_favourites import githubstore.composeapp.generated.resources.installer_saved_downloads +import githubstore.composeapp.generated.resources.removed_from_favourites import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.async @@ -39,7 +41,6 @@ import zed.rainxch.githubstore.core.data.services.Installer import zed.rainxch.githubstore.core.domain.use_cases.SyncInstalledAppsUseCase import zed.rainxch.githubstore.feature.details.domain.repository.DetailsRepository import zed.rainxch.githubstore.feature.details.presentation.model.LogResult -import java.io.File import kotlin.time.Clock.System import kotlin.time.ExperimentalTime @@ -89,10 +90,19 @@ class DetailsViewModel( } val repo = detailsRepository.getRepositoryById(repositoryId.toLong()) + val isFavoriteDeferred = async { + try { + favoritesRepository.isFavoriteSync(repo.id) + } catch (t: Throwable) { + false + } + } + val isFavorite = isFavoriteDeferred.await() + val owner = repo.owner.login val name = repo.name - _state.value = _state.value.copy(repository = repo) + _state.value = _state.value.copy(repository = repo, isFavorite = isFavorite) val latestReleaseDeferred = async { try { @@ -142,7 +152,8 @@ class DetailsViewModel( if (dbApp != null) { if (dbApp.isPendingInstall && - packageMonitor.isPackageInstalled(dbApp.packageName)) { + packageMonitor.isPackageInstalled(dbApp.packageName) + ) { installedAppsRepository.updatePendingStatus( dbApp.packageName, false @@ -160,14 +171,6 @@ class DetailsViewModel( } } - val isFavoriteDeferred = async { - try { - favoritesRepository.isFavoriteSync(repo.id) - } catch (t: Throwable) { - false - } - } - val isObtainiumEnabled = platform.type == PlatformType.ANDROID val isAppManagerEnabled = platform.type == PlatformType.ANDROID @@ -176,7 +179,6 @@ class DetailsViewModel( val readme = readmeDeferred.await() val userProfile = userProfileDeferred.await() val installedApp = installedAppDeferred.await() - val isFavorite = isFavoriteDeferred.await() val installable = latestRelease?.assets?.filter { asset -> installer.isAssetInstallable(asset.name) @@ -206,7 +208,6 @@ class DetailsViewModel( isAppManagerAvailable = isAppManagerAvailable, isAppManagerEnabled = isAppManagerEnabled, installedApp = installedApp, - isFavorite = isFavorite ) } catch (t: Throwable) { Logger.e { "Details load failed: ${t.message}" } @@ -232,29 +233,35 @@ class DetailsViewModel( if (platform.type == PlatformType.ANDROID) { val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName) if (systemInfo != null) { - installedAppsRepository.updateApp(app.copy( - installedVersionName = systemInfo.versionName, - installedVersionCode = systemInfo.versionCode, - latestVersionName = systemInfo.versionName, - latestVersionCode = systemInfo.versionCode - )) + installedAppsRepository.updateApp( + app.copy( + installedVersionName = systemInfo.versionName, + installedVersionCode = systemInfo.versionCode, + latestVersionName = systemInfo.versionName, + latestVersionCode = systemInfo.versionCode + ) + ) Logger.d { "Migrated ${app.packageName}: set versionName/code from system" } } else { - installedAppsRepository.updateApp(app.copy( + installedAppsRepository.updateApp( + app.copy( + installedVersionName = app.installedVersion, + installedVersionCode = 0L, + latestVersionName = app.installedVersion, + latestVersionCode = 0L + ) + ) + Logger.d { "Migrated ${app.packageName}: fallback to tag" } + } + } else { + installedAppsRepository.updateApp( + app.copy( installedVersionName = app.installedVersion, installedVersionCode = 0L, latestVersionName = app.installedVersion, latestVersionCode = 0L - )) - Logger.d { "Migrated ${app.packageName}: fallback to tag" } - } - } else { - installedAppsRepository.updateApp(app.copy( - installedVersionName = app.installedVersion, - installedVersionCode = 0L, - latestVersionName = app.installedVersion, - latestVersionCode = 0L - )) + ) + ) Logger.d { "Migrated ${app.packageName} (desktop): fallback to tag" } } } @@ -329,7 +336,7 @@ class DetailsViewModel( ) } - DetailsAction.ToggleFavorite -> { + DetailsAction.OnToggleFavorite -> { viewModelScope.launch { try { val repo = _state.value.repository ?: return@launch @@ -354,6 +361,18 @@ class DetailsViewModel( val newFavoriteState = favoritesRepository.isFavoriteSync(repo.id) _state.value = _state.value.copy(isFavorite = newFavoriteState) + _events.send( + element = DetailsEvent.OnMessage( + message = getString( + resource = if (newFavoriteState) { + Res.string.added_to_favourites + } else { + Res.string.removed_from_favourites + } + ) + ) + ) + } catch (t: Throwable) { Logger.e { "Failed to toggle favorite: ${t.message}" } } From 7579c2849ccfe4dec797b85d8e4a6e59ab00263e Mon Sep 17 00:00:00 2001 From: Rainxch Zed Date: Mon, 5 Jan 2026 15:34:49 +0500 Subject: [PATCH 02/10] feat(favourites): Add initial structure for Favourites screen This commit introduces the basic structure for the new "Favourites" feature. It includes: - A new `FavouritesScreen` with its corresponding `FavouritesRoot` composable, `ViewModel`, `State`, and `Action` classes. - Integration of the favourites module and navigation graph entry. - A minor logging improvement in the `DetailsViewModel` for favourite status checks. - Accessibility improvement by adding content descriptions to the favourite toggle icon on the details screen. --- .../githubstore/app/di/SharedModules.kt | 9 ++++- .../rainxch/githubstore/app/di/initKoin.kt | 1 + .../app/navigation/AppNavigation.kt | 5 +++ .../app/navigation/GithubStoreGraph.kt | 3 ++ .../details/presentation/DetailsRoot.kt | 14 +++++-- .../details/presentation/DetailsViewModel.kt | 1 + .../feature/favourites/FavouritesAction.kt | 5 +++ .../feature/favourites/FavouritesRoot.kt | 39 +++++++++++++++++++ .../feature/favourites/FavouritesState.kt | 6 +++ .../feature/favourites/FavouritesViewModel.kt | 34 ++++++++++++++++ 10 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesAction.kt create mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesRoot.kt create mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesState.kt create mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesViewModel.kt diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt index c7de3b6ad..63fb29066 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt @@ -5,7 +5,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import org.koin.core.module.Module import org.koin.core.module.dsl.viewModel -import org.koin.core.qualifier.named import org.koin.dsl.module import zed.rainxch.githubstore.MainViewModel import zed.rainxch.githubstore.app.app_state.AppStateManager @@ -25,7 +24,6 @@ import zed.rainxch.githubstore.feature.apps.domain.repository.AppsRepository import zed.rainxch.githubstore.feature.apps.presentation.AppsViewModel import zed.rainxch.githubstore.network.buildAuthedGitHubHttpClient import zed.rainxch.githubstore.feature.auth.data.repository.AuthenticationRepositoryImpl -import zed.rainxch.githubstore.feature.auth.domain.* import zed.rainxch.githubstore.feature.auth.domain.repository.AuthenticationRepository import zed.rainxch.githubstore.feature.auth.presentation.AuthenticationViewModel import zed.rainxch.githubstore.feature.details.data.repository.DetailsRepositoryImpl @@ -34,6 +32,7 @@ import zed.rainxch.githubstore.feature.details.presentation.DetailsViewModel import zed.rainxch.githubstore.core.data.services.Downloader import zed.rainxch.githubstore.core.data.services.Installer import zed.rainxch.githubstore.core.domain.use_cases.SyncInstalledAppsUseCase +import zed.rainxch.githubstore.feature.favourites.FavouritesViewModel import zed.rainxch.githubstore.feature.home.data.data_source.CachedTrendingDataSource import zed.rainxch.githubstore.feature.home.data.repository.HomeRepositoryImpl import zed.rainxch.githubstore.feature.home.domain.repository.HomeRepository @@ -197,6 +196,12 @@ val searchModule: Module = module { ) } } +val favouritesModule: Module = module { + // ViewModel + viewModel { + FavouritesViewModel() + } +} val detailsModule: Module = module { // Repository diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/initKoin.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/initKoin.kt index 70d021d29..818ed16d4 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/initKoin.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/initKoin.kt @@ -12,6 +12,7 @@ fun initKoin(config: KoinAppDeclaration? = null) { authModule, homeModule, searchModule, + favouritesModule, detailsModule, settingsModule, appsModule diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index 1a70a6319..b7649763e 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -27,6 +27,7 @@ import org.koin.core.parameter.parametersOf import zed.rainxch.githubstore.feature.apps.presentation.AppsRoot import zed.rainxch.githubstore.feature.auth.presentation.AuthenticationRoot import zed.rainxch.githubstore.feature.details.presentation.DetailsRoot +import zed.rainxch.githubstore.feature.favourites.FavouritesRoot import zed.rainxch.githubstore.feature.home.presentation.HomeRoot import zed.rainxch.githubstore.feature.search.presentation.SearchRoot import zed.rainxch.githubstore.feature.settings.presentation.SettingsRoot @@ -112,6 +113,10 @@ fun AppNavigation( ) } + entry { + FavouritesRoot() + } + entry { SettingsRoot( onNavigateBack = { diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt index 87e8bee14..d68ff070f 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt @@ -22,6 +22,9 @@ sealed interface GithubStoreGraph: NavKey { @Serializable data object SettingsScreen : GithubStoreGraph + @Serializable + data object FavouritesScreen : GithubStoreGraph + @Serializable data object AppsScreen : GithubStoreGraph } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt index 49863172d..186f87caa 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt @@ -38,8 +38,10 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import githubstore.composeapp.generated.resources.Res +import githubstore.composeapp.generated.resources.added_to_favourites import githubstore.composeapp.generated.resources.navigate_back import githubstore.composeapp.generated.resources.open_repository +import githubstore.composeapp.generated.resources.removed_from_favourites import io.github.fletchmckee.liquid.LiquidState import io.github.fletchmckee.liquid.liquefiable import io.github.fletchmckee.liquid.liquid @@ -216,7 +218,7 @@ private fun DetailsTopbar( } }, actions = { - Row ( + Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { @@ -231,10 +233,16 @@ private fun DetailsTopbar( ) ) { Icon( - imageVector = if(state.isFavorite) { + imageVector = if (state.isFavorite) { Icons.Default.Favorite } else Icons.Default.FavoriteBorder, - contentDescription = null, + contentDescription = stringResource( + resource = if (state.isFavorite) { + Res.string.added_to_favourites + } else { + Res.string.removed_from_favourites + } + ), ) } } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt index b4fda21f5..814751f25 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt @@ -94,6 +94,7 @@ class DetailsViewModel( try { favoritesRepository.isFavoriteSync(repo.id) } catch (t: Throwable) { + Logger.e { "Failed to load if repo is favourite: ${t.localizedMessage}" } false } } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesAction.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesAction.kt new file mode 100644 index 000000000..770e7f47b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesAction.kt @@ -0,0 +1,5 @@ +package zed.rainxch.githubstore.feature.favourites + +sealed interface FavouritesAction { + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesRoot.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesRoot.kt new file mode 100644 index 000000000..a3b47c845 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesRoot.kt @@ -0,0 +1,39 @@ +package zed.rainxch.githubstore.feature.favourites + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.githubstore.core.presentation.theme.GithubStoreTheme + +@Composable +fun FavouritesRoot( + viewModel: FavouritesViewModel = koinViewModel() +) { + val state by viewModel.state.collectAsStateWithLifecycle() + + FavouritesScreen( + state = state, + onAction = viewModel::onAction + ) +} + +@Composable +fun FavouritesScreen( + state: FavouritesState, + onAction: (FavouritesAction) -> Unit, +) { + +} + +@Preview +@Composable +private fun Preview() { + GithubStoreTheme { + FavouritesScreen( + state = FavouritesState(), + onAction = {} + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesState.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesState.kt new file mode 100644 index 000000000..f9c9b9e56 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesState.kt @@ -0,0 +1,6 @@ +package zed.rainxch.githubstore.feature.favourites + +data class FavouritesState( + val paramOne: String = "default", + val paramTwo: List = emptyList(), +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesViewModel.kt new file mode 100644 index 000000000..11e41eb9c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesViewModel.kt @@ -0,0 +1,34 @@ +package zed.rainxch.githubstore.feature.favourites + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +class FavouritesViewModel : ViewModel() { + + private var hasLoadedInitialData = false + + private val _state = MutableStateFlow(FavouritesState()) + val state = _state + .onStart { + if (!hasLoadedInitialData) { + /** Load initial data here **/ + hasLoadedInitialData = true + } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000L), + initialValue = FavouritesState() + ) + + fun onAction(action: FavouritesAction) { + when (action) { + else -> TODO("Handle actions") + } + } + +} \ No newline at end of file From f23b0d0e9448a72d980041906cc33c2596065343 Mon Sep 17 00:00:00 2001 From: Rainxch Zed Date: Mon, 5 Jan 2026 15:46:58 +0500 Subject: [PATCH 03/10] refactor(home, search): Unify repository state with `DiscoveryRepository` This commit introduces a new data class, `DiscoveryRepository`, to create a single source of truth for representing repositories across different features. Key changes: - Replaced feature-specific data classes (`HomeRepo`, `SearchRepo`) with the unified `DiscoveryRepository`. - Updated `RepositoryCard` to accept a `DiscoveryRepository` object, simplifying its signature. - Refactored `HomeViewModel` and `SearchViewModel` to use `DiscoveryRepository` and incorporate favorite status. --- .../githubstore/app/di/SharedModules.kt | 6 +- .../presentation/components/RepositoryCard.kt | 70 ++++++++++--------- .../presentation/model/DiscoveryRepository.kt | 10 +++ .../feature/home/presentation/HomeRoot.kt | 8 +-- .../feature/home/presentation/HomeState.kt | 9 +-- .../home/presentation/HomeViewModel.kt | 27 ++++--- .../feature/search/presentation/SearchRoot.kt | 14 ++-- .../search/presentation/SearchState.kt | 10 +-- .../search/presentation/SearchViewModel.kt | 40 ++++++----- 9 files changed, 101 insertions(+), 93 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/model/DiscoveryRepository.kt diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt index 63fb29066..aea5754df 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt @@ -172,7 +172,8 @@ val homeModule: Module = module { homeRepository = get(), installedAppsRepository = get(), platform = get(), - syncInstalledAppsUseCase = get() + syncInstalledAppsUseCase = get(), + favoritesRepository = get() ) } } @@ -192,7 +193,8 @@ val searchModule: Module = module { SearchViewModel( searchRepository = get(), installedAppsRepository = get(), - syncInstalledAppsUseCase = get() + syncInstalledAppsUseCase = get(), + favoritesRepository = get() ) } } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/components/RepositoryCard.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/components/RepositoryCard.kt index 072156080..b305c555e 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/components/RepositoryCard.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/components/RepositoryCard.kt @@ -46,15 +46,14 @@ import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview import zed.rainxch.githubstore.core.domain.model.GithubRepoSummary import zed.rainxch.githubstore.core.domain.model.GithubUser +import zed.rainxch.githubstore.core.presentation.model.DiscoveryRepository import zed.rainxch.githubstore.core.presentation.theme.GithubStoreTheme import zed.rainxch.githubstore.core.presentation.utils.formatUpdatedAt @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun RepositoryCard( - repository: GithubRepoSummary, - isInstalled: Boolean, - isUpdateAvailable: Boolean, + discoveryRepository: DiscoveryRepository, onClick: () -> Unit, modifier: Modifier = Modifier ) { @@ -77,7 +76,7 @@ fun RepositoryCard( horizontalArrangement = Arrangement.spacedBy(12.dp) ) { CoilImage( - imageModel = { repository.owner.avatarUrl }, + imageModel = { discoveryRepository.repository.owner.avatarUrl }, modifier = Modifier .size(32.dp) .clip(CircleShape), @@ -95,7 +94,7 @@ fun RepositoryCard( ) Text( - text = repository.owner.login, + text = discoveryRepository.repository.owner.login, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.outline, maxLines = 1, @@ -104,7 +103,7 @@ fun RepositoryCard( ) Text( - text = "/ ${repository.name}", + text = "/ ${discoveryRepository.repository.name}", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.outline, softWrap = false, @@ -117,7 +116,7 @@ fun RepositoryCard( Spacer(modifier = Modifier.height(4.dp)) Text( - text = repository.name, + text = discoveryRepository.repository.name, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onSurface, @@ -128,7 +127,7 @@ fun RepositoryCard( Spacer(modifier = Modifier.height(4.dp)) - repository.description?.let { + discoveryRepository.repository.description?.let { Text( text = it, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -147,7 +146,7 @@ fun RepositoryCard( horizontalArrangement = Arrangement.spacedBy(16.dp) ) { Text( - text = "⭐ ${repository.stargazersCount}", + text = "⭐ ${discoveryRepository.repository.stargazersCount}", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, @@ -156,7 +155,7 @@ fun RepositoryCard( ) Text( - text = "• 🌴 ${repository.forksCount}", + text = "• 🌴 ${discoveryRepository.repository.forksCount}", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, @@ -164,7 +163,7 @@ fun RepositoryCard( overflow = TextOverflow.Ellipsis ) - repository.language?.let { + discoveryRepository.repository.language?.let { Text( text = "• $it", style = MaterialTheme.typography.titleMedium, @@ -176,18 +175,18 @@ fun RepositoryCard( } } - if (isInstalled) { + if (discoveryRepository.isInstalled) { Spacer(Modifier.height(12.dp)) InstallStatusBadge( - isUpdateAvailable = isUpdateAvailable + isUpdateAvailable = discoveryRepository.isUpdateAvailable ) } Spacer(Modifier.height(12.dp)) Text( - text = formatUpdatedAt(repository.updatedAt), + text = formatUpdatedAt(discoveryRepository.repository.updatedAt), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.outline, maxLines = 1, @@ -210,7 +209,7 @@ fun RepositoryCard( IconButton( onClick = { - uriHandler.openUri(repository.htmlUrl) + uriHandler.openUri(discoveryRepository.repository.htmlUrl) }, colors = IconButtonDefaults.iconButtonColors( containerColor = MaterialTheme.colorScheme.primaryContainer, @@ -288,29 +287,32 @@ fun InstallStatusBadge( fun RepositoryCardPreview() { GithubStoreTheme { RepositoryCard( - isInstalled = false, - repository = GithubRepoSummary( - id = 0L, - name = "Hello", - fullName = "JIFEOJEF", - owner = GithubUser( + discoveryRepository = DiscoveryRepository( + repository = GithubRepoSummary( id = 0L, - login = "Skydoves", - avatarUrl = "ewfew", - htmlUrl = "grgrre" + name = "Hello", + fullName = "JIFEOJEF", + owner = GithubUser( + id = 0L, + login = "Skydoves", + avatarUrl = "ewfew", + htmlUrl = "grgrre" + ), + description = "Hello wolrd Hello wolrd Hello wolrd Hello wolrd Hello wolrd", + htmlUrl = "", + stargazersCount = 20, + forksCount = 4, + language = "Kotlin", + topics = null, + releasesUrl = "", + updatedAt = "", + defaultBranch = "" ), - description = "Hello wolrd Hello wolrd Hello wolrd Hello wolrd Hello wolrd", - htmlUrl = "", - stargazersCount = 20, - forksCount = 4, - language = "Kotlin", - topics = null, - releasesUrl = "", - updatedAt = "", - defaultBranch = "" + isUpdateAvailable = true, + isFavourite = true, + isInstalled = true ), onClick = { }, - isUpdateAvailable = true ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/model/DiscoveryRepository.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/model/DiscoveryRepository.kt new file mode 100644 index 000000000..2d6170734 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/model/DiscoveryRepository.kt @@ -0,0 +1,10 @@ +package zed.rainxch.githubstore.core.presentation.model + +import zed.rainxch.githubstore.core.domain.model.GithubRepoSummary + +data class DiscoveryRepository( + val isInstalled: Boolean, + val isUpdateAvailable: Boolean, + val isFavourite: Boolean, + val repository: GithubRepoSummary, +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeRoot.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeRoot.kt index 207631399..6c41bd91b 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeRoot.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeRoot.kt @@ -192,15 +192,13 @@ private fun MainState( ) { items( items = state.repos, - key = { it.repo.id }, + key = { it.repository.id }, contentType = { "repo" } ) { homeRepo -> RepositoryCard( - isInstalled = homeRepo.isInstalled, - isUpdateAvailable = homeRepo.isUpdateAvailable, - repository = homeRepo.repo, + discoveryRepository = homeRepo, onClick = { - onAction(HomeAction.OnRepositoryClick(homeRepo.repo)) + onAction(HomeAction.OnRepositoryClick(homeRepo.repository)) }, modifier = Modifier .animateItem() diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeState.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeState.kt index 23db27d1a..104a3795c 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeState.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeState.kt @@ -2,10 +2,11 @@ package zed.rainxch.githubstore.feature.home.presentation import zed.rainxch.githubstore.core.data.local.db.entities.InstalledApp import zed.rainxch.githubstore.core.domain.model.GithubRepoSummary +import zed.rainxch.githubstore.core.presentation.model.DiscoveryRepository import zed.rainxch.githubstore.feature.home.presentation.model.HomeCategory data class HomeState( - val repos: List = emptyList(), + val repos: List = emptyList(), val installedApps: List = emptyList(), val isLoading: Boolean = false, val isLoadingMore: Boolean = false, @@ -14,10 +15,4 @@ data class HomeState( val currentCategory: HomeCategory = HomeCategory.TRENDING, val isAppsSectionVisible: Boolean = false, val isUpdateAvailable: Boolean = false, -) - -data class HomeRepo( - val isInstalled: Boolean, - val isUpdateAvailable: Boolean, - val repo: GithubRepoSummary ) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeViewModel.kt index 9fd37ad91..64ac4d243 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeViewModel.kt @@ -8,9 +8,6 @@ import githubstore.composeapp.generated.resources.home_failed_to_load_repositori import githubstore.composeapp.generated.resources.no_repositories_found import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.first @@ -21,8 +18,10 @@ import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString import zed.rainxch.githubstore.core.domain.Platform import zed.rainxch.githubstore.core.domain.model.PlatformType +import zed.rainxch.githubstore.core.domain.repository.FavoritesRepository import zed.rainxch.githubstore.core.domain.repository.InstalledAppsRepository import zed.rainxch.githubstore.core.domain.use_cases.SyncInstalledAppsUseCase +import zed.rainxch.githubstore.core.presentation.model.DiscoveryRepository import zed.rainxch.githubstore.feature.home.domain.repository.HomeRepository import zed.rainxch.githubstore.feature.home.presentation.model.HomeCategory @@ -30,7 +29,8 @@ class HomeViewModel( private val homeRepository: HomeRepository, private val installedAppsRepository: InstalledAppsRepository, private val platform: Platform, - private val syncInstalledAppsUseCase: SyncInstalledAppsUseCase + private val syncInstalledAppsUseCase: SyncInstalledAppsUseCase, + private val favoritesRepository: FavoritesRepository ) : ViewModel() { private var hasLoadedInitialData = false @@ -56,9 +56,6 @@ class HomeViewModel( initialValue = HomeState() ) - /** - * Sync system state to ensure DB is up-to-date before loading. - */ private fun syncSystemState() { viewModelScope.launch { try { @@ -85,7 +82,7 @@ class HomeViewModel( _state.update { current -> current.copy( repos = current.repos.map { homeRepo -> - val app = installedMap[homeRepo.repo.id] + val app = installedMap[homeRepo.repository.id] homeRepo.copy( isInstalled = app != null, isUpdateAvailable = app?.isUpdateAvailable ?: false @@ -143,18 +140,26 @@ class HomeViewModel( .first() .associateBy { it.repoId } + val favoritesMap = favoritesRepository + .getAllFavorites() + .first() + .associateBy { it.repoId } + val newReposWithStatus = paginatedRepos.repos.map { repo -> val app = installedAppsMap[repo.id] - HomeRepo( + val favourite = favoritesMap[repo.id] + + DiscoveryRepository( isInstalled = app != null, + isFavourite = favourite != null, isUpdateAvailable = app?.isUpdateAvailable ?: false, - repo = repo + repository = repo ) } _state.update { currentState -> val rawList = currentState.repos + newReposWithStatus - val uniqueList = rawList.distinctBy { it.repo.fullName } + val uniqueList = rawList.distinctBy { it.repository.fullName } currentState.copy( repos = uniqueList, diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchRoot.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchRoot.kt index 4e13f80a7..f3bbbc98c 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchRoot.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchRoot.kt @@ -1,6 +1,5 @@ package zed.rainxch.githubstore.feature.search.presentation -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -63,7 +62,6 @@ import githubstore.composeapp.generated.resources.navigate_back import githubstore.composeapp.generated.resources.results_found import githubstore.composeapp.generated.resources.retry import githubstore.composeapp.generated.resources.search_repositories_hint -import githubstore.composeapp.generated.resources.sort_by import kotlinx.coroutines.delay import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview @@ -73,9 +71,7 @@ import zed.rainxch.githubstore.core.presentation.components.RepositoryCard import zed.rainxch.githubstore.core.presentation.theme.GithubStoreTheme import zed.rainxch.githubstore.feature.search.domain.model.ProgrammingLanguage import zed.rainxch.githubstore.feature.search.domain.model.SearchPlatformType -import zed.rainxch.githubstore.feature.search.domain.model.SortBy import zed.rainxch.githubstore.feature.search.presentation.components.LanguageFilterBottomSheet -import zed.rainxch.githubstore.feature.search.presentation.components.SortByBottomSheet @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -340,14 +336,12 @@ fun SearchScreen( ) { items( items = state.repositories, - key = { it.repo.id }, - ) { searchRepo -> + key = { it.repository.id }, + ) { discoveryRepository -> RepositoryCard( - repository = searchRepo.repo, - isInstalled = searchRepo.isInstalled, - isUpdateAvailable = searchRepo.isUpdateAvailable, + discoveryRepository = discoveryRepository, onClick = { - onAction(SearchAction.OnRepositoryClick(searchRepo.repo)) + onAction(SearchAction.OnRepositoryClick(discoveryRepository.repository)) }, modifier = Modifier.animateItem() ) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchState.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchState.kt index eff0e10a5..3b3b367c7 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchState.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchState.kt @@ -1,13 +1,13 @@ package zed.rainxch.githubstore.feature.search.presentation -import zed.rainxch.githubstore.core.domain.model.GithubRepoSummary +import zed.rainxch.githubstore.core.presentation.model.DiscoveryRepository import zed.rainxch.githubstore.feature.search.domain.model.ProgrammingLanguage import zed.rainxch.githubstore.feature.search.domain.model.SearchPlatformType import zed.rainxch.githubstore.feature.search.domain.model.SortBy data class SearchState( val query: String = "", - val repositories: List = emptyList(), + val repositories: List = emptyList(), val selectedSearchPlatformType: SearchPlatformType = SearchPlatformType.All, val selectedSortBy: SortBy = SortBy.BestMatch, val selectedLanguage: ProgrammingLanguage = ProgrammingLanguage.All, @@ -18,9 +18,3 @@ data class SearchState( val totalCount: Int? = null, val isLanguageSheetVisible: Boolean = false ) - -data class SearchRepo( - val isInstalled: Boolean, - val isUpdateAvailable: Boolean, - val repo: GithubRepoSummary -) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchViewModel.kt index 84adebc10..2e712b91b 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchViewModel.kt @@ -7,11 +7,7 @@ import githubstore.composeapp.generated.resources.Res import githubstore.composeapp.generated.resources.no_repositories_found import githubstore.composeapp.generated.resources.search_failed import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -19,14 +15,17 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString +import zed.rainxch.githubstore.core.domain.repository.FavoritesRepository import zed.rainxch.githubstore.core.domain.repository.InstalledAppsRepository import zed.rainxch.githubstore.core.domain.use_cases.SyncInstalledAppsUseCase +import zed.rainxch.githubstore.core.presentation.model.DiscoveryRepository import zed.rainxch.githubstore.feature.search.domain.repository.SearchRepository class SearchViewModel( private val searchRepository: SearchRepository, private val installedAppsRepository: InstalledAppsRepository, - private val syncInstalledAppsUseCase: SyncInstalledAppsUseCase + private val syncInstalledAppsUseCase: SyncInstalledAppsUseCase, + private val favoritesRepository: FavoritesRepository ) : ViewModel() { private var currentSearchJob: Job? = null @@ -61,7 +60,7 @@ class SearchViewModel( _state.update { current -> current.copy( repositories = current.repositories.map { searchRepo -> - val app = installedMap[searchRepo.repo.id] + val app = installedMap[searchRepo.repository.id] searchRepo.copy( isInstalled = app != null, isUpdateAvailable = app?.isUpdateAvailable ?: false @@ -102,8 +101,14 @@ class SearchViewModel( } try { - val installedAppsSnapshot = installedAppsRepository.getAllInstalledApps().first() - val installedMap = installedAppsSnapshot.associateBy { it.repoId } + val installedMap = installedAppsRepository + .getAllInstalledApps() + .first() + .associateBy { it.repoId } + val favoritesMap = favoritesRepository + .getAllFavorites() + .first() + .associateBy { it.repoId } searchRepository .searchRepositories( @@ -117,29 +122,32 @@ class SearchViewModel( val newReposWithStatus = paginatedRepos.repos.map { repo -> val app = installedMap[repo.id] - SearchRepo( + val favourite = favoritesMap[repo.id] + + DiscoveryRepository( isInstalled = app != null, + isFavourite = favourite != null, isUpdateAvailable = app?.isUpdateAvailable ?: false, - repo = repo + repository = repo ) } _state.update { currentState -> - val mergedMap = LinkedHashMap() + val mergedMap = LinkedHashMap() currentState.repositories.forEach { r -> - mergedMap[r.repo.id] = r + mergedMap[r.repository.id] = r } newReposWithStatus.forEach { r -> - val existing = mergedMap[r.repo.id] + val existing = mergedMap[r.repository.id] if (existing == null) { - mergedMap[r.repo.id] = r + mergedMap[r.repository.id] = r } else { - mergedMap[r.repo.id] = existing.copy( + mergedMap[r.repository.id] = existing.copy( isInstalled = r.isInstalled, isUpdateAvailable = r.isUpdateAvailable, - repo = r.repo + repository = r.repository ) } } From 0ce32fee8069dc69c58b767d5258b0813116fed4 Mon Sep 17 00:00:00 2001 From: Rainxch Zed Date: Mon, 5 Jan 2026 15:52:34 +0500 Subject: [PATCH 04/10] refactor(core): Move platform-specific services to a dedicated package This moves platform-specific service implementations (Downloader, Installer, FileLocationsProvider, ApkInfoExtractor, PackageMonitor, LocalizationManager) from feature and general data packages into a new `core.data.services` package. This refactoring centralizes platform-dependent services, improving code organization and modularity within the core data layer. The dependency injection modules for both Android and JVM have been updated to reflect these new file paths. --- .../githubstore/app/di/PlatformModules.android.kt | 12 ++++++------ .../data/{ => services}/AndroidApkInfoExtractor.kt | 3 +-- .../data => core/data/services}/AndroidDownloader.kt | 4 +--- .../data/services}/AndroidFileLocationsProvider.kt | 3 +-- .../data => core/data/services}/AndroidInstaller.kt | 4 +--- .../{ => services}/AndroidLocalizationManager.kt | 3 +-- .../data/{ => services}/AndroidPackageMonitor.kt | 3 +-- .../githubstore/app/di/PlatformModules.jvm.kt | 12 ++++++------ .../data/{ => services}/DesktopApkInfoExtractor.kt | 3 +-- .../data => core/data/services}/DesktopDownloader.kt | 4 +--- .../data/services}/DesktopFileLocationsProvider.kt | 3 +-- .../data => core/data/services}/DesktopInstaller.kt | 5 ++--- .../{ => services}/DesktopLocalizationManager.kt | 8 ++++---- .../data/{ => services}/DesktopPackageMonitor.kt | 3 +-- 14 files changed, 28 insertions(+), 42 deletions(-) rename composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/{ => services}/AndroidApkInfoExtractor.kt (95%) rename composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/{feature/details/data => core/data/services}/AndroidDownloader.kt (97%) rename composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/{feature/details/data => core/data/services}/AndroidFileLocationsProvider.kt (81%) rename composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/{feature/details/data => core/data/services}/AndroidInstaller.kt (97%) rename composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/{ => services}/AndroidLocalizationManager.kt (81%) rename composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/{ => services}/AndroidPackageMonitor.kt (95%) rename composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/{ => services}/DesktopApkInfoExtractor.kt (67%) rename composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/{feature/details/data => core/data/services}/DesktopDownloader.kt (96%) rename composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/{feature/details/data => core/data/services}/DesktopFileLocationsProvider.kt (97%) rename composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/{feature/details/data => core/data/services}/DesktopInstaller.kt (99%) rename composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/{ => services}/DesktopLocalizationManager.kt (63%) rename composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/{ => services}/DesktopPackageMonitor.kt (80%) diff --git a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/di/PlatformModules.android.kt b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/di/PlatformModules.android.kt index 2f5116de4..b0eea9347 100644 --- a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/di/PlatformModules.android.kt +++ b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/di/PlatformModules.android.kt @@ -5,9 +5,9 @@ import androidx.datastore.preferences.core.Preferences import org.koin.android.ext.koin.androidContext import org.koin.core.module.Module import org.koin.dsl.module -import zed.rainxch.githubstore.core.data.AndroidApkInfoExtractor -import zed.rainxch.githubstore.core.data.AndroidLocalizationManager -import zed.rainxch.githubstore.core.data.AndroidPackageMonitor +import zed.rainxch.githubstore.core.data.services.AndroidApkInfoExtractor +import zed.rainxch.githubstore.core.data.services.AndroidLocalizationManager +import zed.rainxch.githubstore.core.data.services.AndroidPackageMonitor import zed.rainxch.githubstore.core.data.services.PackageMonitor import zed.rainxch.githubstore.core.data.local.data_store.createDataStore import zed.rainxch.githubstore.core.data.local.db.AppDatabase @@ -20,9 +20,9 @@ import zed.rainxch.githubstore.core.presentation.utils.BrowserHelper import zed.rainxch.githubstore.core.presentation.utils.ClipboardHelper import zed.rainxch.githubstore.feature.auth.data.AndroidTokenStore import zed.rainxch.githubstore.feature.auth.data.TokenStore -import zed.rainxch.githubstore.feature.details.data.AndroidDownloader -import zed.rainxch.githubstore.feature.details.data.AndroidFileLocationsProvider -import zed.rainxch.githubstore.feature.details.data.AndroidInstaller +import zed.rainxch.githubstore.core.data.services.AndroidDownloader +import zed.rainxch.githubstore.core.data.services.AndroidFileLocationsProvider +import zed.rainxch.githubstore.core.data.services.AndroidInstaller import zed.rainxch.githubstore.core.data.services.Downloader import zed.rainxch.githubstore.core.data.services.FileLocationsProvider import zed.rainxch.githubstore.core.data.services.Installer diff --git a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/AndroidApkInfoExtractor.kt b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidApkInfoExtractor.kt similarity index 95% rename from composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/AndroidApkInfoExtractor.kt rename to composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidApkInfoExtractor.kt index d6d71a5ba..e878c5746 100644 --- a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/AndroidApkInfoExtractor.kt +++ b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidApkInfoExtractor.kt @@ -1,4 +1,4 @@ -package zed.rainxch.githubstore.core.data +package zed.rainxch.githubstore.core.data.services import android.content.Context import android.content.pm.PackageManager @@ -6,7 +6,6 @@ import android.os.Build import co.touchlab.kermit.Logger import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import zed.rainxch.githubstore.core.data.services.ApkInfoExtractor import zed.rainxch.githubstore.core.domain.model.ApkPackageInfo import java.io.File diff --git a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/feature/details/data/AndroidDownloader.kt b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidDownloader.kt similarity index 97% rename from composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/feature/details/data/AndroidDownloader.kt rename to composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidDownloader.kt index 94bfb254e..c72299b5f 100644 --- a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/feature/details/data/AndroidDownloader.kt +++ b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidDownloader.kt @@ -1,4 +1,4 @@ -package zed.rainxch.githubstore.feature.details.data +package zed.rainxch.githubstore.core.data.services import android.app.DownloadManager import android.content.Context @@ -14,8 +14,6 @@ import java.io.File import java.util.UUID import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flowOn -import zed.rainxch.githubstore.core.data.services.Downloader -import zed.rainxch.githubstore.core.data.services.FileLocationsProvider import zed.rainxch.githubstore.feature.details.domain.model.DownloadProgress import java.util.concurrent.ConcurrentHashMap import androidx.core.net.toUri diff --git a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/feature/details/data/AndroidFileLocationsProvider.kt b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidFileLocationsProvider.kt similarity index 81% rename from composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/feature/details/data/AndroidFileLocationsProvider.kt rename to composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidFileLocationsProvider.kt index 8961d9d5b..05821cd0a 100644 --- a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/feature/details/data/AndroidFileLocationsProvider.kt +++ b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidFileLocationsProvider.kt @@ -1,7 +1,6 @@ -package zed.rainxch.githubstore.feature.details.data +package zed.rainxch.githubstore.core.data.services import android.content.Context -import zed.rainxch.githubstore.core.data.services.FileLocationsProvider import java.io.File class AndroidFileLocationsProvider( diff --git a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/feature/details/data/AndroidInstaller.kt b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidInstaller.kt similarity index 97% rename from composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/feature/details/data/AndroidInstaller.kt rename to composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidInstaller.kt index 63870c088..91aef0cfb 100644 --- a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/feature/details/data/AndroidInstaller.kt +++ b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidInstaller.kt @@ -1,4 +1,4 @@ -package zed.rainxch.githubstore.feature.details.data +package zed.rainxch.githubstore.core.data.services import android.content.ActivityNotFoundException import android.content.Context @@ -10,8 +10,6 @@ import androidx.core.content.FileProvider import java.io.File import androidx.core.net.toUri import co.touchlab.kermit.Logger -import zed.rainxch.githubstore.core.data.services.ApkInfoExtractor -import zed.rainxch.githubstore.core.data.services.Installer import zed.rainxch.githubstore.core.domain.model.Architecture import zed.rainxch.githubstore.core.domain.model.GithubAsset diff --git a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/AndroidLocalizationManager.kt b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidLocalizationManager.kt similarity index 81% rename from composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/AndroidLocalizationManager.kt rename to composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidLocalizationManager.kt index f16c4bc09..4203c61a1 100644 --- a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/AndroidLocalizationManager.kt +++ b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidLocalizationManager.kt @@ -1,6 +1,5 @@ -package zed.rainxch.githubstore.core.data +package zed.rainxch.githubstore.core.data.services -import zed.rainxch.githubstore.core.data.services.LocalizationManager import java.util.Locale class AndroidLocalizationManager : LocalizationManager { diff --git a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/AndroidPackageMonitor.kt b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidPackageMonitor.kt similarity index 95% rename from composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/AndroidPackageMonitor.kt rename to composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidPackageMonitor.kt index b1a2d2958..a8ec9fb8e 100644 --- a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/AndroidPackageMonitor.kt +++ b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/core/data/services/AndroidPackageMonitor.kt @@ -1,11 +1,10 @@ -package zed.rainxch.githubstore.core.data +package zed.rainxch.githubstore.core.data.services import android.content.Context import android.content.pm.PackageManager import android.os.Build import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import zed.rainxch.githubstore.core.data.services.PackageMonitor import zed.rainxch.githubstore.core.domain.model.SystemPackageInfo class AndroidPackageMonitor( diff --git a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/app/di/PlatformModules.jvm.kt b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/app/di/PlatformModules.jvm.kt index 151a92655..55c298546 100644 --- a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/app/di/PlatformModules.jvm.kt +++ b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/app/di/PlatformModules.jvm.kt @@ -4,9 +4,9 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import org.koin.core.module.Module import org.koin.dsl.module -import zed.rainxch.githubstore.core.data.DesktopApkInfoExtractor -import zed.rainxch.githubstore.core.data.DesktopLocalizationManager -import zed.rainxch.githubstore.core.data.DesktopPackageMonitor +import zed.rainxch.githubstore.core.data.services.DesktopApkInfoExtractor +import zed.rainxch.githubstore.core.data.services.DesktopLocalizationManager +import zed.rainxch.githubstore.core.data.services.DesktopPackageMonitor import zed.rainxch.githubstore.core.data.services.PackageMonitor import zed.rainxch.githubstore.core.data.local.data_store.createDataStore import zed.rainxch.githubstore.core.data.local.db.AppDatabase @@ -24,9 +24,9 @@ import zed.rainxch.githubstore.core.data.services.Downloader import zed.rainxch.githubstore.core.data.services.FileLocationsProvider import zed.rainxch.githubstore.core.data.services.Installer import zed.rainxch.githubstore.core.data.services.LocalizationManager -import zed.rainxch.githubstore.feature.details.data.DesktopDownloader -import zed.rainxch.githubstore.feature.details.data.DesktopFileLocationsProvider -import zed.rainxch.githubstore.feature.details.data.DesktopInstaller +import zed.rainxch.githubstore.core.data.services.DesktopDownloader +import zed.rainxch.githubstore.core.data.services.DesktopFileLocationsProvider +import zed.rainxch.githubstore.core.data.services.DesktopInstaller actual val platformModule: Module = module { single { diff --git a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/DesktopApkInfoExtractor.kt b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopApkInfoExtractor.kt similarity index 67% rename from composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/DesktopApkInfoExtractor.kt rename to composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopApkInfoExtractor.kt index 7365dfe53..669ac1a1a 100644 --- a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/DesktopApkInfoExtractor.kt +++ b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopApkInfoExtractor.kt @@ -1,6 +1,5 @@ -package zed.rainxch.githubstore.core.data +package zed.rainxch.githubstore.core.data.services -import zed.rainxch.githubstore.core.data.services.ApkInfoExtractor import zed.rainxch.githubstore.core.domain.model.ApkPackageInfo class DesktopApkInfoExtractor : ApkInfoExtractor { diff --git a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/feature/details/data/DesktopDownloader.kt b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopDownloader.kt similarity index 96% rename from composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/feature/details/data/DesktopDownloader.kt rename to composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopDownloader.kt index e11724c79..1c01907da 100644 --- a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/feature/details/data/DesktopDownloader.kt +++ b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopDownloader.kt @@ -1,4 +1,4 @@ -package zed.rainxch.githubstore.feature.details.data +package zed.rainxch.githubstore.core.data.services import co.touchlab.kermit.Logger import io.ktor.client.* @@ -11,8 +11,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext -import zed.rainxch.githubstore.core.data.services.Downloader -import zed.rainxch.githubstore.core.data.services.FileLocationsProvider import zed.rainxch.githubstore.feature.details.domain.model.DownloadProgress import java.io.File import java.io.FileOutputStream diff --git a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/feature/details/data/DesktopFileLocationsProvider.kt b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopFileLocationsProvider.kt similarity index 97% rename from composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/feature/details/data/DesktopFileLocationsProvider.kt rename to composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopFileLocationsProvider.kt index 33b49a947..7a2862ca9 100644 --- a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/feature/details/data/DesktopFileLocationsProvider.kt +++ b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopFileLocationsProvider.kt @@ -1,7 +1,6 @@ -package zed.rainxch.githubstore.feature.details.data +package zed.rainxch.githubstore.core.data.services import co.touchlab.kermit.Logger -import zed.rainxch.githubstore.core.data.services.FileLocationsProvider import zed.rainxch.githubstore.core.domain.model.PlatformType import java.io.File import java.nio.file.Files diff --git a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/feature/details/data/DesktopInstaller.kt b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopInstaller.kt similarity index 99% rename from composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/feature/details/data/DesktopInstaller.kt rename to composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopInstaller.kt index eb47fdae4..0f4480e52 100644 --- a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/feature/details/data/DesktopInstaller.kt +++ b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopInstaller.kt @@ -1,10 +1,8 @@ -package zed.rainxch.githubstore.feature.details.data +package zed.rainxch.githubstore.core.data.services import co.touchlab.kermit.Logger import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import zed.rainxch.githubstore.core.data.services.ApkInfoExtractor -import zed.rainxch.githubstore.core.data.services.Installer import zed.rainxch.githubstore.core.domain.model.Architecture import zed.rainxch.githubstore.core.domain.model.GithubAsset import zed.rainxch.githubstore.core.domain.model.PlatformType @@ -15,6 +13,7 @@ import java.awt.Toolkit import java.awt.datatransfer.StringSelection import java.io.File import java.io.IOException +import kotlin.collections.iterator class DesktopInstaller( private val platform: PlatformType, diff --git a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/DesktopLocalizationManager.kt b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopLocalizationManager.kt similarity index 63% rename from composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/DesktopLocalizationManager.kt rename to composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopLocalizationManager.kt index 5c3a8384e..d74628b19 100644 --- a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/DesktopLocalizationManager.kt +++ b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopLocalizationManager.kt @@ -1,10 +1,10 @@ -package zed.rainxch.githubstore.core.data +package zed.rainxch.githubstore.core.data.services -import zed.rainxch.githubstore.core.data.services.LocalizationManager +import java.util.Locale class DesktopLocalizationManager : LocalizationManager { override fun getCurrentLanguageCode(): String { - val locale = java.util.Locale.getDefault() + val locale = Locale.getDefault() val language = locale.language val country = locale.country return if (country.isNotEmpty()) { @@ -15,6 +15,6 @@ class DesktopLocalizationManager : LocalizationManager { } override fun getPrimaryLanguageCode(): String { - return java.util.Locale.getDefault().language + return Locale.getDefault().language } } \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/DesktopPackageMonitor.kt b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopPackageMonitor.kt similarity index 80% rename from composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/DesktopPackageMonitor.kt rename to composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopPackageMonitor.kt index 85ac7b728..1a2a010e1 100644 --- a/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/DesktopPackageMonitor.kt +++ b/composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/core/data/services/DesktopPackageMonitor.kt @@ -1,6 +1,5 @@ -package zed.rainxch.githubstore.core.data +package zed.rainxch.githubstore.core.data.services -import zed.rainxch.githubstore.core.data.services.PackageMonitor import zed.rainxch.githubstore.core.domain.model.SystemPackageInfo class DesktopPackageMonitor : PackageMonitor { From bc13711447ca8b099dbf8e14cc21fe646970630b Mon Sep 17 00:00:00 2001 From: Rainxch Zed Date: Mon, 5 Jan 2026 15:55:20 +0500 Subject: [PATCH 05/10] feat(details): Improve favorite button accessibility This commit introduces new string resources for "Add to favourites" and "Remove from favourites" to provide more accurate content descriptions for the favorite button's state. These new strings have been added for all supported languages: English, Japanese, Chinese, French, Bengali, Korean, Russian, Italian, and Spanish. The implementation has been updated to use these more descriptive labels. Additionally, the `isFavourite` status is now correctly updated in the search results when changes are made. --- .../commonMain/composeResources/values-bn/strings-bn.xml | 2 ++ .../commonMain/composeResources/values-es/strings-es.xml | 2 ++ .../commonMain/composeResources/values-fr/strings-fr.xml | 2 ++ .../commonMain/composeResources/values-it/strings-it.xml | 2 ++ .../commonMain/composeResources/values-ja/strings-ja.xml | 2 ++ .../commonMain/composeResources/values-kr/strings-kr.xml | 2 ++ .../commonMain/composeResources/values-ru/strings-ru.xml | 2 ++ .../composeResources/values-zh-rCN/strings-zh-rCN.xml | 2 ++ .../src/commonMain/composeResources/values/strings.xml | 3 +++ .../githubstore/feature/details/presentation/DetailsRoot.kt | 6 ++++-- .../feature/search/presentation/SearchViewModel.kt | 1 + 11 files changed, 24 insertions(+), 2 deletions(-) diff --git a/composeApp/src/commonMain/composeResources/values-bn/strings-bn.xml b/composeApp/src/commonMain/composeResources/values-bn/strings-bn.xml index 73bbaacd4..18c3ec92e 100644 --- a/composeApp/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/composeApp/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -249,5 +249,7 @@ রিপোজিটরি প্রিয় তালিকায় যোগ করা হয়েছে রিপোজিটরি প্রিয় তালিকা থেকে সরানো হয়েছে + প্রিয় তালিকায় যোগ করুন + প্রিয় তালিকা থেকে সরান diff --git a/composeApp/src/commonMain/composeResources/values-es/strings-es.xml b/composeApp/src/commonMain/composeResources/values-es/strings-es.xml index fea85d28c..5fa6faca1 100644 --- a/composeApp/src/commonMain/composeResources/values-es/strings-es.xml +++ b/composeApp/src/commonMain/composeResources/values-es/strings-es.xml @@ -196,5 +196,7 @@ Repositorio añadido a favoritos Repositorio eliminado de favoritos + Añadir a favoritos + Quitar de favoritos \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-fr/strings-fr.xml b/composeApp/src/commonMain/composeResources/values-fr/strings-fr.xml index 065173361..babc5a130 100644 --- a/composeApp/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/composeApp/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -196,5 +196,7 @@ Dépôt ajouté aux favoris Dépôt supprimé des favoris + Ajouter aux favoris + Retirer des favoris \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-it/strings-it.xml b/composeApp/src/commonMain/composeResources/values-it/strings-it.xml index a4f8f0afe..8373ef95f 100644 --- a/composeApp/src/commonMain/composeResources/values-it/strings-it.xml +++ b/composeApp/src/commonMain/composeResources/values-it/strings-it.xml @@ -245,5 +245,7 @@ Repository aggiunto ai preferiti Repository rimosso dai preferiti + Aggiungi ai preferiti + Rimuovi dai preferiti \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-ja/strings-ja.xml b/composeApp/src/commonMain/composeResources/values-ja/strings-ja.xml index fa55d8305..98c095df3 100644 --- a/composeApp/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/composeApp/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -196,5 +196,7 @@ リポジトリをお気に入りに追加しました リポジトリをお気に入りから削除しました + お気に入りに追加 + お気に入りから削除 \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-kr/strings-kr.xml b/composeApp/src/commonMain/composeResources/values-kr/strings-kr.xml index 0c492d7fb..ce95d07c2 100644 --- a/composeApp/src/commonMain/composeResources/values-kr/strings-kr.xml +++ b/composeApp/src/commonMain/composeResources/values-kr/strings-kr.xml @@ -247,5 +247,7 @@ 리포지토리를 즐겨찾기에 추가했습니다 리포지토리를 즐겨찾기에서 제거했습니다 + 즐겨찾기에 추가 + 즐겨찾기에서 제거 \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-ru/strings-ru.xml b/composeApp/src/commonMain/composeResources/values-ru/strings-ru.xml index 844dd50de..a9e5d48b4 100644 --- a/composeApp/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/composeApp/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -214,5 +214,7 @@ Репозиторий добавлен в избранное Репозиторий удалён из избранного + Добавить в избранное + Удалить из избранного \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/composeApp/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 236c24c6f..69172486c 100644 --- a/composeApp/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/composeApp/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -197,5 +197,7 @@ 已添加到收藏 已从收藏中移除 + 添加到收藏 + 从收藏中移除 \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index c7a569873..b66cc7514 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -249,5 +249,8 @@ Repository added to favourites Repository removed from favourites + Add to favourites + Remove from favourites + \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt index 186f87caa..a4d1aebbd 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt @@ -38,9 +38,11 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import githubstore.composeapp.generated.resources.Res +import githubstore.composeapp.generated.resources.add_to_favourites import githubstore.composeapp.generated.resources.added_to_favourites import githubstore.composeapp.generated.resources.navigate_back import githubstore.composeapp.generated.resources.open_repository +import githubstore.composeapp.generated.resources.remove_from_favourites import githubstore.composeapp.generated.resources.removed_from_favourites import io.github.fletchmckee.liquid.LiquidState import io.github.fletchmckee.liquid.liquefiable @@ -238,9 +240,9 @@ private fun DetailsTopbar( } else Icons.Default.FavoriteBorder, contentDescription = stringResource( resource = if (state.isFavorite) { - Res.string.added_to_favourites + Res.string.remove_from_favourites } else { - Res.string.removed_from_favourites + Res.string.add_to_favourites } ), ) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchViewModel.kt index 2e712b91b..e35fb0c22 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchViewModel.kt @@ -147,6 +147,7 @@ class SearchViewModel( mergedMap[r.repository.id] = existing.copy( isInstalled = r.isInstalled, isUpdateAvailable = r.isUpdateAvailable, + isFavourite = r.isFavourite, repository = r.repository ) } From d1ba54d7c77e29ba02373df2b57747fac76e774f Mon Sep 17 00:00:00 2001 From: Rainxch Zed Date: Mon, 5 Jan 2026 16:01:23 +0500 Subject: [PATCH 06/10] feat(navigation): Add Favourites screen to bottom navigation Adds a new "Favourites" item to the bottom navigation bar, allowing users to access a dedicated screen for their favourited repositories. This includes adding the corresponding string translations for "Favourites" in English, Japanese, Chinese (Simplified), French, Bengali, Korean, Russian, Italian, and Spanish. --- .../commonMain/composeResources/values-bn/strings-bn.xml | 1 + .../commonMain/composeResources/values-es/strings-es.xml | 1 + .../commonMain/composeResources/values-fr/strings-fr.xml | 1 + .../commonMain/composeResources/values-it/strings-it.xml | 1 + .../commonMain/composeResources/values-ja/strings-ja.xml | 1 + .../commonMain/composeResources/values-kr/strings-kr.xml | 1 + .../commonMain/composeResources/values-ru/strings-ru.xml | 1 + .../composeResources/values-zh-rCN/strings-zh-rCN.xml | 1 + .../src/commonMain/composeResources/values/strings.xml | 1 + .../githubstore/app/navigation/BottomNavigationUtils.kt | 7 +++++++ 10 files changed, 16 insertions(+) diff --git a/composeApp/src/commonMain/composeResources/values-bn/strings-bn.xml b/composeApp/src/commonMain/composeResources/values-bn/strings-bn.xml index 18c3ec92e..3474043fc 100644 --- a/composeApp/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/composeApp/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -251,5 +251,6 @@ রিপোজিটরি প্রিয় তালিকা থেকে সরানো হয়েছে প্রিয় তালিকায় যোগ করুন প্রিয় তালিকা থেকে সরান + প্রিয় diff --git a/composeApp/src/commonMain/composeResources/values-es/strings-es.xml b/composeApp/src/commonMain/composeResources/values-es/strings-es.xml index 5fa6faca1..22c17f7e4 100644 --- a/composeApp/src/commonMain/composeResources/values-es/strings-es.xml +++ b/composeApp/src/commonMain/composeResources/values-es/strings-es.xml @@ -198,5 +198,6 @@ Repositorio eliminado de favoritos Añadir a favoritos Quitar de favoritos + Favoritos \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-fr/strings-fr.xml b/composeApp/src/commonMain/composeResources/values-fr/strings-fr.xml index babc5a130..e552b7835 100644 --- a/composeApp/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/composeApp/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -198,5 +198,6 @@ Dépôt supprimé des favoris Ajouter aux favoris Retirer des favoris + Favoris \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-it/strings-it.xml b/composeApp/src/commonMain/composeResources/values-it/strings-it.xml index 8373ef95f..2b8e86a85 100644 --- a/composeApp/src/commonMain/composeResources/values-it/strings-it.xml +++ b/composeApp/src/commonMain/composeResources/values-it/strings-it.xml @@ -247,5 +247,6 @@ Repository rimosso dai preferiti Aggiungi ai preferiti Rimuovi dai preferiti + Preferiti \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-ja/strings-ja.xml b/composeApp/src/commonMain/composeResources/values-ja/strings-ja.xml index 98c095df3..b4f897293 100644 --- a/composeApp/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/composeApp/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -198,5 +198,6 @@ リポジトリをお気に入りから削除しました お気に入りに追加 お気に入りから削除 + お気に入り \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-kr/strings-kr.xml b/composeApp/src/commonMain/composeResources/values-kr/strings-kr.xml index ce95d07c2..65078dd91 100644 --- a/composeApp/src/commonMain/composeResources/values-kr/strings-kr.xml +++ b/composeApp/src/commonMain/composeResources/values-kr/strings-kr.xml @@ -249,5 +249,6 @@ 리포지토리를 즐겨찾기에서 제거했습니다 즐겨찾기에 추가 즐겨찾기에서 제거 + 즐겨찾기 \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-ru/strings-ru.xml b/composeApp/src/commonMain/composeResources/values-ru/strings-ru.xml index a9e5d48b4..224685c84 100644 --- a/composeApp/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/composeApp/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -216,5 +216,6 @@ Репозиторий удалён из избранного Добавить в избранное Удалить из избранного + Избранное \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/composeApp/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 69172486c..3a80b5010 100644 --- a/composeApp/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/composeApp/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -199,5 +199,6 @@ 已从收藏中移除 添加到收藏 从收藏中移除 + 收藏 \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index b66cc7514..580c57675 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -251,6 +251,7 @@ Repository removed from favourites Add to favourites Remove from favourites + Favourites \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigationUtils.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigationUtils.kt index dabd2c40f..d84435f44 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigationUtils.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigationUtils.kt @@ -5,10 +5,12 @@ import androidx.compose.material.icons.filled.Apps import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.outlined.Apps +import androidx.compose.material.icons.outlined.Favorite import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Settings import androidx.compose.ui.graphics.vector.ImageVector import githubstore.composeapp.generated.resources.Res +import githubstore.composeapp.generated.resources.favourites import githubstore.composeapp.generated.resources.installed_apps import githubstore.composeapp.generated.resources.search_repositories_hint import githubstore.composeapp.generated.resources.settings_title @@ -33,6 +35,11 @@ object BottomNavigationUtils { iconRes = Icons.Outlined.Apps, screen = GithubStoreGraph.AppsScreen ), + BottomNavigationItem( + titleRes = Res.string.favourites, + iconRes = Icons.Outlined.Favorite, + screen = GithubStoreGraph.FavouritesScreen + ), BottomNavigationItem( titleRes = Res.string.settings_title, iconRes = Icons.Outlined.Settings, From 9eb589d31193ec248000b4255e3786c21226d97a Mon Sep 17 00:00:00 2001 From: Rainxch Zed Date: Mon, 5 Jan 2026 16:01:36 +0500 Subject: [PATCH 07/10] refactor: Standardize spelling to 'Favourites' Corrected the spelling of "Favorites" to "Favourites" across multiple files for consistency. This includes renaming files, classes, interfaces, and variable names. --- .../rainxch/githubstore/app/di/SharedModules.kt | 14 +++++++------- ...positoryImpl.kt => FavouritesRepositoryImpl.kt} | 6 +++--- ...oritesRepository.kt => FavouritesRepository.kt} | 2 +- .../details/presentation/DetailsViewModel.kt | 12 ++++++------ .../feature/home/presentation/HomeViewModel.kt | 6 +++--- .../feature/search/presentation/SearchViewModel.kt | 6 +++--- 6 files changed, 23 insertions(+), 23 deletions(-) rename composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/repository/{FavoritesRepositoryImpl.kt => FavouritesRepositoryImpl.kt} (94%) rename composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/domain/repository/{FavoritesRepository.kt => FavouritesRepository.kt} (94%) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt index aea5754df..27f60a362 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt @@ -12,11 +12,11 @@ import zed.rainxch.githubstore.core.data.services.PackageMonitor import zed.rainxch.githubstore.core.data.data_source.DefaultTokenDataSource import zed.rainxch.githubstore.core.data.data_source.TokenDataSource import zed.rainxch.githubstore.core.data.local.db.AppDatabase -import zed.rainxch.githubstore.core.data.repository.FavoritesRepositoryImpl +import zed.rainxch.githubstore.core.data.repository.FavouritesRepositoryImpl import zed.rainxch.githubstore.core.data.repository.InstalledAppsRepositoryImpl import zed.rainxch.githubstore.core.data.repository.ThemesRepositoryImpl import zed.rainxch.githubstore.core.domain.getPlatform -import zed.rainxch.githubstore.core.domain.repository.FavoritesRepository +import zed.rainxch.githubstore.core.domain.repository.FavouritesRepository import zed.rainxch.githubstore.core.domain.repository.InstalledAppsRepository import zed.rainxch.githubstore.core.domain.repository.ThemesRepository import zed.rainxch.githubstore.feature.apps.data.repository.AppsRepositoryImpl @@ -100,8 +100,8 @@ val coreModule: Module = module { } // Repositories - single { - FavoritesRepositoryImpl( + single { + FavouritesRepositoryImpl( dao = get(), installedAppsDao = get(), detailsRepository = get() @@ -173,7 +173,7 @@ val homeModule: Module = module { installedAppsRepository = get(), platform = get(), syncInstalledAppsUseCase = get(), - favoritesRepository = get() + favouritesRepository = get() ) } } @@ -194,7 +194,7 @@ val searchModule: Module = module { searchRepository = get(), installedAppsRepository = get(), syncInstalledAppsUseCase = get(), - favoritesRepository = get() + favouritesRepository = get() ) } } @@ -225,7 +225,7 @@ val detailsModule: Module = module { platform = get(), helper = get(), installedAppsRepository = get(), - favoritesRepository = get(), + favouritesRepository = get(), packageMonitor = get(), syncInstalledAppsUseCase = get() ) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/repository/FavoritesRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/repository/FavouritesRepositoryImpl.kt similarity index 94% rename from composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/repository/FavoritesRepositoryImpl.kt rename to composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/repository/FavouritesRepositoryImpl.kt index 959b29cb0..fcb8e18df 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/repository/FavoritesRepositoryImpl.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/repository/FavouritesRepositoryImpl.kt @@ -5,14 +5,14 @@ import kotlinx.coroutines.flow.first import zed.rainxch.githubstore.core.data.local.db.dao.FavoriteRepoDao import zed.rainxch.githubstore.core.data.local.db.dao.InstalledAppDao import zed.rainxch.githubstore.core.data.local.db.entities.FavoriteRepo -import zed.rainxch.githubstore.core.domain.repository.FavoritesRepository +import zed.rainxch.githubstore.core.domain.repository.FavouritesRepository import zed.rainxch.githubstore.feature.details.domain.repository.DetailsRepository -class FavoritesRepositoryImpl( +class FavouritesRepositoryImpl( private val dao: FavoriteRepoDao, private val installedAppsDao: InstalledAppDao, private val detailsRepository: DetailsRepository -) : FavoritesRepository { +) : FavouritesRepository { override fun getAllFavorites(): Flow> = dao.getAllFavorites() diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/domain/repository/FavoritesRepository.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/domain/repository/FavouritesRepository.kt similarity index 94% rename from composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/domain/repository/FavoritesRepository.kt rename to composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/domain/repository/FavouritesRepository.kt index 7839f8e10..4f5f8075e 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/domain/repository/FavoritesRepository.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/domain/repository/FavouritesRepository.kt @@ -3,7 +3,7 @@ package zed.rainxch.githubstore.core.domain.repository import kotlinx.coroutines.flow.Flow import zed.rainxch.githubstore.core.data.local.db.entities.FavoriteRepo -interface FavoritesRepository { +interface FavouritesRepository { fun getAllFavorites(): Flow> fun isFavorite(repoId: Long): Flow suspend fun isFavoriteSync(repoId: Long): Boolean diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt index 814751f25..b2d09ed61 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt @@ -33,7 +33,7 @@ import zed.rainxch.githubstore.core.data.local.db.entities.InstallSource import zed.rainxch.githubstore.core.data.local.db.entities.InstalledApp import zed.rainxch.githubstore.core.domain.Platform import zed.rainxch.githubstore.core.domain.model.PlatformType -import zed.rainxch.githubstore.core.domain.repository.FavoritesRepository +import zed.rainxch.githubstore.core.domain.repository.FavouritesRepository import zed.rainxch.githubstore.core.domain.repository.InstalledAppsRepository import zed.rainxch.githubstore.core.presentation.utils.BrowserHelper import zed.rainxch.githubstore.core.data.services.Downloader @@ -52,7 +52,7 @@ class DetailsViewModel( private val platform: Platform, private val helper: BrowserHelper, private val installedAppsRepository: InstalledAppsRepository, - private val favoritesRepository: FavoritesRepository, + private val favouritesRepository: FavouritesRepository, private val packageMonitor: PackageMonitor, private val syncInstalledAppsUseCase: SyncInstalledAppsUseCase ) : ViewModel() { @@ -92,7 +92,7 @@ class DetailsViewModel( val repo = detailsRepository.getRepositoryById(repositoryId.toLong()) val isFavoriteDeferred = async { try { - favoritesRepository.isFavoriteSync(repo.id) + favouritesRepository.isFavoriteSync(repo.id) } catch (t: Throwable) { Logger.e { "Failed to load if repo is favourite: ${t.localizedMessage}" } false @@ -357,9 +357,9 @@ class DetailsViewModel( lastSyncedAt = System.now().toEpochMilliseconds() ) - favoritesRepository.toggleFavorite(favoriteRepo) + favouritesRepository.toggleFavorite(favoriteRepo) - val newFavoriteState = favoritesRepository.isFavoriteSync(repo.id) + val newFavoriteState = favouritesRepository.isFavoriteSync(repo.id) _state.value = _state.value.copy(isFavorite = newFavoriteState) _events.send( @@ -736,7 +736,7 @@ class DetailsViewModel( } if (_state.value.isFavorite) { - favoritesRepository.updateFavoriteInstallStatus( + favouritesRepository.updateFavoriteInstallStatus( repoId = repo.id, installed = true, packageName = packageName diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeViewModel.kt index 64ac4d243..14c50d414 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeViewModel.kt @@ -18,7 +18,7 @@ import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString import zed.rainxch.githubstore.core.domain.Platform import zed.rainxch.githubstore.core.domain.model.PlatformType -import zed.rainxch.githubstore.core.domain.repository.FavoritesRepository +import zed.rainxch.githubstore.core.domain.repository.FavouritesRepository import zed.rainxch.githubstore.core.domain.repository.InstalledAppsRepository import zed.rainxch.githubstore.core.domain.use_cases.SyncInstalledAppsUseCase import zed.rainxch.githubstore.core.presentation.model.DiscoveryRepository @@ -30,7 +30,7 @@ class HomeViewModel( private val installedAppsRepository: InstalledAppsRepository, private val platform: Platform, private val syncInstalledAppsUseCase: SyncInstalledAppsUseCase, - private val favoritesRepository: FavoritesRepository + private val favouritesRepository: FavouritesRepository ) : ViewModel() { private var hasLoadedInitialData = false @@ -140,7 +140,7 @@ class HomeViewModel( .first() .associateBy { it.repoId } - val favoritesMap = favoritesRepository + val favoritesMap = favouritesRepository .getAllFavorites() .first() .associateBy { it.repoId } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchViewModel.kt index e35fb0c22..1aed05bfd 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchViewModel.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString -import zed.rainxch.githubstore.core.domain.repository.FavoritesRepository +import zed.rainxch.githubstore.core.domain.repository.FavouritesRepository import zed.rainxch.githubstore.core.domain.repository.InstalledAppsRepository import zed.rainxch.githubstore.core.domain.use_cases.SyncInstalledAppsUseCase import zed.rainxch.githubstore.core.presentation.model.DiscoveryRepository @@ -25,7 +25,7 @@ class SearchViewModel( private val searchRepository: SearchRepository, private val installedAppsRepository: InstalledAppsRepository, private val syncInstalledAppsUseCase: SyncInstalledAppsUseCase, - private val favoritesRepository: FavoritesRepository + private val favouritesRepository: FavouritesRepository ) : ViewModel() { private var currentSearchJob: Job? = null @@ -105,7 +105,7 @@ class SearchViewModel( .getAllInstalledApps() .first() .associateBy { it.repoId } - val favoritesMap = favoritesRepository + val favoritesMap = favouritesRepository .getAllFavorites() .first() .associateBy { it.repoId } From 4a175e3d46f57719da1ef3269c474072f0dad8e3 Mon Sep 17 00:00:00 2001 From: Rainxch Zed Date: Mon, 5 Jan 2026 16:29:24 +0500 Subject: [PATCH 08/10] feat: Display favorite status on repository cards This commit introduces the following changes: - Updates `HomeViewModel` and `SearchViewModel` to observe the favorites repository. - The `isFavourite` status of a repository is now reflected in the UI. - A decorative favorite icon is now displayed on the `RepositoryCard` for repositories that have been marked as a favorite. --- .../presentation/components/RepositoryCard.kt | 262 ++++++++++-------- .../home/presentation/HomeViewModel.kt | 18 ++ .../search/presentation/SearchViewModel.kt | 18 ++ 3 files changed, 175 insertions(+), 123 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/components/RepositoryCard.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/components/RepositoryCard.kt index b305c555e..489730d38 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/components/RepositoryCard.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/components/RepositoryCard.kt @@ -8,12 +8,14 @@ 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.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.OpenInBrowser import androidx.compose.material.icons.filled.Update import androidx.compose.material3.Card @@ -66,161 +68,175 @@ fun RepositoryCard( ), shape = RoundedCornerShape(24.dp) ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - CoilImage( - imageModel = { discoveryRepository.repository.owner.avatarUrl }, + Box { + if(discoveryRepository.isFavourite) { + Icon( + imageVector = Icons.Default.Favorite, + contentDescription = null, modifier = Modifier - .size(32.dp) - .clip(CircleShape), - loading = { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularWavyProgressIndicator() - } - }, - component = rememberImageComponent { - CrossfadePlugin() - } - ) - - Text( - text = discoveryRepository.repository.owner.login, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.outline, - maxLines = 1, - softWrap = false, - overflow = TextOverflow.Ellipsis - ) - - Text( - text = "/ ${discoveryRepository.repository.name}", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.outline, - softWrap = false, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - modifier = Modifier.weight(1f) + .size(128.dp) + .align(Alignment.BottomStart) + .offset(x = -(32).dp, y = 32.dp), + tint = MaterialTheme.colorScheme.primary.copy(alpha = .1f), ) } - Spacer(modifier = Modifier.height(4.dp)) - - Text( - text = discoveryRepository.repository.name, - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - softWrap = false, - overflow = TextOverflow.Ellipsis - ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + CoilImage( + imageModel = { discoveryRepository.repository.owner.avatarUrl }, + modifier = Modifier + .size(32.dp) + .clip(CircleShape), + loading = { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularWavyProgressIndicator() + } + }, + component = rememberImageComponent { + CrossfadePlugin() + } + ) - Spacer(modifier = Modifier.height(4.dp)) + Text( + text = discoveryRepository.repository.owner.login, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.outline, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis + ) - discoveryRepository.repository.description?.let { - Text( - text = it, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.bodyLarge, - softWrap = false - ) - } + Text( + text = "/ ${discoveryRepository.repository.name}", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.outline, + softWrap = false, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + modifier = Modifier.weight(1f) + ) + } - Spacer(Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(4.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { Text( - text = "⭐ ${discoveryRepository.repository.stargazersCount}", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, + text = discoveryRepository.repository.name, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, maxLines = 1, softWrap = false, overflow = TextOverflow.Ellipsis ) - Text( - text = "• 🌴 ${discoveryRepository.repository.forksCount}", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - softWrap = false, - overflow = TextOverflow.Ellipsis - ) + Spacer(modifier = Modifier.height(4.dp)) + + discoveryRepository.repository.description?.let { + Text( + text = it, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodyLarge, + softWrap = false + ) + } - discoveryRepository.repository.language?.let { + Spacer(Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { Text( - text = "• $it", + text = "⭐ ${discoveryRepository.repository.stargazersCount}", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, softWrap = false, overflow = TextOverflow.Ellipsis ) - } - } - if (discoveryRepository.isInstalled) { - Spacer(Modifier.height(12.dp)) + Text( + text = "• 🌴 ${discoveryRepository.repository.forksCount}", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis + ) - InstallStatusBadge( - isUpdateAvailable = discoveryRepository.isUpdateAvailable - ) - } + discoveryRepository.repository.language?.let { + Text( + text = "• $it", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis + ) + } + } - Spacer(Modifier.height(12.dp)) + if (discoveryRepository.isInstalled) { + Spacer(Modifier.height(12.dp)) - Text( - text = formatUpdatedAt(discoveryRepository.repository.updatedAt), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.outline, - maxLines = 1, - softWrap = false, - overflow = TextOverflow.Ellipsis - ) + InstallStatusBadge( + isUpdateAvailable = discoveryRepository.isUpdateAvailable + ) + } - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(12.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - GithubStoreButton( - text = stringResource(Res.string.home_view_details), - onClick = onClick, - modifier = Modifier.weight(1f) + Text( + text = formatUpdatedAt(discoveryRepository.repository.updatedAt), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.outline, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis ) - IconButton( - onClick = { - uriHandler.openUri(discoveryRepository.repository.htmlUrl) - }, - colors = IconButtonDefaults.iconButtonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer - ), - shapes = IconButtonDefaults.shapes(), + Spacer(Modifier.height(24.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Icon( - imageVector = Icons.Default.OpenInBrowser, - contentDescription = stringResource(Res.string.open_in_browser), + GithubStoreButton( + text = stringResource(Res.string.home_view_details), + onClick = onClick, + modifier = Modifier.weight(1f) ) + + IconButton( + onClick = { + uriHandler.openUri(discoveryRepository.repository.htmlUrl) + }, + colors = IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ), + shapes = IconButtonDefaults.shapes(), + ) { + Icon( + imageVector = Icons.Default.OpenInBrowser, + contentDescription = stringResource(Res.string.open_in_browser), + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeViewModel.kt index 14c50d414..9605bdc2d 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/home/presentation/HomeViewModel.kt @@ -46,6 +46,7 @@ class HomeViewModel( loadPlatform() loadRepos(isInitial = true) observeInstalledApps() + observeFavourites() hasLoadedInitialData = true } @@ -242,6 +243,23 @@ class HomeViewModel( } } + private fun observeFavourites() { + viewModelScope.launch { + favouritesRepository.getAllFavorites().collect { favourites -> + val favouritesMap = favourites.associateBy { it.repoId } + _state.update { current -> + current.copy( + repos = current.repos.map { homeRepo -> + homeRepo.copy( + isFavourite = favouritesMap.containsKey(homeRepo.repository.id) + ) + } + ) + } + } + } + } + override fun onCleared() { super.onCleared() currentJob?.cancel() diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchViewModel.kt index 1aed05bfd..0c8f71e9d 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/search/presentation/SearchViewModel.kt @@ -38,6 +38,7 @@ class SearchViewModel( init { syncSystemState() observeInstalledApps() + observeFavouriteApps() } private fun syncSystemState() { @@ -71,6 +72,23 @@ class SearchViewModel( } } } + private fun observeFavouriteApps() { + viewModelScope.launch { + favouritesRepository.getAllFavorites().collect { favoriteRepos -> + val installedMap = favoriteRepos.associateBy { it.repoId } + _state.update { current -> + current.copy( + repositories = current.repositories.map { searchRepo -> + val app = installedMap[searchRepo.repository.id] + searchRepo.copy( + isFavourite = app != null + ) + } + ) + } + } + } + } private fun performSearch(isInitial: Boolean = false) { if (_state.value.query.isBlank()) { From a8aa815b288325b163010d20ae376728671deb56 Mon Sep 17 00:00:00 2001 From: Rainxch Zed Date: Tue, 6 Jan 2026 11:53:49 +0500 Subject: [PATCH 09/10] feat(favourites): Implement favourites screen This commit introduces the Favourites screen, which displays a list of repositories that the user has marked as favourites. Key changes include: - A new `FavouritesScreen` with its `ViewModel` and `State` management. - A `FavouriteRepositoryItem` composable to display individual favourited repositories. - Navigation to and from the Favourites screen and to the repository details screen. - Removal of the unused `SubscribedDeveloper` and `DeveloperApp` database entities. - Added localized strings for the "added at" timestamps in various languages. - Refactored the `repositoryId` in navigation and ViewModels from `Int` to `Long` for consistency. - Moved the `UpdatedAtFormatter` to a new `TimeFormatters.kt` file. --- .../composeResources/values-bn/strings-bn.xml | 8 +- .../composeResources/values-es/strings-es.xml | 6 + .../composeResources/values-fr/strings-fr.xml | 6 + .../composeResources/values-it/strings-it.xml | 6 + .../composeResources/values-ja/strings-ja.xml | 6 + .../composeResources/values-kr/strings-kr.xml | 6 + .../composeResources/values-ru/strings-ru.xml | 6 + .../values-zh-rCN/strings-zh-rCN.xml | 6 + .../composeResources/values/strings.xml | 6 + .../githubstore/app/di/SharedModules.kt | 4 +- .../app/navigation/AppNavigation.kt | 15 +- .../app/navigation/GithubStoreGraph.kt | 2 +- .../core/data/local/db/AppDatabase.kt | 2 - .../data/local/db/entities/DeveloperApp.kt | 33 ---- .../local/db/entities/SubscribedDeveloper.kt | 18 -- .../presentation/components/RepositoryCard.kt | 2 +- .../core/presentation/utils/TimeFormatters.kt | 105 ++++++++++ .../presentation/utils/UpdatedAtFormmater.kt | 37 ---- .../feature/apps/presentation/AppsRoot.kt | 7 +- .../apps/presentation/AppsViewModel.kt | 75 ++++--- .../details/presentation/DetailsEvent.kt | 2 +- .../details/presentation/DetailsRoot.kt | 2 +- .../details/presentation/DetailsViewModel.kt | 76 +------- .../feature/favourites/FavouritesAction.kt | 6 +- .../feature/favourites/FavouritesRoot.kt | 143 +++++++++++++- .../feature/favourites/FavouritesState.kt | 8 +- .../feature/favourites/FavouritesViewModel.kt | 65 ++++++- .../components/FavouriteRepositoryItem.kt | 183 ++++++++++++++++++ .../mappers/FavouriteRepositoryMapper.kt | 21 ++ .../favourites/model/FavouriteRepository.kt | 14 ++ .../settings/presentation/SettingsRoot.kt | 5 +- 31 files changed, 679 insertions(+), 202 deletions(-) delete mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/entities/DeveloperApp.kt delete mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/entities/SubscribedDeveloper.kt create mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/utils/TimeFormatters.kt delete mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/utils/UpdatedAtFormmater.kt create mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/components/FavouriteRepositoryItem.kt create mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/mappers/FavouriteRepositoryMapper.kt create mode 100644 composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/model/FavouriteRepository.kt diff --git a/composeApp/src/commonMain/composeResources/values-bn/strings-bn.xml b/composeApp/src/commonMain/composeResources/values-bn/strings-bn.xml index 3474043fc..0c28dc331 100644 --- a/composeApp/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/composeApp/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -253,4 +253,10 @@ প্রিয় তালিকা থেকে সরান প্রিয় - + এইমাত্র যোগ করা হয়েছে + %1$d ঘণ্টা আগে যোগ করা হয়েছে + গতকাল যোগ করা হয়েছে + %1$d দিন আগে যোগ করা হয়েছে + %1$s তারিখে যোগ করা হয়েছে + + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-es/strings-es.xml b/composeApp/src/commonMain/composeResources/values-es/strings-es.xml index 22c17f7e4..7239e3cc0 100644 --- a/composeApp/src/commonMain/composeResources/values-es/strings-es.xml +++ b/composeApp/src/commonMain/composeResources/values-es/strings-es.xml @@ -200,4 +200,10 @@ Quitar de favoritos Favoritos + añadido justo ahora + añadido hace %1$d hora(s) + añadido ayer + añadido hace %1$d día(s) + añadido el %1$s + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-fr/strings-fr.xml b/composeApp/src/commonMain/composeResources/values-fr/strings-fr.xml index e552b7835..745840371 100644 --- a/composeApp/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/composeApp/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -200,4 +200,10 @@ Retirer des favoris Favoris + ajouté à l’instant + ajouté il y a %1$d heure(s) + ajouté hier + ajouté il y a %1$d jour(s) + ajouté le %1$s + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-it/strings-it.xml b/composeApp/src/commonMain/composeResources/values-it/strings-it.xml index 2b8e86a85..5b533f1fc 100644 --- a/composeApp/src/commonMain/composeResources/values-it/strings-it.xml +++ b/composeApp/src/commonMain/composeResources/values-it/strings-it.xml @@ -249,4 +249,10 @@ Rimuovi dai preferiti Preferiti + aggiunto poco fa + aggiunto %1$d ora(e) fa + aggiunto ieri + aggiunto %1$d giorno(i) fa + aggiunto il %1$s + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-ja/strings-ja.xml b/composeApp/src/commonMain/composeResources/values-ja/strings-ja.xml index b4f897293..f6f384a05 100644 --- a/composeApp/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/composeApp/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -200,4 +200,10 @@ お気に入りから削除 お気に入り + たった今追加されました + %1$d時間前に追加 + 昨日追加 + %1$d日前に追加 + %1$s に追加 + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-kr/strings-kr.xml b/composeApp/src/commonMain/composeResources/values-kr/strings-kr.xml index 65078dd91..e3cdf2508 100644 --- a/composeApp/src/commonMain/composeResources/values-kr/strings-kr.xml +++ b/composeApp/src/commonMain/composeResources/values-kr/strings-kr.xml @@ -251,4 +251,10 @@ 즐겨찾기에서 제거 즐겨찾기 + 방금 추가됨 + %1$d시간 전에 추가됨 + 어제 추가됨 + %1$d일 전에 추가됨 + %1$s에 추가됨 + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-ru/strings-ru.xml b/composeApp/src/commonMain/composeResources/values-ru/strings-ru.xml index 224685c84..80f331a25 100644 --- a/composeApp/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/composeApp/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -218,4 +218,10 @@ Удалить из избранного Избранное + только что добавлено + добавлено %1$d час(ов) назад + добавлено вчера + добавлено %1$d дн. назад + добавлено %1$s + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/composeApp/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 3a80b5010..295fcf6d0 100644 --- a/composeApp/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/composeApp/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -201,4 +201,10 @@ 从收藏中移除 收藏 + 刚刚添加 + %1$d 小时前添加 + 昨天添加 + %1$d 天前添加 + %1$s 添加 + \ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 580c57675..ddd84127e 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -253,5 +253,11 @@ Remove from favourites Favourites + added just now + added %1$d hour(s) ago + added yesterday + added %1$d day(s) ago + added on %1$s + \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt index 27f60a362..b6130edd2 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt @@ -201,7 +201,9 @@ val searchModule: Module = module { val favouritesModule: Module = module { // ViewModel viewModel { - FavouritesViewModel() + FavouritesViewModel( + favouritesRepository = get() + ) } } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index b7649763e..bdb88b87d 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -64,7 +64,7 @@ fun AppNavigation( onNavigateToDetails = { repo -> navBackStack.add( GithubStoreGraph.DetailsScreen( - repositoryId = repo.id.toInt() + repositoryId = repo.id ) ) } @@ -79,7 +79,7 @@ fun AppNavigation( onNavigateToDetails = { repo -> navBackStack.add( GithubStoreGraph.DetailsScreen( - repositoryId = repo.id.toInt() + repositoryId = repo.id ) ) } @@ -114,7 +114,14 @@ fun AppNavigation( } entry { - FavouritesRoot() + FavouritesRoot( + onNavigateBack = { + navBackStack.removeLastOrNull() + }, + onNavigateToDetails = { + navBackStack.add(GithubStoreGraph.DetailsScreen(it)) + }, + ) } entry { @@ -133,7 +140,7 @@ fun AppNavigation( onNavigateToRepo = { repoId -> navBackStack.add( GithubStoreGraph.DetailsScreen( - repositoryId = repoId.toInt() + repositoryId = repoId ) ) } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt index d68ff070f..60d3e9932 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt @@ -16,7 +16,7 @@ sealed interface GithubStoreGraph: NavKey { @Serializable data class DetailsScreen( - val repositoryId: Int + val repositoryId: Long ) : GithubStoreGraph @Serializable diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/AppDatabase.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/AppDatabase.kt index 15196d731..f269727f0 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/AppDatabase.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/AppDatabase.kt @@ -5,10 +5,8 @@ import androidx.room.RoomDatabase import zed.rainxch.githubstore.core.data.local.db.dao.FavoriteRepoDao import zed.rainxch.githubstore.core.data.local.db.dao.InstalledAppDao import zed.rainxch.githubstore.core.data.local.db.dao.UpdateHistoryDao -import zed.rainxch.githubstore.core.data.local.db.entities.DeveloperApp import zed.rainxch.githubstore.core.data.local.db.entities.FavoriteRepo import zed.rainxch.githubstore.core.data.local.db.entities.InstalledApp -import zed.rainxch.githubstore.core.data.local.db.entities.SubscribedDeveloper import zed.rainxch.githubstore.core.data.local.db.entities.UpdateHistory @Database( diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/entities/DeveloperApp.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/entities/DeveloperApp.kt deleted file mode 100644 index 75fba2d73..000000000 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/entities/DeveloperApp.kt +++ /dev/null @@ -1,33 +0,0 @@ -package zed.rainxch.githubstore.core.data.local.db.entities - -import androidx.room.Entity -import androidx.room.ForeignKey - -@Entity( - tableName = "developer_apps", - primaryKeys = ["developerLogin", "repoId"], - foreignKeys = [ - ForeignKey( - entity = SubscribedDeveloper::class, - parentColumns = ["developerLogin"], - childColumns = ["developerLogin"], - onDelete = ForeignKey.CASCADE - ) - ] -) -data class DeveloperApp( - val developerLogin: String, - val repoId: Long, - - val repoName: String, - val repoDescription: String?, - val primaryLanguage: String?, - - val latestVersion: String?, - val releaseUrl: String?, - - val isInstalled: Boolean = false, - val installedPackageName: String? = null, - - val lastUpdatedAt: Long -) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/entities/SubscribedDeveloper.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/entities/SubscribedDeveloper.kt deleted file mode 100644 index 1af5dcce7..000000000 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/data/local/db/entities/SubscribedDeveloper.kt +++ /dev/null @@ -1,18 +0,0 @@ -package zed.rainxch.githubstore.core.data.local.db.entities - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = "subscribed_developers") -data class SubscribedDeveloper( - @PrimaryKey - val developerLogin: String, - - val developerName: String?, - val developerAvatarUrl: String, - val developerBio: String?, - - val repositoryCount: Int = 0, - val subscribedAt: Long, - val lastSyncedAt: Long -) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/components/RepositoryCard.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/components/RepositoryCard.kt index 489730d38..3c2ffff9d 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/components/RepositoryCard.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/components/RepositoryCard.kt @@ -149,7 +149,7 @@ fun RepositoryCard( maxLines = 2, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodyLarge, - softWrap = false + softWrap = true ) } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/utils/TimeFormatters.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/utils/TimeFormatters.kt new file mode 100644 index 000000000..202926474 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/utils/TimeFormatters.kt @@ -0,0 +1,105 @@ +package zed.rainxch.githubstore.core.presentation.utils + +import androidx.compose.runtime.Composable +import githubstore.composeapp.generated.resources.Res +import githubstore.composeapp.generated.resources.added_days_ago +import githubstore.composeapp.generated.resources.added_hours_ago +import githubstore.composeapp.generated.resources.added_just_now +import githubstore.composeapp.generated.resources.added_on_date +import githubstore.composeapp.generated.resources.added_yesterday +import githubstore.composeapp.generated.resources.updated_days_ago +import githubstore.composeapp.generated.resources.updated_hours_ago +import githubstore.composeapp.generated.resources.updated_just_now +import githubstore.composeapp.generated.resources.updated_on_date +import githubstore.composeapp.generated.resources.updated_yesterday +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import org.jetbrains.compose.resources.getString +import org.jetbrains.compose.resources.stringResource +import kotlin.time.Clock +import kotlin.time.Duration +import kotlin.time.ExperimentalTime +import kotlin.time.Instant + +@OptIn(ExperimentalTime::class) +@Composable +fun formatUpdatedAt(isoInstant: String): String { + val updated = Instant.parse(isoInstant) + val now = Instant.fromEpochMilliseconds(Clock.System.now().toEpochMilliseconds()) + val diff: Duration = now - updated + + val hoursDiff = diff.inWholeHours + val daysDiff = diff.inWholeDays + + return when { + hoursDiff < 1 -> stringResource(Res.string.updated_just_now) + hoursDiff < 24 -> stringResource(Res.string.updated_hours_ago, hoursDiff) + daysDiff == 1L -> stringResource(Res.string.updated_yesterday) + daysDiff < 7 -> stringResource(Res.string.updated_days_ago, daysDiff) + else -> { + val date = updated.toLocalDateTime(TimeZone.currentSystemDefault()).date + stringResource(Res.string.updated_on_date, date.toString()) + } + } +} + +@OptIn(ExperimentalTime::class) +suspend fun formatUpdatedAt(epochMillis: Long): String { + val updated = Instant.fromEpochMilliseconds(epochMillis) + val now = Clock.System.now() + val diff: Duration = now - updated + + val hoursDiff = diff.inWholeHours + val daysDiff = diff.inWholeDays + + return when { + hoursDiff < 1 -> + getString(Res.string.updated_just_now) + + hoursDiff < 24 -> + getString(Res.string.updated_hours_ago, hoursDiff) + + daysDiff == 1L -> + getString(Res.string.updated_yesterday) + + daysDiff < 7 -> + getString(Res.string.updated_days_ago, daysDiff) + + else -> { + val date = updated + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date + getString(Res.string.updated_on_date, date.toString()) + } + } +} +@OptIn(ExperimentalTime::class) +suspend fun formatAddedAt(epochMillis: Long): String { + val updated = Instant.fromEpochMilliseconds(epochMillis) + val now = Clock.System.now() + val diff: Duration = now - updated + + val hoursDiff = diff.inWholeHours + val daysDiff = diff.inWholeDays + + return when { + hoursDiff < 1 -> + getString(Res.string.added_just_now) + + hoursDiff < 24 -> + getString(Res.string.added_hours_ago, hoursDiff) + + daysDiff == 1L -> + getString(Res.string.added_yesterday) + + daysDiff < 7 -> + getString(Res.string.added_days_ago, daysDiff) + + else -> { + val date = updated + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date + getString(Res.string.added_on_date, date.toString()) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/utils/UpdatedAtFormmater.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/utils/UpdatedAtFormmater.kt deleted file mode 100644 index b96a4b26f..000000000 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/utils/UpdatedAtFormmater.kt +++ /dev/null @@ -1,37 +0,0 @@ -package zed.rainxch.githubstore.core.presentation.utils - -import androidx.compose.runtime.Composable -import githubstore.composeapp.generated.resources.Res -import githubstore.composeapp.generated.resources.updated_days_ago -import githubstore.composeapp.generated.resources.updated_hours_ago -import githubstore.composeapp.generated.resources.updated_just_now -import githubstore.composeapp.generated.resources.updated_yesterday -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime -import org.jetbrains.compose.resources.stringResource -import kotlin.time.Clock -import kotlin.time.Duration -import kotlin.time.ExperimentalTime -import kotlin.time.Instant - -@OptIn(ExperimentalTime::class) -@Composable -fun formatUpdatedAt(isoInstant: String): String { - val updated = Instant.parse(isoInstant) - val now = Instant.fromEpochMilliseconds(Clock.System.now().toEpochMilliseconds()) - val diff: Duration = now - updated - - val hoursDiff = diff.inWholeHours - val daysDiff = diff.inWholeDays - - return when { - hoursDiff < 1 -> stringResource(Res.string.updated_just_now) - hoursDiff < 24 -> stringResource(Res.string.updated_hours_ago, hoursDiff) - daysDiff == 1L -> stringResource(Res.string.updated_yesterday) - daysDiff < 7 -> stringResource(Res.string.updated_days_ago, daysDiff) - else -> { - val date = updated.toLocalDateTime(TimeZone.currentSystemDefault()).date - "updated on $date" - } - } -} diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsRoot.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsRoot.kt index f82edf284..60259e411 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsRoot.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsRoot.kt @@ -44,6 +44,7 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -139,7 +140,7 @@ fun AppsScreen( ) { Scaffold( topBar = { - CenterAlignedTopAppBar( + TopAppBar( navigationIcon = { IconButton( onClick = { onAction(AppsAction.OnNavigateBackClick) } @@ -152,7 +153,9 @@ fun AppsScreen( }, title = { Text( - text = stringResource(Res.string.installed_apps) + text = stringResource(Res.string.installed_apps), + style = MaterialTheme.typography.titleMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface ) }, actions = { diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsViewModel.kt index e6b6a571c..4d2ebae80 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/apps/presentation/AppsViewModel.kt @@ -78,7 +78,6 @@ class AppsViewModel( _state.update { it.copy(isLoading = true) } try { - // Sync system state using shared use case val syncResult = syncInstalledAppsUseCase() if (syncResult.isFailure) { Logger.w { "Sync had issues but continuing: ${syncResult.exceptionOrNull()?.message}" } @@ -131,29 +130,35 @@ class AppsViewModel( if (platform.type == PlatformType.ANDROID) { val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName) if (systemInfo != null) { - installedAppsRepository.updateApp(app.copy( - installedVersionName = systemInfo.versionName, - installedVersionCode = systemInfo.versionCode, - latestVersionName = systemInfo.versionName, - latestVersionCode = systemInfo.versionCode - )) + installedAppsRepository.updateApp( + app.copy( + installedVersionName = systemInfo.versionName, + installedVersionCode = systemInfo.versionCode, + latestVersionName = systemInfo.versionName, + latestVersionCode = systemInfo.versionCode + ) + ) Logger.d { "Migrated ${app.packageName}: set versionName/code from system" } } else { - installedAppsRepository.updateApp(app.copy( + installedAppsRepository.updateApp( + app.copy( + installedVersionName = app.installedVersion, + installedVersionCode = 0L, + latestVersionName = app.installedVersion, + latestVersionCode = 0L + ) + ) + Logger.d { "Migrated ${app.packageName}: fallback to tag as versionName" } + } + } else { + installedAppsRepository.updateApp( + app.copy( installedVersionName = app.installedVersion, installedVersionCode = 0L, latestVersionName = app.installedVersion, latestVersionCode = 0L - )) - Logger.d { "Migrated ${app.packageName}: fallback to tag as versionName" } - } - } else { - installedAppsRepository.updateApp(app.copy( - installedVersionName = app.installedVersion, - installedVersionCode = 0L, - latestVersionName = app.installedVersion, - latestVersionCode = 0L - )) + ) + ) Logger.d { "Migrated ${app.packageName} (desktop): fallback to tag as versionName" } } } @@ -227,14 +232,26 @@ class AppsViewModel( onCantLaunchApp = { viewModelScope.launch { _events.send( - AppsEvent.ShowError(getString(Res.string.cannot_launch, arrayOf(app.appName))) + AppsEvent.ShowError( + getString( + Res.string.cannot_launch, + arrayOf(app.appName) + ) + ) ) } } ) } catch (e: Exception) { Logger.e { "Failed to open app: ${e.message}" } - _events.send(AppsEvent.ShowError(getString(Res.string.failed_to_open, arrayOf(app.appName)))) + _events.send( + AppsEvent.ShowError( + getString( + Res.string.failed_to_open, + arrayOf(app.appName) + ) + ) + ) } } } @@ -351,7 +368,14 @@ class AppsViewModel( app.packageName, UpdateState.Error(e.message ?: "Update failed") ) - _events.send(AppsEvent.ShowError(getString(Res.string.failed_to_update, arrayOf(app.appName, e.message?:"")))) + _events.send( + AppsEvent.ShowError( + getString( + Res.string.failed_to_update, + arrayOf(app.appName, e.message ?: "") + ) + ) + ) } finally { activeUpdates.remove(app.packageName) } @@ -413,7 +437,14 @@ class AppsViewModel( Logger.d { "Update all cancelled" } } catch (e: Exception) { Logger.e { "Update all failed: ${e.message}" } - _events.send(AppsEvent.ShowError(getString(Res.string.update_all_failed, arrayOf(e.message)))) + _events.send( + AppsEvent.ShowError( + getString( + Res.string.update_all_failed, + arrayOf(e.message) + ) + ) + ) } finally { _state.update { it.copy( diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsEvent.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsEvent.kt index 9f7de00fd..0bbcb5428 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsEvent.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsEvent.kt @@ -1,7 +1,7 @@ package zed.rainxch.githubstore.feature.details.presentation sealed interface DetailsEvent { - data class OnOpenRepositoryInApp(val repositoryId: Int) : DetailsEvent + data class OnOpenRepositoryInApp(val repositoryId: Long) : DetailsEvent data class InstallTrackingFailed(val message: String) : DetailsEvent data class OnMessage(val message: String) : DetailsEvent } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt index a4d1aebbd..695678fc2 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsRoot.kt @@ -66,7 +66,7 @@ import zed.rainxch.githubstore.feature.details.presentation.utils.isLiquidTopbar @Composable fun DetailsRoot( - onOpenRepositoryInApp: (repoId: Int) -> Unit, + onOpenRepositoryInApp: (repoId: Long) -> Unit, onNavigateBack: () -> Unit, viewModel: DetailsViewModel = koinViewModel() ) { diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt index b2d09ed61..85842ccc7 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/details/presentation/DetailsViewModel.kt @@ -7,20 +7,17 @@ import githubstore.composeapp.generated.resources.Res import githubstore.composeapp.generated.resources.added_to_favourites import githubstore.composeapp.generated.resources.installer_saved_downloads import githubstore.composeapp.generated.resources.removed_from_favourites -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.format @@ -45,7 +42,7 @@ import kotlin.time.Clock.System import kotlin.time.ExperimentalTime class DetailsViewModel( - private val repositoryId: Int, + private val repositoryId: Long, private val detailsRepository: DetailsRepository, private val downloader: Downloader, private val installer: Installer, @@ -89,7 +86,7 @@ class DetailsViewModel( Logger.w { "Sync had issues but continuing: ${syncResult.exceptionOrNull()?.message}" } } - val repo = detailsRepository.getRepositoryById(repositoryId.toLong()) + val repo = detailsRepository.getRepositoryById(repositoryId) val isFavoriteDeferred = async { try { favouritesRepository.isFavoriteSync(repo.id) @@ -121,7 +118,7 @@ class DetailsViewModel( val statsDeferred = async { try { detailsRepository.getRepoStats(owner, name) - } catch (t: Throwable) { + } catch (_: Throwable) { null } } @@ -133,7 +130,7 @@ class DetailsViewModel( repo = name, defaultBranch = repo.defaultBranch ) - } catch (t: Throwable) { + } catch (_: Throwable) { null } } @@ -220,61 +217,6 @@ class DetailsViewModel( } } - private suspend fun syncSystemExistenceAndMigrate() { - withContext(Dispatchers.IO) { - try { - val installedPackageNames = packageMonitor.getAllInstalledPackageNames() - val appsInDb = installedAppsRepository.getAllInstalledApps().first() - - appsInDb.forEach { app -> - if (!installedPackageNames.contains(app.packageName)) { - Logger.d { "App ${app.packageName} no longer installed, removing from DB" } - installedAppsRepository.deleteInstalledApp(app.packageName) - } else if (app.installedVersionName == null) { - if (platform.type == PlatformType.ANDROID) { - val systemInfo = packageMonitor.getInstalledPackageInfo(app.packageName) - if (systemInfo != null) { - installedAppsRepository.updateApp( - app.copy( - installedVersionName = systemInfo.versionName, - installedVersionCode = systemInfo.versionCode, - latestVersionName = systemInfo.versionName, - latestVersionCode = systemInfo.versionCode - ) - ) - Logger.d { "Migrated ${app.packageName}: set versionName/code from system" } - } else { - installedAppsRepository.updateApp( - app.copy( - installedVersionName = app.installedVersion, - installedVersionCode = 0L, - latestVersionName = app.installedVersion, - latestVersionCode = 0L - ) - ) - Logger.d { "Migrated ${app.packageName}: fallback to tag" } - } - } else { - installedAppsRepository.updateApp( - app.copy( - installedVersionName = app.installedVersion, - installedVersionCode = 0L, - latestVersionName = app.installedVersion, - latestVersionCode = 0L - ) - ) - Logger.d { "Migrated ${app.packageName} (desktop): fallback to tag" } - } - } - } - - Logger.d { "System existence sync and data migration completed" } - } catch (e: Exception) { - Logger.e { "Failed to sync existence or migrate data: ${e.message}" } - } - } - } - @OptIn(ExperimentalTime::class) fun onAction(action: DetailsAction) { when (action) { @@ -671,7 +613,7 @@ class DetailsViewModel( var packageName: String var appName = repo.name var versionName: String? = null - var versionCode: Long = 0L + var versionCode = 0L if (platform.type == PlatformType.ANDROID && assetName.lowercase().endsWith(".apk")) { val apkInfo = installer.getApkInfoExtractor().extractPackageInfo(filePath) @@ -852,12 +794,8 @@ class DetailsViewModel( } } - private fun normalizeVersion(version: String): String { - return version.removePrefix("v").removePrefix("V").trim() - } - private companion object { - const val OBTAINIUM_REPO_ID = 523534328 - const val APP_MANAGER_REPO_ID = 268006778 + const val OBTAINIUM_REPO_ID : Long = 523534328 + const val APP_MANAGER_REPO_ID : Long = 268006778 } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesAction.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesAction.kt index 770e7f47b..c940d46db 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesAction.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesAction.kt @@ -1,5 +1,9 @@ package zed.rainxch.githubstore.feature.favourites -sealed interface FavouritesAction { +import zed.rainxch.githubstore.feature.favourites.model.FavouriteRepository +sealed interface FavouritesAction { + data object OnNavigateBackClick : FavouritesAction + data class OnToggleFavorite(val favouriteRepository: FavouriteRepository) : FavouritesAction + data class OnRepositoryClick(val favouriteRepository: FavouriteRepository) : FavouritesAction } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesRoot.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesRoot.kt index a3b47c845..b813466cd 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesRoot.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesRoot.kt @@ -1,32 +1,150 @@ package zed.rainxch.githubstore.feature.favourites +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.foundation.lazy.staggeredgrid.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.CircularWavyProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import githubstore.composeapp.generated.resources.Res +import githubstore.composeapp.generated.resources.favourites +import githubstore.composeapp.generated.resources.navigate_back +import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview import org.koin.compose.viewmodel.koinViewModel import zed.rainxch.githubstore.core.presentation.theme.GithubStoreTheme +import zed.rainxch.githubstore.feature.favourites.components.FavouriteRepositoryItem @Composable fun FavouritesRoot( + onNavigateBack: () -> Unit, + onNavigateToDetails: (repoId: Long) -> Unit, viewModel: FavouritesViewModel = koinViewModel() ) { val state by viewModel.state.collectAsStateWithLifecycle() FavouritesScreen( state = state, - onAction = viewModel::onAction + onAction = { action -> + when (action) { + FavouritesAction.OnNavigateBackClick -> { + onNavigateBack() + } + + is FavouritesAction.OnRepositoryClick -> { + onNavigateToDetails(action.favouriteRepository.repoId) + } + + else -> { + viewModel.onAction(action) + } + } + } ) } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun FavouritesScreen( state: FavouritesState, onAction: (FavouritesAction) -> Unit, ) { + Scaffold( + topBar = { + FavouritesTopbar(onAction, state) + }, + containerColor = MaterialTheme.colorScheme.background, + ) { innerPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Adaptive( + 350.dp + ), + verticalItemSpacing = 12.dp, + horizontalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 12.dp), + modifier = Modifier.fillMaxSize() + ) { + items( + items = state.favouriteRepositories, + key = { it.repoId } + ) { repo -> + FavouriteRepositoryItem( + favouriteRepository = repo, + onToggleFavouriteClick = { + onAction(FavouritesAction.OnToggleFavorite(repo)) + }, + onItemClick = { + onAction(FavouritesAction.OnRepositoryClick(repo)) + }, + modifier = Modifier.animateItem() + ) + } + } + + if (state.isLoading) { + CircularWavyProgressIndicator() + } + } + } +} +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun FavouritesTopbar( + onAction: (FavouritesAction) -> Unit, + state: FavouritesState, +) { + TopAppBar( + title = { + Text( + text = stringResource(Res.string.favourites), + style = MaterialTheme.typography.titleMediumEmphasized, + color = MaterialTheme.colorScheme.onSurface + ) + }, + navigationIcon = { + IconButton( + shapes = IconButtonDefaults.shapes(), + onClick = { + onAction(FavouritesAction.OnNavigateBackClick) + } + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(Res.string.navigate_back), + modifier = Modifier.size(24.dp) + ) + } + } + ) } + @Preview @Composable private fun Preview() { @@ -36,4 +154,27 @@ private fun Preview() { onAction = {} ) } +} + +class Container { + private val y = 0 + private fun hi() { + val innerClass = InnerClass() + + } + + inner class InnerClass { + private val innerValue = 0 + fun main() { + println(y) + hi() + } + } +} + +class OtherClass { + fun main() { + val container = Container() + val another = container.InnerClass() + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesState.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesState.kt index f9c9b9e56..fa4488680 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesState.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesState.kt @@ -1,6 +1,10 @@ package zed.rainxch.githubstore.feature.favourites +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import zed.rainxch.githubstore.feature.favourites.model.FavouriteRepository + data class FavouritesState( - val paramOne: String = "default", - val paramTwo: List = emptyList(), + val favouriteRepositories: ImmutableList = persistentListOf(), + val isLoading: Boolean = false, ) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesViewModel.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesViewModel.kt index 11e41eb9c..2a27f2e4d 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesViewModel.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesViewModel.kt @@ -2,12 +2,27 @@ package zed.rainxch.githubstore.feature.favourites import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import zed.rainxch.githubstore.core.data.local.db.entities.FavoriteRepo +import zed.rainxch.githubstore.core.domain.repository.FavouritesRepository +import zed.rainxch.githubstore.feature.favourites.mappers.toFavouriteRepositoryUi +import zed.rainxch.githubstore.feature.favourites.model.FavouriteRepository +import kotlin.time.Clock +import kotlin.time.ExperimentalTime -class FavouritesViewModel : ViewModel() { +class FavouritesViewModel( + private val favouritesRepository: FavouritesRepository +) : ViewModel() { private var hasLoadedInitialData = false @@ -15,7 +30,8 @@ class FavouritesViewModel : ViewModel() { val state = _state .onStart { if (!hasLoadedInitialData) { - /** Load initial data here **/ + loadFavouriteRepos() + hasLoadedInitialData = true } } @@ -25,9 +41,52 @@ class FavouritesViewModel : ViewModel() { initialValue = FavouritesState() ) + private fun loadFavouriteRepos() { + viewModelScope.launch { + favouritesRepository + .getAllFavorites() + .map { it.map { it.toFavouriteRepositoryUi() } } + .flowOn(Dispatchers.Default) + .collect { favoriteRepos -> + _state.update { it.copy( + favouriteRepositories = favoriteRepos.toImmutableList() + ) } + } + } + } + + @OptIn(ExperimentalTime::class) fun onAction(action: FavouritesAction) { when (action) { - else -> TODO("Handle actions") + FavouritesAction.OnNavigateBackClick -> { + // Handled in composable + } + + is FavouritesAction.OnRepositoryClick -> { + // Handled in composable + } + + is FavouritesAction.OnToggleFavorite -> { + viewModelScope.launch { + val repo = action.favouriteRepository + + val favoriteRepo = FavoriteRepo( + repoId = repo.repoId, + repoName = repo.repoName, + repoOwner = repo.repoOwner, + repoOwnerAvatarUrl = repo.repoOwnerAvatarUrl, + repoDescription = repo.repoDescription, + primaryLanguage = repo.primaryLanguage, + repoUrl = repo.repoUrl, + latestVersion = repo.latestRelease, + latestReleaseUrl = repo.latestReleaseUrl, + addedAt = Clock.System.now().toEpochMilliseconds(), + lastSyncedAt = Clock.System.now().toEpochMilliseconds() + ) + + favouritesRepository.toggleFavorite(favoriteRepo) + } + } } } diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/components/FavouriteRepositoryItem.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/components/FavouriteRepositoryItem.kt new file mode 100644 index 000000000..3c3c2069b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/components/FavouriteRepositoryItem.kt @@ -0,0 +1,183 @@ +package zed.rainxch.githubstore.feature.favourites.components + +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.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularWavyProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.skydoves.landscapist.coil3.CoilImage +import com.skydoves.landscapist.components.rememberImageComponent +import com.skydoves.landscapist.crossfade.CrossfadePlugin +import zed.rainxch.githubstore.feature.favourites.model.FavouriteRepository + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun FavouriteRepositoryItem( + favouriteRepository: FavouriteRepository, + onToggleFavouriteClick: () -> Unit, + onItemClick: () -> Unit, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier.fillMaxWidth(), + onClick = onItemClick, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer + ), + shape = RoundedCornerShape(24.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + CoilImage( + imageModel = { favouriteRepository.repoOwnerAvatarUrl }, + modifier = Modifier + .size(32.dp) + .clip(CircleShape), + loading = { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularWavyProgressIndicator() + } + }, + component = rememberImageComponent { + CrossfadePlugin() + } + ) + + Text( + text = favouriteRepository.repoOwner, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.outline, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + + Row ( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Column ( + modifier = Modifier.weight(1f) + ) { + Text( + text = favouriteRepository.repoName, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis + ) + + favouriteRepository.repoDescription?.let { + Spacer(Modifier.height(4.dp)) + + Text( + text = it, + fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + softWrap = true, + overflow = TextOverflow.Ellipsis + ) + } + } + + IconButton( + onClick = { + onToggleFavouriteClick() + }, + colors = IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + shapes = IconButtonDefaults.shapes() + ) { + Icon( + imageVector = Icons.Default.Favorite, + contentDescription = null + ) + } + } + + favouriteRepository.primaryLanguage?.let { + Spacer(modifier = Modifier.height(6.dp)) + + Text( + text = it, + fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primaryFixed, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis + ) + } + favouriteRepository.latestRelease?.let { + Spacer(modifier = Modifier.height(6.dp)) + + Text( + text = it, + fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis + ) + } + + Spacer(modifier = Modifier.height(6.dp)) + + Text( + text = favouriteRepository.addedAtFormatter, + fontWeight = FontWeight.Medium, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/mappers/FavouriteRepositoryMapper.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/mappers/FavouriteRepositoryMapper.kt new file mode 100644 index 000000000..f810d8a80 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/mappers/FavouriteRepositoryMapper.kt @@ -0,0 +1,21 @@ +package zed.rainxch.githubstore.feature.favourites.mappers + +import zed.rainxch.githubstore.core.data.local.db.entities.FavoriteRepo +import zed.rainxch.githubstore.core.presentation.utils.formatAddedAt +import zed.rainxch.githubstore.core.presentation.utils.formatUpdatedAt +import zed.rainxch.githubstore.feature.favourites.model.FavouriteRepository + +suspend fun FavoriteRepo.toFavouriteRepositoryUi(): FavouriteRepository { + return FavouriteRepository( + repoId = repoId, + repoName = repoName, + repoOwner = repoOwner, + repoOwnerAvatarUrl = repoOwnerAvatarUrl, + repoDescription = repoDescription, + primaryLanguage = primaryLanguage, + repoUrl = repoUrl, + latestRelease = latestVersion, + latestReleaseUrl = latestReleaseUrl, + addedAtFormatter = formatAddedAt(addedAt) + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/model/FavouriteRepository.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/model/FavouriteRepository.kt new file mode 100644 index 000000000..fe00cf817 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/model/FavouriteRepository.kt @@ -0,0 +1,14 @@ +package zed.rainxch.githubstore.feature.favourites.model + +data class FavouriteRepository( + val repoId: Long, + val repoName: String, + val repoOwner: String, + val repoOwnerAvatarUrl: String, + val repoDescription: String?, + val primaryLanguage: String?, + val repoUrl: String, + val addedAtFormatter: String, + val latestRelease: String?, + val latestReleaseUrl: String?, +) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/settings/presentation/SettingsRoot.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/settings/presentation/SettingsRoot.kt index ea29dcaaf..78ad7596b 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/settings/presentation/SettingsRoot.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/settings/presentation/SettingsRoot.kt @@ -19,6 +19,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -170,7 +171,7 @@ fun SettingsScreen( @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable private fun TopAppBar(onAction: (SettingsAction) -> Unit) { - CenterAlignedTopAppBar( + TopAppBar( navigationIcon = { IconButton( shapes = IconButtonDefaults.shapes(), @@ -188,7 +189,7 @@ private fun TopAppBar(onAction: (SettingsAction) -> Unit) { title = { Text( text = stringResource(Res.string.settings_title), - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.titleMediumEmphasized, color = MaterialTheme.colorScheme.onSurface ) } From 581cfd63a9c882a07b7055512d1af9e929a9e8e2 Mon Sep 17 00:00:00 2001 From: Rainxch Zed Date: Tue, 6 Jan 2026 12:01:21 +0500 Subject: [PATCH 10/10] feat(a11y): Add content description to favourites icon This commit adds a content description to the "remove from favourites" icon button, making it more accessible. Additionally, it refactors the Favourites screen by: - Centering the loading indicator. - Removing an unused parameter from the `FavouritesTopbar`. - Deleting unused code. --- .../presentation/components/RepositoryCard.kt | 2 +- .../feature/favourites/FavouritesRoot.kt | 31 +++---------------- .../components/FavouriteRepositoryItem.kt | 5 ++- 3 files changed, 10 insertions(+), 28 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/components/RepositoryCard.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/components/RepositoryCard.kt index 3c2ffff9d..03637029d 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/components/RepositoryCard.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/core/presentation/components/RepositoryCard.kt @@ -321,7 +321,7 @@ fun RepositoryCardPreview() { language = "Kotlin", topics = null, releasesUrl = "", - updatedAt = "", + updatedAt = "2025-12-01T12:00:00Z", defaultBranch = "" ), isUpdateAvailable = true, diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesRoot.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesRoot.kt index b813466cd..ffb1bc422 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesRoot.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/FavouritesRoot.kt @@ -23,6 +23,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -71,7 +72,7 @@ fun FavouritesScreen( ) { Scaffold( topBar = { - FavouritesTopbar(onAction, state) + FavouritesTopbar(onAction) }, containerColor = MaterialTheme.colorScheme.background, ) { innerPadding -> @@ -107,7 +108,9 @@ fun FavouritesScreen( } if (state.isLoading) { - CircularWavyProgressIndicator() + CircularWavyProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) } } } @@ -117,7 +120,6 @@ fun FavouritesScreen( @Composable private fun FavouritesTopbar( onAction: (FavouritesAction) -> Unit, - state: FavouritesState, ) { TopAppBar( title = { @@ -155,26 +157,3 @@ private fun Preview() { ) } } - -class Container { - private val y = 0 - private fun hi() { - val innerClass = InnerClass() - - } - - inner class InnerClass { - private val innerValue = 0 - fun main() { - println(y) - hi() - } - } -} - -class OtherClass { - fun main() { - val container = Container() - val another = container.InnerClass() - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/components/FavouriteRepositoryItem.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/components/FavouriteRepositoryItem.kt index 3c3c2069b..b6dd6a19a 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/components/FavouriteRepositoryItem.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/feature/favourites/components/FavouriteRepositoryItem.kt @@ -33,6 +33,9 @@ import androidx.compose.ui.unit.dp import com.skydoves.landscapist.coil3.CoilImage import com.skydoves.landscapist.components.rememberImageComponent import com.skydoves.landscapist.crossfade.CrossfadePlugin +import githubstore.composeapp.generated.resources.Res +import githubstore.composeapp.generated.resources.remove_from_favourites +import org.jetbrains.compose.resources.stringResource import zed.rainxch.githubstore.feature.favourites.model.FavouriteRepository @OptIn(ExperimentalMaterial3ExpressiveApi::class) @@ -135,7 +138,7 @@ fun FavouriteRepositoryItem( ) { Icon( imageVector = Icons.Default.Favorite, - contentDescription = null + contentDescription = stringResource(Res.string.remove_from_favourites) ) } }