From 7f45ab26b4c75cb1c76acf4acf35c2d611474ec9 Mon Sep 17 00:00:00 2001 From: sosauce2 <98750531+sosauce@users.noreply.github.com> Date: Wed, 25 Sep 2024 18:21:46 +0200 Subject: [PATCH] v2.1.0 --- .DS_Store | Bin 8196 -> 8196 bytes ...kotlin-compiler-6227421308228870792.salive | 0 app/.DS_Store | Bin 8196 -> 8196 bytes app/build.gradle.kts | 4 +- app/release/baselineProfiles/0/app-release.dm | Bin 7183 -> 7194 bytes app/release/baselineProfiles/1/app-release.dm | Bin 7151 -> 7183 bytes app/release/output-metadata.json | 4 +- .../main/java/com/sosauce/cutemusic/.DS_Store | Bin 6148 -> 6148 bytes .../cutemusic/data/actions/PlayerActions.kt | 1 + .../cutemusic/data/datastore/DataStore.kt | 22 ++ .../cutemusic/data/datastore/SettingsExt.kt | 36 +++ .../com/sosauce/cutemusic/di/AppModule.kt | 2 +- .../sosauce/cutemusic/domain/model/Lyrics.kt | 6 + .../domain/repository/MediaStoreHelper.kt | 17 +- .../java/com/sosauce/cutemusic/main/App.kt | 4 + .../sosauce/cutemusic/main/MainActivity.kt | 4 +- .../sosauce/cutemusic/main/PlaybackService.kt | 4 + .../main/quickplay/QuickPlayActivity.kt | 2 +- .../cutemusic/ui/navigation/Navigation.kt | 16 +- .../ui/screens/album/AlbumDetailsLandscape.kt | 7 +- .../ui/screens/album/AlbumDetailsScreen.kt | 130 +++----- .../cutemusic/ui/screens/album/AlbumScreen.kt | 149 ++++----- .../ui/screens/album/AlbumScreenLandscape.kt | 127 ++------ .../ui/screens/artist/ArtistDetails.kt | 167 ++-------- .../screens/artist/ArtistDetailsLandscape.kt | 116 ++++--- .../ui/screens/artist/ArtistsScreen.kt | 68 ++-- .../screens/artist/ArtistsScreenLandscape.kt | 41 +-- .../cutemusic/ui/screens/lyrics/LyricsView.kt | 219 +++++++++++++ .../lyrics/components/SongInfoLyrics.kt | 62 ++++ .../cutemusic/ui/screens/main/MainScreen.kt | 287 +++++++++-------- .../ui/screens/main/MainScreenLandscape.kt | 183 +++++------ .../main/components/BottomSheetContent.kt | 7 +- .../ui/screens/metadata/MetadataEditor.kt | 11 + .../ui/screens/metadata/MetadataViewModel.kt | 5 +- .../ui/screens/playing/NowPlayingLandscape.kt | 291 ++++++++++-------- .../ui/screens/playing/NowPlayingScreen.kt | 98 ++++-- .../ui/screens/playing/components/Buttons.kt | 23 +- .../screens/playing/components/SpeedCard.kt | 177 ++++++++--- .../screens/settings/compenents/Switches.kt | 9 +- .../ui/shared_components/CuteText.kt | 10 +- .../ui/shared_components/MusicViewModel.kt | 91 +++++- .../ui/shared_components/PostViewModel.kt | 16 +- .../ui/shared_components/RadioButtons.kt | 13 +- .../ui/shared_components/Searchbar.kt | 39 ++- .../com/sosauce/cutemusic/utils/Extensions.kt | 59 ++++ gradle/libs.versions.toml | 14 +- 46 files changed, 1513 insertions(+), 1028 deletions(-) create mode 100644 .kotlin/sessions/kotlin-compiler-6227421308228870792.salive create mode 100644 app/src/main/java/com/sosauce/cutemusic/domain/model/Lyrics.kt create mode 100644 app/src/main/java/com/sosauce/cutemusic/ui/screens/lyrics/LyricsView.kt create mode 100644 app/src/main/java/com/sosauce/cutemusic/ui/screens/lyrics/components/SongInfoLyrics.kt create mode 100644 app/src/main/java/com/sosauce/cutemusic/utils/Extensions.kt diff --git a/.DS_Store b/.DS_Store index 9ae199f9391f966cbfa5f57f72f9db0bb7d0766c..fb9ffd8479591b2c6412312e3e82df94fde078c7 100644 GIT binary patch delta 141 zcmZp1XmOa}&&abeU^hP_&t@Khxhx!Jre-<{hGwRdHweg1J||+u&B~C(P|A?Wki(EN eSxTfIU4aU+g3Uig1erFoOE9t`ixQ)aj|l(*XCpiS delta 65 zcmZp1XmOa}&&ahgU^hP_*Jd7pxhx{41_n9`CT1qJItta6Mj*C{@#MWC-kVv)n3yIu O{N2nh!N>~b`~m<@YZ6uf diff --git a/.kotlin/sessions/kotlin-compiler-6227421308228870792.salive b/.kotlin/sessions/kotlin-compiler-6227421308228870792.salive new file mode 100644 index 0000000..e69de29 diff --git a/app/.DS_Store b/app/.DS_Store index 86581bb001372a1d16594a8f1ec83cc8482eb861..503656357ac9fb4478e3fbc0165cd44a6adeb2fa 100644 GIT binary patch delta 33 ocmZp1XmQxERzSqe)J#Xg(9E<}N1@u%$UsNI#Mo@}K>=re0HvY{F#rGn delta 16 XcmZp1XmQxER$#KLxaa0?0(|@cI5P$W diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8078b95..6a11625 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "com.sosauce.cutemusic" minSdk = 26 targetSdk = 35 - versionCode = 10 - versionName = "2.0.0" + versionCode = 11 + versionName = "2.1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm index fc3ccb8f0eed1e23f1b86a1ac875de6e8ce5c7c3..590055c64dd9a1b7633d1ae74b0b2d5b0d7c42eb 100644 GIT binary patch literal 7194 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 delta 7090 zcmY+}XE@t$_$Y9rEvl%Rt&ystc2S!|tG_C0mD;0r?Y$*mt(La-rbbH5BKF>U#|~ly zL2P2g=JfyOf6jTX>waF`*ZuMyFFwDdqNJ!aRY`Br6OogX6PZK`$5648axgE%2yp=i zBt%4S9}p3d5)lz8|NExtiHO#ch=_=Z8sp<7)E^QNiF*+dQ4(1?ntOOydk9%s`}z`3 z>di)uKfj;;g2Qf{vgx;XRVg{2Ah{duek<8NncRM_?SAMbWlI|?{9SjSO~8zV2}fa> zeDyHvaCi@**E?R-Jy$f}RkV+5k_0d=1pT!%@KDW(P9cO;tz;(mAX2x4%GHajv6i$N z@RmfL^zab-i_eic=id_FQ9fM+z#4EQh}^lU;F8>NI5e)r@3oldT1Su81hx^@SY zm(APp!>u5~e~1w^J-pw*uuRBHGN8h<>&s_B{GWk+n8w7jUj(VEk<1 zeTu#5^c}`Q>E_ww(N`H;gJo+`Ob0n}9Wgq@hTD}K+;5q`n}LZiTv{m4tQA(f#7@dd-RgjA4)GL*x2g&I)ICq8hbx)6#h@6=!O$ z<*xCp;?Dxk@8g45yPtUp;5$wS5rVUoHd5ATW7$Wg8nF6{1)f}- zXtlUglo3|I2R_`hT4i)$KMXYqNweYU7p2%#?iJhJn)km3YDQ^(rQ%&EglVn3(r)Xw zA@2~N;Wfe+V_kv@@U9agtCU7m>;NR2i|Ls!@s2Un)p*hi;-WWpm$Rr!NGWe3t#3khUC;KkfG5Dr{haU)d7egFA5z~{a7GE3{d62RYvFjjTr`P)qeq^qP2v058SZ6uOY*11mp)QydcRPNifTMJcVD6bg$uQvaXn- zufGUK0;`thr4wcD;+rwK@zzc*6Wo<=6Kw8rV9Mu4>tXp$^?WVwYy=oS5$eWq~1?W$z%0Gk>SvhWR| zw#&3nr+2MOgmr84o9Wd?@1Zm!7?vhGqd(#9f}Fv@qJ*;l7wG}$CDvAD z18{n`Z%|0KP-B36KF_ieHfaTl4wYTd_fo_QzgAD86*kW7Z}9&8V$3Nvx5RV3!3Mw& zU4^ngr%AT;sPCsv^x)P;lSQCD@FJukRW4C|$?Xp!{w9a<9amWVDSPLXrL4+lIq~KX z-!7Ntw;r?4)-$Db*_Lujx5u^QOfJ3B4}j>WkO~FH>hu;7An#I*gwZY)dFtqT6lIYhgIrGdwaoyQF%y{HNdp=Ns)efWvm} z0G%d^PvoIZlkS%+QS1K6*ymK4_*pLf%W7xk^n%KJ_sjx@onRJAW_Ma-N#3ZP`rp081{-`x!0Z}7_tUoYT&R9mCFtH1Y+nri-+hT~yR zDmr0c)9TLmLvI6As>$5L?*e3)pr>~d(>pruJzrude>9Zx@pxqBwfZec+CD&7DT&Le z`7lty!!mqdu)ALTW>R_i2VlLNosjzaByqgy`nb@(<|~uW)QW=CluQNoP5AbE8^#AIE`t; z&^z{;wYb9arPkmFUE{LY3n;+ZrD3I-aZCD!md=lX)2%ii=_e~GV2IZM>U9_z)A^yQ z65!~%Ra~l>p9^ra28Y>q4j_-qNtOLR;UU+4e(YogZ@_w3XEC;-=DPaU1}nIM-Uty^ zC^Z-gJ+QKG;XM*Ccw${4f7tVWPRvXI;Lj5<)Fi#i6I;7zcPM(}SGtCq*td5Ry4Ej-{n=_%>jd$dp=mi?wi>HncO{ucc&|(GDu32~aj_i)UTIAR z_s(0aaws2%3=^*_of{O} z`U@ugUi+be6ZOCYaxK_^^fBF`1(9(??8nL&qYov878}_a_e3U_j_pqh^(oE>fv{RN z#-}SQn^ralvYsEj^u#+Q!FivcMBy!n0vpS>!~X^IDI{bZeSk^&i)l)?KdO+`DJ*!{ zUTjq5d=G|`u9&$j0rO89hkeGtV1r(MJO&a7HZ|p4y*F`Gz)GDVq#8F4mI@&$Plb zq-8ryi1VLEWCNFp%*XO)H7z*IdBQBCl%ht>W zjgv*_zm&@Z62w;DMNs@F*1gqYs^0fWnYQhvnttl==(H^N3(H~?87;r`sV-9=EsQyp z_bvas$`P-}ahAes$BHhkW_mN8Pu#8Dn&T$UA`y+*{BbIsR!gf(V%Goh-P5ZxB!0Cr zP;fwY_;d3uWTqLWZZnl@>pbM(c3wFo9$QzRTzkbb1H3N1&dT^)^c`t{XSunYRraH% z4A?qHZr$unEEn3C_){w;V5WssLm6Qdi=fY1_zuRde0&H7fXv7)+F@1?WTgP5(=E%^5%C5u}*Z9sV3? zECEVS16Tu(Z;@=cFF}k&FFrS>MK4q)Y0y}KG_0mBb`B*>qnpaMCd`K-tFO~V3i;U5 z4elG`#*R%GeCyE0V@Tei z?OXHcj~qb%l3BHQAT_ zH}-cyX?JoKEDkMmj5k38fCpw%)o=l*B+quzq1FVJ^&M!=NDBLo0>-FrXgyV&{{6?$ zILZ4xv=!aNSC@;WBFDNXz3^ohB+jQV8XU8+bzb_uQ^zB~%Bxui*~gz->d|m|&Z@OH zrZBskH9r_}W*4&e8rv2>#0?jTlDclSDe_e~`cWu3?E4{kYBuo?fcZ}BMJqWu66qn& zQf3{b8?{iRro@NYmg*q^Wr!?qO9}tvT#oepn0gyzPfKmo8h1wy7g}bCnT(KYmrE~{ z^wbhQeLPSW)br5xzpot*@AR0gm-hvY0~>S2gwifHSUXS=tmfB`F(chQp6Jh)7A)?A zf)jibvZ*#(@-eelfQZ`?I`x_997KI(x+)NIE}qA5s5=;y!&+;)#dGpBP%%S&7E#gt zlZ6b&gI*pR)_Sxl?VQ#(cQpcT$&8qiSnQgKu!SCR_!KW7U}^j>c{04yESBD;-s?P# zTXMlyFHiP=o=GCjXDfO*E)RI8|Jp4xJhwFy%2@XZk)HwsG->O2DbvfAwqH{vh1zbg zObK>7sNDNAsfI{KXNg(x2a#XJBFTIWc`Cy?LrN9?+~omJ4CSu^X~<YCW)q`2 zW{&)wHcOOv7EmrVQK*?1@5+yt*KU>AcGg87!J{9+g5Cl3P8FCRV(@wVk37L%qsAIH zd^Tx)U4$7BRJ9>CnQL-+CwNa-%qh%?bmx!Swg$b!0m(9JIAqLvHdn2BsEmgnchGQV zbwRmc19eCoW^0*&rM2MxD|6IUst7y|ESs{ds5cB0s|ksEL8y6i_6^ha1;Ivp`SaBp zIf0@w20o~T60V`f?tLz64g!?+IICQ^R}Fd=Kxcpms+XpA<2gdbO&XD)s8p?#}E)5BN75C(AH1 z(HAn;%!S35+w*}IjjcW6B;Uc#NpQ>H5Q+7Xm-3b04vev-U8}^xCtjPR)H_k@Y9@3q z*F&2Ka|#*F+QJ;3ZPZ5`WPWQDJ?5;_WKSVzZ}3vM^__x64_M(yhhFZApcjkva850}K3T1`%pE@6lO|z9E4OeBTKA;i$b)@^ zu%}xyE?ZA&GSwU=s;%er6;M^c=%npH@ERd19)5r@tV!Kh?eM`?Cixx<`-(9d^k1T*;1k%@4vfg(0=5w>S^-^FYMo zV%o1}ZWp90;Ovvv2XLL`*GG&_giajB6XBN8;X&-ayyaiBwzuBh3Ybg{b;3o9cx1Eu zuoWbprQXB`#!=!l4;x8a&%$l}7BlHyDD?hrc4gg1i_=2)-@|3ueeGC0mu%qT@z_zb z$Zy~b<+iB$dj`GHk=IjCSkaf)3i3gLc@aL+Ejf~Qi#<@wTc7jHR(=G3*JEX`XAYB+ zaa|<71@c>`Ja?pv1#F)*Y@;=@LQ+x#V!nJ=TX@OM_3j) z8do@7h00D&zu+HVcIa{}m%e|(E~6fLO<6D;MN8a?A^XZ z~%FvNq&TemvbBA|lPX^sAsG5RzcH1uz z|Ap0av02w;Q`-0~Bol6V|5aW@iR7DVYWNO64kYv4*0gJ<3PH6y{aagul}c*A7Dp4P znC%=MhfF52)jO1?R_QO9+N*{wbTuH;6TE6yre>#=dTiT!h=ca6YO6 zJNK5x?!+E`!HT3S(#oEVK5p+GLJ>kI^(LfF*?mF+Rq)4~W3Ry)^gvRzPEQbGk~Z5E z05@1Pn+FTp*4UYq9CV%$E!6(%aieA;6qxodl%x_bmb`{mig52(AT1)E_XJ%yG_{2_ z-5P0X3ubl&-2ScZyrxo0Fe*&&Q#dh);~UlyTY;bs17;B;gygxtr5vZ*wTKX!^~M98 zQNa9es+J%|Z1GiprY_QrHn^gytyT4_Eou>L@<)h^FT7#J` z#MdaR*epLfM$&wL^u_qLuygNHF3sX+R(vU?)J3EDK+N9T$H&C&)Vq|O9iZwRH{%;8 z!$Nt9&>4<-F8aK?xA!#KyF$OZ^)Vm1GU~R8t5yr-s1^<{Zbp8%J$aZXj48mbb zXf;d~nr&}K_#&$!I3r{XT?LRz{ETU&uMlbh8B#aD-L9h&p~Y^w5zKVp!MM2lA zkI|N~-o*#Cr@Fv$@-*J1%YIqEYI*#p20xjhwCj^K1r*u{Z<>x%Ix(`VD03S&MWhAf zdTnUWz&FcsEi_ioLSgkX^M_0=wv$*NsOPb^b9uD){r3wyQy1yLf#eqiI2DuACb`CS z=Ey!mt08H#avQiLZ8Ag6f;~`SDMOZ9uCvv0=+Yljml+bLqqv=ZF*2lH#wu;g!1!hj zAy1dO(*1V=sScS3njZG|L?9FTD`+DnLWt*o1!q(UIWPS7kBPukv{&jx^9_B@Hh(0OMRoK^m&X zV~drPs62K)nqc<=&`0oR5E#?W4Ka-3kVsRNj691F2IFQXBI5WL{~s#E0n8v^g@onG z)!m8c`E${KgkbOw7RLSo!~ckZw8yvtJuHy{fz;Jmj3m%9_@7YslP0pt*xp2Q zKNc0Upuy|_@4RX}0-Bt-SefwoN!UL+Q5dclQ&Lpcr4Oe5B4e_V zWYfPkJ1!6I8Cuyi#Q&X`pMN+`diK{_es{Hylj6Jp$@Bh8|6!+R@2{osM{HKMnmj_| zE2Kyt{Xu2JgGbz`HJ4*w;kpt-6>QFw`Gt4rCcM_s}uJamzf zIvUB(e1(tL=qqmFw4wXlet)$T1T7-OQ@fCTMV)G&w!p9U_Op>}! zwwEa$JtOmJ|Gi;C907QFx(ygRMOE_EM-%j|1MPRN;$D$dG)hzF=jmDJYFVeQYta@x z))}83wK)$nG?7dgX-Ktl?nswh1ji&H&MmjJr+8*pe13pF(HCjIs=L0R(bQ}l?dx$? zKaSiZ^)?XyiJ}CN)OkI!7!Gj1|GM~RFhc*1b4qX-j!uv44IQxC`L!9}nE&?Hi%RnZ z&aZMl*A?}@hl~b$hZwp`bW@cxzPM-bW=MV9kLH>9au`!8dmjfK(nY}jBr5ypVsDFl zfPOW!iuTcA$vlk^yEf$DZu8wID6&6La?soc%Mt{C7`zLg8kwZfHNKr#66X-Z!+xEj z)cq1({EC~(>JbjO+3hu{otCHcqkXCPa~=-R*LSRU)>$HGdEQa}shAhI#b5z`R(L4% z#2&q8sl=rF#}(xLV$^SYt=qppF6(Zfa2#r_eVh1-%xtgANd#kZM|yI92p!)t&-6-L zFkA7?^yj759bcOgfhxO)3I8(=QBP}ZH~zKw%D>Q5Wg;e_C;DGzGx*L0oN-}f=@h~@E}uPpzQ`9FDzT66#a diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm index 4692dee47859f21d6740048deafc45539ab49121..813499d37fad515aa7b772c9db876e1e9cf849e5 100644 GIT binary patch literal 7183 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

$uFmcv$eMO#r&Gm1aD%jsB@}cuP1Sj2|e5% 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 q|F8bbSbFKD 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 O^l!NTn=P6Cv-%%?%Rt)z diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index 2fabaf8..7326125 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -11,8 +11,8 @@ "type": "SINGLE", "filters": [], "attributes": [], - "versionCode": 9, - "versionName": "1.4.2", + "versionCode": 10, + "versionName": "2.0.0", "outputFile": "app-release.apk" } ], diff --git a/app/src/main/java/com/sosauce/cutemusic/.DS_Store b/app/src/main/java/com/sosauce/cutemusic/.DS_Store index 2680c8872d2415cf30109fd82e2e56dcb0248bda..dd74cbabff6c95abccdc2ec712904d5ef2b19eee 100644 GIT binary patch delta 38 ucmZoMXfc@J&&akhU^gQp+hiUlb50h9T!uu3OoqJ8CQQ3oH?wp6tr4#^UcvrYgi{XNNi^3_{$Ffn4$@x diff --git a/app/src/main/java/com/sosauce/cutemusic/data/actions/PlayerActions.kt b/app/src/main/java/com/sosauce/cutemusic/data/actions/PlayerActions.kt index 37bab29..4698540 100644 --- a/app/src/main/java/com/sosauce/cutemusic/data/actions/PlayerActions.kt +++ b/app/src/main/java/com/sosauce/cutemusic/data/actions/PlayerActions.kt @@ -4,6 +4,7 @@ sealed interface PlayerActions { data object PlayOrPause : PlayerActions data object SeekToNextMusic : PlayerActions data object SeekToPreviousMusic : PlayerActions + data object RestartSong : PlayerActions data class SeekTo(val position: Long) : PlayerActions data class SeekToSlider(val position: Long) : PlayerActions data class RewindTo(val position: Long) : PlayerActions diff --git a/app/src/main/java/com/sosauce/cutemusic/data/datastore/DataStore.kt b/app/src/main/java/com/sosauce/cutemusic/data/datastore/DataStore.kt index 0d3338e..4a2d83e 100644 --- a/app/src/main/java/com/sosauce/cutemusic/data/datastore/DataStore.kt +++ b/app/src/main/java/com/sosauce/cutemusic/data/datastore/DataStore.kt @@ -10,7 +10,10 @@ import androidx.datastore.preferences.preferencesDataStore import com.sosauce.cutemusic.data.datastore.PreferencesKeys.BLACKLISTED_FOLDERS import com.sosauce.cutemusic.data.datastore.PreferencesKeys.FOLLOW_SYS import com.sosauce.cutemusic.data.datastore.PreferencesKeys.HAS_SEEN_TIP +import com.sosauce.cutemusic.data.datastore.PreferencesKeys.SNAP_SPEED_N_PITCH import com.sosauce.cutemusic.data.datastore.PreferencesKeys.SORT_ORDER +import com.sosauce.cutemusic.data.datastore.PreferencesKeys.SORT_ORDER_ALBUMS +import com.sosauce.cutemusic.data.datastore.PreferencesKeys.SORT_ORDER_ARTISTS import com.sosauce.cutemusic.data.datastore.PreferencesKeys.USE_AMOLED_MODE import com.sosauce.cutemusic.data.datastore.PreferencesKeys.USE_DARK_MODE import com.sosauce.cutemusic.data.datastore.PreferencesKeys.USE_SYSTEM_FONT @@ -19,18 +22,30 @@ val Context.dataStore: DataStore by preferencesDataStore(name = "se data object PreferencesKeys { val SORT_ORDER = booleanPreferencesKey("sort_order") + val SORT_ORDER_ARTISTS = booleanPreferencesKey("sort_order_artists") + val SORT_ORDER_ALBUMS = booleanPreferencesKey("sort_order_albums") val USE_DARK_MODE = booleanPreferencesKey("use_dark_mode") val USE_AMOLED_MODE = booleanPreferencesKey("use_amoled_mode") val FOLLOW_SYS = booleanPreferencesKey("follow_sys") val USE_SYSTEM_FONT = booleanPreferencesKey("use_sys_font") val BLACKLISTED_FOLDERS = stringSetPreferencesKey("blacklisted_folders") val HAS_SEEN_TIP = booleanPreferencesKey("has_seen_tip") + val SNAP_SPEED_N_PITCH = booleanPreferencesKey("snap_peed_n_pitch") + val KILL_SERVICE = booleanPreferencesKey("kill_service") } @Composable fun rememberSortASC() = rememberPreference(key = SORT_ORDER, defaultValue = true) +@Composable +fun rememberSortASCArtists() = + rememberPreference(key = SORT_ORDER_ARTISTS, defaultValue = true) + +@Composable +fun rememberSortASCAlbums() = + rememberPreference(key = SORT_ORDER_ALBUMS, defaultValue = true) + @Composable fun rememberUseDarkMode() = rememberPreference(key = USE_DARK_MODE, defaultValue = false) @@ -55,3 +70,10 @@ fun rememberAllBlacklistedFolders() = fun rememberHasSeenTip() = rememberPreference(key = HAS_SEEN_TIP, defaultValue = false) +@Composable +fun rememberSnapSpeedAndPitch() = + rememberPreference(key = SNAP_SPEED_N_PITCH, defaultValue = false) + +//fun rememberKillService(context: Context) = +// rememberNonComposablePreference(key = KILL_SERVICE, defaultValue = true, context = context) + diff --git a/app/src/main/java/com/sosauce/cutemusic/data/datastore/SettingsExt.kt b/app/src/main/java/com/sosauce/cutemusic/data/datastore/SettingsExt.kt index 4069664..c4027da 100644 --- a/app/src/main/java/com/sosauce/cutemusic/data/datastore/SettingsExt.kt +++ b/app/src/main/java/com/sosauce/cutemusic/data/datastore/SettingsExt.kt @@ -1,9 +1,11 @@ package com.sosauce.cutemusic.data.datastore +import android.content.Context import android.content.res.Configuration import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalConfiguration @@ -11,6 +13,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -44,6 +48,38 @@ fun rememberPreference( } } +fun rememberNonComposablePreference( + key: Preferences.Key, + defaultValue: T, + context: Context +): MutableState { + val coroutineScope = CoroutineScope(Dispatchers.Main) + val state = mutableStateOf(defaultValue) + + coroutineScope.launch { + context.dataStore.data + .map { preferences -> preferences[key] ?: defaultValue } + .collect { newValue -> + state.value = newValue + } + } + + return object : MutableState { + override var value: T + get() = state.value + set(value) { + coroutineScope.launch { + context.dataStore.edit { + it[key] = value + } + } + } + + override fun component1() = value + override fun component2(): (T) -> Unit = { value = it } + } +} + @Composable fun rememberIsLandscape(): Boolean { val config = LocalConfiguration.current diff --git a/app/src/main/java/com/sosauce/cutemusic/di/AppModule.kt b/app/src/main/java/com/sosauce/cutemusic/di/AppModule.kt index add959d..7575fc1 100644 --- a/app/src/main/java/com/sosauce/cutemusic/di/AppModule.kt +++ b/app/src/main/java/com/sosauce/cutemusic/di/AppModule.kt @@ -10,7 +10,7 @@ import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.ui.shared_components.PostViewModel import org.koin.android.ext.koin.androidApplication import org.koin.android.ext.koin.androidContext -import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.module.dsl.viewModel import org.koin.dsl.module val appModule = module { diff --git a/app/src/main/java/com/sosauce/cutemusic/domain/model/Lyrics.kt b/app/src/main/java/com/sosauce/cutemusic/domain/model/Lyrics.kt new file mode 100644 index 0000000..5139160 --- /dev/null +++ b/app/src/main/java/com/sosauce/cutemusic/domain/model/Lyrics.kt @@ -0,0 +1,6 @@ +package com.sosauce.cutemusic.domain.model + +data class Lyrics( + val timestamp: Long = 0L, + val lineLyrics: String = "" +) diff --git a/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreHelper.kt b/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreHelper.kt index 0b09c6d..32d7d08 100644 --- a/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreHelper.kt +++ b/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreHelper.kt @@ -4,16 +4,20 @@ import android.content.ContentUris import android.content.Context import android.os.Bundle import android.provider.MediaStore +import android.util.Log import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import com.sosauce.cutemusic.domain.model.Album import com.sosauce.cutemusic.domain.model.Artist import com.sosauce.cutemusic.domain.model.Folder +import com.sosauce.cutemusic.utils.queryAsFlow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow class MediaStoreHelper( private val context: Context ) { - fun getMusics(): List { + fun getMusics():List { val musics = mutableListOf() @@ -33,7 +37,6 @@ class MediaStoreHelper( projection, null, null, - null )?.use { cursor -> val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID) val titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE) @@ -58,8 +61,7 @@ class MediaStoreHelper( ) val artUri = ContentUris.appendId( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.buildUpon(), id - ) - .appendPath("albumart").build() + ).appendPath("albumart").build() musics.add( MediaItem @@ -86,6 +88,7 @@ class MediaStoreHelper( ) } } + return musics } @@ -104,7 +107,7 @@ class MediaStoreHelper( projection, null, null, - "${MediaStore.Audio.Albums.ALBUM} ASC" + null )?.use { cursor -> val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums._ID) val albumColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM) @@ -163,7 +166,7 @@ class MediaStoreHelper( val folders = mutableListOf() val projection = arrayOf( - MediaStore.Audio.Media.DATA + MediaStore.Audio.Media.DATA, ) context.contentResolver.query( @@ -186,7 +189,7 @@ class MediaStoreHelper( folders.add( Folder( name = folderName, - path = path + path = path, ) ) } diff --git a/app/src/main/java/com/sosauce/cutemusic/main/App.kt b/app/src/main/java/com/sosauce/cutemusic/main/App.kt index 9a68163..9408d5f 100644 --- a/app/src/main/java/com/sosauce/cutemusic/main/App.kt +++ b/app/src/main/java/com/sosauce/cutemusic/main/App.kt @@ -3,9 +3,13 @@ package com.sosauce.cutemusic.main import android.app.Application import com.sosauce.cutemusic.di.appModule import org.koin.android.ext.koin.androidContext +import org.koin.android.java.KoinAndroidApplication +import org.koin.core.Koin +import org.koin.core.KoinApplication import org.koin.core.context.startKoin class App : Application() { + override fun onCreate() { super.onCreate() startKoin { diff --git a/app/src/main/java/com/sosauce/cutemusic/main/MainActivity.kt b/app/src/main/java/com/sosauce/cutemusic/main/MainActivity.kt index f7cd53d..3ffd30d 100644 --- a/app/src/main/java/com/sosauce/cutemusic/main/MainActivity.kt +++ b/app/src/main/java/com/sosauce/cutemusic/main/MainActivity.kt @@ -6,7 +6,6 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -39,8 +38,7 @@ class MainActivity : ComponentActivity() { Scaffold( modifier = Modifier .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - ) { innerPadding -> + ) { _ -> MaterialTheme { Nav() } diff --git a/app/src/main/java/com/sosauce/cutemusic/main/PlaybackService.kt b/app/src/main/java/com/sosauce/cutemusic/main/PlaybackService.kt index d73d7d4..fa29b7b 100644 --- a/app/src/main/java/com/sosauce/cutemusic/main/PlaybackService.kt +++ b/app/src/main/java/com/sosauce/cutemusic/main/PlaybackService.kt @@ -17,6 +17,8 @@ class PlaybackService : MediaLibraryService() { .setUsage(C.USAGE_MEDIA) .build() + //private val shouldKill = rememberKillService(this) + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? = mediaLibrarySession @@ -42,11 +44,13 @@ class PlaybackService : MediaLibraryService() { override fun onTaskRemoved(rootIntent: Intent?) { super.onTaskRemoved(rootIntent) + //if (shouldKill.value) { mediaLibrarySession?.run { player.release() release() mediaLibrarySession = null stopSelf() } + //} } } \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/main/quickplay/QuickPlayActivity.kt b/app/src/main/java/com/sosauce/cutemusic/main/quickplay/QuickPlayActivity.kt index 721d448..1444998 100644 --- a/app/src/main/java/com/sosauce/cutemusic/main/quickplay/QuickPlayActivity.kt +++ b/app/src/main/java/com/sosauce/cutemusic/main/quickplay/QuickPlayActivity.kt @@ -106,7 +106,7 @@ class QuickPlayActivity : ComponentActivity() { ) { FloatingActionButton( onClick = { - if (!vm.isPlaylistEmpty()) vm.quickPlay(uri) else vm.handlePlayerActions( + if (!vm.isPlaylistEmptyAndDataNotNull()) vm.quickPlay(uri) else vm.handlePlayerActions( PlayerActions.PlayOrPause ) } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/navigation/Navigation.kt b/app/src/main/java/com/sosauce/cutemusic/ui/navigation/Navigation.kt index 11b5a4d..1190332 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/navigation/Navigation.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/navigation/Navigation.kt @@ -3,10 +3,13 @@ package com.sosauce.cutemusic.ui.navigation import android.annotation.SuppressLint +import android.util.Log import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionLayout import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -25,6 +28,7 @@ import com.sosauce.cutemusic.ui.screens.playing.NowPlayingScreen import com.sosauce.cutemusic.ui.screens.settings.SettingsScreen import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.ui.shared_components.PostViewModel +import kotlinx.coroutines.Dispatchers import org.koin.androidx.compose.koinViewModel @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter", "SuspiciousIndentation") @@ -36,8 +40,13 @@ fun Nav() { val postViewModel = koinViewModel() val metadataViewModel = koinViewModel() val blacklistedFolders by rememberAllBlacklistedFolders() - val musics = - postViewModel.musics.filter { it.mediaMetadata.extras?.getString("folder") !in blacklistedFolders } + val musics = postViewModel.musics + .filter { it.mediaMetadata.extras?.getString("folder") !in blacklistedFolders } + + LaunchedEffect(musics) { + Log.d("new musics", musics.toString()) + } + SharedTransitionLayout { @@ -113,7 +122,8 @@ fun Nav() { artist = artist, navController = navController, viewModel = viewModel, - postViewModel = postViewModel + postViewModel = postViewModel, + onNavigate = { screen -> navController.navigate(screen) } ) } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumDetailsLandscape.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumDetailsLandscape.kt index 0980077..501aa79 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumDetailsLandscape.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumDetailsLandscape.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import com.sosauce.cutemusic.R import com.sosauce.cutemusic.domain.model.Album +import com.sosauce.cutemusic.ui.screens.main.MusicListItem import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.ui.shared_components.PostViewModel @@ -97,14 +98,14 @@ fun AlbumDetailsLandscape( Spacer(modifier = Modifier.width(5.dp)) LazyColumn { items(albumSongs, key = { it.mediaId }) { music -> - AlbumSong( + MusicListItem( music = music, - onShortClick = { viewModel.itemClicked(music.mediaId, listOf()) }, + currentMusicUri = viewModel.currentMusicUri, + onShortClick = { viewModel.itemClicked(it, listOf()) } ) } } } } } - } \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumDetailsScreen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumDetailsScreen.kt index b34e4db..6e77c6e 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumDetailsScreen.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumDetailsScreen.kt @@ -3,25 +3,23 @@ package com.sosauce.cutemusic.ui.screens.album import androidx.compose.foundation.basicMarquee -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio 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.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -37,6 +35,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.media3.common.MediaItem @@ -44,6 +43,7 @@ import coil3.compose.AsyncImage import com.sosauce.cutemusic.R import com.sosauce.cutemusic.data.datastore.rememberIsLandscape import com.sosauce.cutemusic.domain.model.Album +import com.sosauce.cutemusic.ui.screens.main.MusicListItem import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.ui.shared_components.PostViewModel @@ -88,16 +88,11 @@ private fun AlbumDetailsContent( Scaffold( topBar = { TopAppBar( - title = { - CuteText( - text = album.name, - - ) - }, + title = {}, navigationIcon = { IconButton(onClick = { onPopBackStack() }) { Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back arrow" ) } @@ -108,11 +103,18 @@ private fun AlbumDetailsContent( Box( modifier = Modifier .fillMaxSize() - .padding(values) + .padding( + start = values.calculateLeftPadding(LayoutDirection.Ltr) + 10.dp, + end = values.calculateRightPadding(LayoutDirection.Rtl) + 10.dp, + top = values.calculateTopPadding(), + bottom = values.calculateBottomPadding() + ) + .verticalScroll(rememberScrollState()) ) { Column { Row( - horizontalArrangement = Arrangement.Center, + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.Top, modifier = Modifier.fillMaxWidth() ) { AsyncImage( @@ -120,96 +122,46 @@ private fun AlbumDetailsContent( img = ImageUtils.getAlbumArt(album.id), context = context ), - contentDescription = "Album Art", + contentDescription = stringResource(R.string.artwork), modifier = Modifier - .aspectRatio(1 / 1f) - .padding(17.dp) - .clip(RoundedCornerShape(24.dp)), + .size(150.dp) + .clip(RoundedCornerShape(12.dp)), contentScale = ContentScale.Crop ) - } - Column { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center + Spacer(Modifier.width(10.dp)) + Column( + horizontalAlignment = Alignment.Start ) { CuteText( - text = if (album.artist.length >= 18) album.artist.take(18) + "..." + " · " else album.artist + " · ", - - fontSize = 22.sp + text = album.name, + fontSize = 22.sp, + modifier = Modifier.basicMarquee() + ) + CuteText( + text = album.artist, + fontSize = 22.sp, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.85f), + modifier = Modifier.basicMarquee() ) + Spacer(Modifier.height(60.dp)) CuteText( text = "${albumSongs.size} ${if (albumSongs.size <= 1) "song" else "songs"}", - - fontSize = 22.sp + fontSize = 22.sp, + modifier = Modifier.basicMarquee() ) } } - Spacer(modifier = Modifier.height(5.dp)) - HorizontalDivider() - LazyColumn { - itemsIndexed(albumSongs) { _, music -> - AlbumSong( + Spacer(Modifier.height(10.dp)) + Column { + albumSongs.forEach { music -> + MusicListItem( music = music, onShortClick = { viewModel.itemClicked(it, listOf()) }, - - ) + currentMusicUri = viewModel.currentMusicUri + ) } } } } } } - - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AlbumSong( - music: MediaItem, - onShortClick: (String) -> Unit, -) { - - val context = LocalContext.current - - - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(20.dp)) - .clickable { onShortClick(music.mediaId) }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - AsyncImage( - model = ImageUtils.imageRequester( - img = music.mediaMetadata.artworkUri, - context = context - ), - stringResource(R.string.artwork), - modifier = Modifier - .padding(start = 10.dp) - .size(45.dp), - contentScale = ContentScale.Crop, - ) - Column( - modifier = Modifier.padding(15.dp) - ) { - CuteText( - text = music.mediaMetadata.title.toString(), - maxLines = 1, - modifier = Modifier.then( - if (music.mediaMetadata.title?.length!! >= 25) { - Modifier.basicMarquee() - } else Modifier - ) - ) - CuteText( - text = music.mediaMetadata.artist.toString(), - color = MaterialTheme.colorScheme.onBackground.copy(0.85f) - ) - } - } - - } -} diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumScreen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumScreen.kt index 9f213a6..159dd60 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumScreen.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumScreen.kt @@ -10,24 +10,27 @@ import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.aspectRatio +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.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Sort import androidx.compose.material.icons.rounded.Album import androidx.compose.material.icons.rounded.Settings -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.DropdownMenu import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -52,6 +55,7 @@ import coil3.compose.AsyncImage import com.sosauce.cutemusic.R import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.data.datastore.rememberIsLandscape +import com.sosauce.cutemusic.data.datastore.rememberSortASCAlbums import com.sosauce.cutemusic.domain.model.Album import com.sosauce.cutemusic.ui.navigation.Screen import com.sosauce.cutemusic.ui.shared_components.CuteSearchbar @@ -60,7 +64,9 @@ import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.ui.shared_components.NavigationItem import com.sosauce.cutemusic.ui.shared_components.PostViewModel import com.sosauce.cutemusic.ui.shared_components.ScreenSelection +import com.sosauce.cutemusic.ui.shared_components.SortRadioButtons import com.sosauce.cutemusic.utils.ImageUtils +import com.sosauce.cutemusic.utils.thenIf @Composable fun SharedTransitionScope.AlbumsScreen( @@ -107,7 +113,7 @@ fun SharedTransitionScope.AlbumsScreen( onHandlePlayerActions = viewModel::handlePlayerActions, isPlaying = viewModel.isCurrentlyPlaying, animatedVisibilityScope = animatedVisibilityScope, - isPlaylistEmpty = viewModel.isPlaylistEmpty() + isPlaylistEmpty = viewModel.isPlaylistEmptyAndDataNotNull() ) } @@ -128,8 +134,10 @@ private fun SharedTransitionScope.AlbumsScreenContent( ) { var query by remember { mutableStateOf("") } + var sort by rememberSortASCAlbums() + var sortExpanded by remember { mutableStateOf(false) } var screenSelectionExpanded by remember { mutableStateOf(false) } - val displayAlbums by remember(query) { + val displayAlbums by remember { derivedStateOf { if (query.isNotEmpty()) { albums.filter { @@ -138,19 +146,20 @@ private fun SharedTransitionScope.AlbumsScreenContent( ignoreCase = true ) } - } else albums + } else { + if (sort) albums + else albums.sortedByDescending { it.name } + } } } Box { - Scaffold { values -> if (albums.isEmpty()) { Column( modifier = Modifier .fillMaxSize() - .padding(values) ) { CuteText( text = stringResource(id = R.string.no_albums_found), @@ -167,26 +176,27 @@ private fun SharedTransitionScope.AlbumsScreenContent( columns = GridCells.Fixed(2), modifier = Modifier .fillMaxSize() - .padding(values) ) { - items( + itemsIndexed( items = displayAlbums, - key = { it.id } - ) { album -> + key = { _, album -> album.id } + ) { index, album -> AlbumCard( album = album, modifier = Modifier - .padding(horizontal = 5.dp, vertical = 5.dp) .clip(RoundedCornerShape(15.dp)) .clickable { chargePVMAlbumSongs(album.id) onNavigate(Screen.AlbumsDetails(album.id)) } + .thenIf( + index == 0 || index == 1, + Modifier.statusBarsPadding() + ) ) } } } - } CuteSearchbar( query = query, onQueryChange = { query = it }, @@ -195,17 +205,6 @@ private fun SharedTransitionScope.AlbumsScreenContent( .fillMaxWidth(0.9f) .padding(bottom = 10.dp) .align(Alignment.BottomCenter) - .background( - color = MaterialTheme.colorScheme.surface, - shape = RoundedCornerShape(24.dp) - ) - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.surfaceContainer, - shape = RoundedCornerShape(24.dp) - ) - .clip(RoundedCornerShape(24.dp)) - .clickable { onNavigate(Screen.NowPlaying) } .sharedElement( state = rememberSharedContentState(key = "searchbar"), animatedVisibilityScope = animatedVisibilityScope, @@ -242,18 +241,41 @@ private fun SharedTransitionScope.AlbumsScreenContent( } }, trailingIcon = { - IconButton(onClick = { onNavigate(Screen.Settings) }) { - Icon( - imageVector = Icons.Rounded.Settings, - contentDescription = null - ) + Row { + IconButton(onClick = { sortExpanded = true }) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.Sort, + contentDescription = null + ) + } + IconButton( + onClick = { onNavigate(Screen.Settings) } + ) { + Icon( + imageVector = Icons.Rounded.Settings, + contentDescription = null + ) + } + DropdownMenu( + expanded = sortExpanded, + onDismissRequest = { sortExpanded = false }, + modifier = Modifier + .width(180.dp) + .background(color = MaterialTheme.colorScheme.surface) + ) { + SortRadioButtons( + sort = sort, + onChangeSort = { sort = !sort } + ) + } } }, currentlyPlaying = currentlyPlaying, onHandlePlayerActions = { onHandlePlayerActions(it) }, isPlaying = isPlaying, animatedVisibilityScope = animatedVisibilityScope, - isPlaylistEmpty = isPlaylistEmpty + isPlaylistEmpty = isPlaylistEmpty, + onNavigate = { onNavigate(Screen.NowPlaying) } ) } } @@ -264,53 +286,38 @@ fun AlbumCard( modifier: Modifier = Modifier ) { val context = LocalContext.current - Card( - colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer), + + Column( modifier = modifier + .padding(20.dp) ) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, + AsyncImage( + model = ImageUtils.imageRequester( + img = ImageUtils.getAlbumArt(album.id) ?: R.drawable.ic_launcher_foreground, + context = context + ), + contentDescription = stringResource(id = R.string.artwork), modifier = Modifier - .fillMaxWidth() - ) { - AsyncImage( - model = ImageUtils.imageRequester( - img = ImageUtils.getAlbumArt(album.id), - context = context - ), - contentDescription = stringResource(id = R.string.artwork), - modifier = Modifier - .aspectRatio(1 / 1f) - .padding(10.dp) - .clip(RoundedCornerShape(15)), - contentScale = ContentScale.Crop + .size(160.dp) + .clip(RoundedCornerShape(24.dp)), + contentScale = ContentScale.Crop + ) + Spacer(Modifier.height(10.dp)) + Column { + CuteText( + text = album.name, + maxLines = 1, + modifier = Modifier.basicMarquee() + ) + CuteText( + text = album.artist, + color = MaterialTheme.colorScheme.onBackground.copy(0.85f), + modifier = Modifier.basicMarquee() ) - Column( - modifier = Modifier.padding(15.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - CuteText( - text = album.name, - maxLines = 1, - modifier = Modifier.then( - if (album.name.length >= 15) { - Modifier.basicMarquee() - } else Modifier - ) - ) - CuteText( - text = album.artist, - - color = MaterialTheme.colorScheme.onBackground.copy(0.85f) - ) - } } } } -// Previews are commented by default, un-comment to use them, re-comment them when finalizing your changes for a PR - //@Preview //@Composable //private fun AlbumScreenPreview() { diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumScreenLandscape.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumScreenLandscape.kt index 3990af8..c0edc0d 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumScreenLandscape.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumScreenLandscape.kt @@ -7,10 +7,8 @@ import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.core.tween import androidx.compose.foundation.background -import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -27,10 +25,8 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.Sort -import androidx.compose.material.icons.rounded.MusicNote +import androidx.compose.material.icons.rounded.Album import androidx.compose.material.icons.rounded.Settings -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.DropdownMenu import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -45,16 +41,13 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import coil3.compose.AsyncImage import com.sosauce.cutemusic.R -import com.sosauce.cutemusic.data.datastore.rememberSortASC +import com.sosauce.cutemusic.data.datastore.rememberSortASCAlbums import com.sosauce.cutemusic.domain.model.Album import com.sosauce.cutemusic.ui.navigation.Screen import com.sosauce.cutemusic.ui.shared_components.CuteSearchbar @@ -64,7 +57,6 @@ import com.sosauce.cutemusic.ui.shared_components.NavigationItem import com.sosauce.cutemusic.ui.shared_components.PostViewModel import com.sosauce.cutemusic.ui.shared_components.ScreenSelection import com.sosauce.cutemusic.ui.shared_components.SortRadioButtons -import com.sosauce.cutemusic.utils.ImageUtils @Composable fun SharedTransitionScope.AlbumScreenLandscape( @@ -81,11 +73,11 @@ fun SharedTransitionScope.AlbumScreenLandscape( ) { - val sort by rememberSortASC() + var sort by rememberSortASCAlbums() var query by remember { mutableStateOf("") } var sortExpanded by remember { mutableStateOf(false) } var screenSelectionExpanded by remember { mutableStateOf(false) } - val displayAlbums by remember(sort, albums, query) { + val displayAlbums by remember { derivedStateOf { if (query.isNotEmpty()) { albums.filter { @@ -102,42 +94,41 @@ fun SharedTransitionScope.AlbumScreenLandscape( } } - Scaffold { values -> Box(Modifier.fillMaxSize()) { if (albums.isEmpty()) { Column( - modifier = Modifier - .fillMaxSize() - .padding(values) + modifier = Modifier.fillMaxSize() ) { CuteText( text = stringResource(id = R.string.no_albums_found), modifier = Modifier .padding(16.dp) .fillMaxWidth(), - textAlign = TextAlign.Center, - - ) + textAlign = TextAlign.Center + ) } } else { LazyVerticalGrid( - columns = GridCells.Fixed(3), + columns = GridCells.Fixed(4), modifier = Modifier .fillMaxSize() - .padding(values) - .padding(start = 80.dp) + .padding(start = 30.dp) ) { items( items = displayAlbums, key = { it.id } ) { album -> - AlbumCardLandscape( + AlbumCard( album = album, - onClick = { - postViewModel.albumSongs(album.id) - navController.navigate(Screen.AlbumsDetails(id = album.id)) - }, + modifier = Modifier + .padding(horizontal = 5.dp, vertical = 5.dp) + .clip(RoundedCornerShape(15.dp)) + .clickable { + postViewModel.albumSongs(album.id) + navController.navigate(Screen.AlbumsDetails(id = album.id)) + } + .size(230.dp) ) } } @@ -150,23 +141,10 @@ fun SharedTransitionScope.AlbumScreenLandscape( .navigationBarsPadding() .fillMaxWidth(0.4f) .padding( - bottom = values.calculateBottomPadding() + 5.dp, - end = values.calculateEndPadding( - layoutDirection = LayoutDirection.Rtl - ) + 10.dp + bottom = 5.dp, + end = 10.dp ) .align(Alignment.BottomEnd) - .background( - color = MaterialTheme.colorScheme.surface, - shape = RoundedCornerShape(24.dp) - ) - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.surfaceContainer, - shape = RoundedCornerShape(24.dp) - ) - .clip(RoundedCornerShape(24.dp)) - .clickable { onNavigateTo(Screen.NowPlaying) } .sharedElement( state = rememberSharedContentState(key = "searchbar"), animatedVisibilityScope = animatedVisibilityScope, @@ -184,7 +162,7 @@ fun SharedTransitionScope.AlbumScreenLandscape( leadingIcon = { IconButton(onClick = { screenSelectionExpanded = true }) { Icon( - imageVector = Icons.Rounded.MusicNote, + imageVector = Icons.Rounded.Album, contentDescription = null ) } @@ -225,7 +203,10 @@ fun SharedTransitionScope.AlbumScreenLandscape( .width(180.dp) .background(color = MaterialTheme.colorScheme.surface) ) { - SortRadioButtons() + SortRadioButtons( + sort = sort, + onChangeSort = { sort = !sort } + ) } } }, @@ -233,62 +214,8 @@ fun SharedTransitionScope.AlbumScreenLandscape( onHandlePlayerActions = { viewModel.handlePlayerActions(it) }, isPlaying = isCurrentlyPlaying, animatedVisibilityScope = animatedVisibilityScope, - isPlaylistEmpty = viewModel.isPlaylistEmpty() + isPlaylistEmpty = viewModel.isPlaylistEmptyAndDataNotNull(), + onNavigate = { onNavigateTo(Screen.NowPlaying) } ) } - } -} - -@Composable -private fun AlbumCardLandscape( - album: Album, - onClick: () -> Unit, -) { - val context = LocalContext.current - Card( - colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer), - modifier = Modifier - .padding(horizontal = 5.dp, vertical = 5.dp) - .clip(RoundedCornerShape(15.dp)) - .clickable { onClick() }, - ) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - ) { - AsyncImage( - model = ImageUtils.imageRequester( - img = ImageUtils.getAlbumArt(album.id), - context = context - ), - stringResource(R.string.artwork), - modifier = Modifier - .size(215.dp) - .padding(top = 7.dp) - .clip(RoundedCornerShape(15)), - contentScale = ContentScale.Crop - ) - Column( - modifier = Modifier.padding(15.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - CuteText( - text = album.name, - maxLines = 1, - modifier = Modifier.then( - if (album.name.length >= 15) { - Modifier.basicMarquee() - } else Modifier - ) - ) - CuteText( - text = album.artist, - - color = MaterialTheme.colorScheme.onBackground.copy(0.85f) - ) - } - } - } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistDetails.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistDetails.kt index 01d8e68..93efe3f 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistDetails.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistDetails.kt @@ -6,11 +6,7 @@ package com.sosauce.cutemusic.ui.screens.artist import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -19,62 +15,49 @@ 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.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.media3.common.MediaItem import androidx.navigation.NavController -import coil3.compose.AsyncImage -import coil3.compose.rememberAsyncImagePainter -import com.sosauce.cutemusic.R import com.sosauce.cutemusic.data.datastore.rememberIsLandscape -import com.sosauce.cutemusic.domain.model.Album import com.sosauce.cutemusic.domain.model.Artist import com.sosauce.cutemusic.ui.navigation.Screen +import com.sosauce.cutemusic.ui.screens.album.AlbumCard +import com.sosauce.cutemusic.ui.screens.main.MusicListItem import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.ui.shared_components.PostViewModel -import com.sosauce.cutemusic.utils.ImageUtils @Composable fun ArtistDetails( artist: Artist, navController: NavController, viewModel: MusicViewModel, - postViewModel: PostViewModel + postViewModel: PostViewModel, + onNavigate: (Screen) -> Unit, ) { val artistSongs by remember { mutableStateOf(postViewModel.artistSongs) } val artistAlbums by remember { mutableStateOf(postViewModel.artistAlbums) } - val isLandscape = rememberIsLandscape() - - if (isLandscape) { + if (rememberIsLandscape()) { ArtistDetailsLandscape( onNavigateUp = navController::navigateUp, artistAlbums = artistAlbums, @@ -83,8 +66,8 @@ fun ArtistDetails( onNavigate = { navController.navigate(it) }, chargePVMAlbumSongs = { postViewModel.albumSongs(it) }, artist = artist, - - ) + currentMusicUri = viewModel.currentMusicUri + ) } else { Scaffold( topBar = { @@ -95,7 +78,6 @@ fun ArtistDetails( ) { CuteText( text = artist.name + " · ", - fontSize = 20.sp ) CuteText( @@ -126,9 +108,15 @@ fun ArtistDetails( Column { LazyRow { items(items = artistAlbums, key = { it.id }) { album -> - AlbumsCard(album) { - navController.navigate(Screen.AlbumsDetails(album.id)) - } + AlbumCard( + album = album, + modifier = Modifier + .clip(RoundedCornerShape(15.dp)) + .clickable { + postViewModel.albumSongs(album.id) + onNavigate(Screen.AlbumsDetails(album.id)) + } + ) } } Spacer(modifier = Modifier.height(10.dp)) @@ -136,11 +124,10 @@ fun ArtistDetails( Spacer(modifier = Modifier.height(10.dp)) LazyColumn { items(artistSongs) { music -> - ArtistMusicList( + MusicListItem( music = music, - onShortClick = { viewModel.itemClicked(music.mediaId, listOf()) }, - onSelected = { /*TODO*/ }, - isSelected = false, + onShortClick = { viewModel.itemClicked(it, listOf()) }, + currentMusicUri = viewModel.currentMusicUri ) } } @@ -149,120 +136,4 @@ fun ArtistDetails( } } } - -} - -@Composable -private fun AlbumsCard( - album: Album, - onClick: () -> Unit -) { - val context = LocalContext.current - - Card( - colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer), - modifier = Modifier - .padding(horizontal = 10.dp, vertical = 5.dp) - .clickable { onClick() }, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - .padding(12.dp) - ) { - AsyncImage( - model = ImageUtils.imageRequester( - img = ImageUtils.getAlbumArt(album.id), - context = context - ), - contentDescription = "Album Art", - modifier = Modifier - .size(160.dp) - .clip(RoundedCornerShape(15)), - contentScale = ContentScale.Crop - ) - Spacer(modifier = Modifier.height(10.dp)) - CuteText( - text = album.name, - modifier = Modifier.then( - if (album.name.length >= 15) { - Modifier.basicMarquee() - } else Modifier - ) - ) - } - } -} - -@Composable -fun ArtistMusicList( - music: MediaItem, - onShortClick: (String) -> Unit, - onSelected: () -> Unit, - isSelected: Boolean, -) { - - val context = LocalContext.current - - Row( - modifier = Modifier - .fillMaxWidth() - .combinedClickable( - onClick = { onShortClick(music.mediaId) }, - onLongClick = { onSelected() } - ), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - - Row(verticalAlignment = Alignment.CenterVertically) { - if (!isSelected) { - AsyncImage( - model = ImageUtils.imageRequester( - img = music.mediaMetadata.artworkUri, - context = context - ), - stringResource(R.string.artwork), - modifier = Modifier - .padding(start = 10.dp) - .size(45.dp) - .clip(RoundedCornerShape(15)), - contentScale = ContentScale.Crop - ) - } else { - Image( - painter = rememberAsyncImagePainter( - music.mediaMetadata.artworkUri ?: R.drawable.cute_music_icon - ), - stringResource(R.string.artwork), - modifier = Modifier - .padding(start = 10.dp) - .size(45.dp) - .clip(RoundedCornerShape(15)), - contentScale = ContentScale.Crop, - ) - } - - Column( - modifier = Modifier.padding(15.dp) - ) { - CuteText( - text = music.mediaMetadata.title.toString(), - maxLines = 1, - modifier = Modifier.then( - if (music.mediaMetadata.title?.length!! >= 25) { - Modifier.basicMarquee() - } else { - Modifier - } - ) - - ) - CuteText( - text = music.mediaMetadata.artist.toString() - ) - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistDetailsLandscape.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistDetailsLandscape.kt index a0e5f26..5566c97 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistDetailsLandscape.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistDetailsLandscape.kt @@ -5,9 +5,10 @@ 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.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -16,7 +17,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -28,7 +28,7 @@ import com.sosauce.cutemusic.domain.model.Album import com.sosauce.cutemusic.domain.model.Artist import com.sosauce.cutemusic.ui.navigation.Screen import com.sosauce.cutemusic.ui.screens.album.AlbumCard -import com.sosauce.cutemusic.ui.screens.album.AlbumSong +import com.sosauce.cutemusic.ui.screens.main.MusicListItem import com.sosauce.cutemusic.ui.shared_components.CuteText @Composable @@ -40,70 +40,66 @@ fun ArtistDetailsLandscape( onNavigate: (Screen) -> Unit, chargePVMAlbumSongs: (Long) -> Unit, artist: Artist, - + currentMusicUri: String +) { + Column( + modifier = Modifier + .navigationBarsPadding() + .statusBarsPadding() + .padding(start = 20.dp) ) { - - Scaffold( - modifier = Modifier.padding(45.dp) - ) { values -> - - Column { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically + Row( + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + onClick = { onNavigateUp() } ) { - IconButton( - onClick = { onNavigateUp() } - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = null, - modifier = Modifier.size(25.dp) - ) - } - CuteText( - text = artist.name + " · ", - - fontSize = 20.sp - ) - CuteText( - text = "${artistSongs.size} ${if (artistSongs.size <= 1) "song" else "songs"}", - - fontSize = 20.sp + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + modifier = Modifier.size(25.dp) ) } - Row( - modifier = Modifier.fillMaxSize() - ) { - LazyColumn { - items(artistAlbums, key = { it.id }) { album -> - AlbumCard( - album = album, - modifier = Modifier - .padding(horizontal = 5.dp, vertical = 5.dp) - .clip(RoundedCornerShape(15.dp)) - .clickable { - chargePVMAlbumSongs(album.id) - onNavigate(Screen.AlbumsDetails(album.id)) - } - .size(230.dp) - ) - } + CuteText( + text = artist.name + " · ", + fontSize = 20.sp + ) + CuteText( + text = "${artistSongs.size} ${if (artistSongs.size <= 1) "song" else "songs"}", + fontSize = 20.sp + ) + } + Row( + modifier = Modifier.fillMaxSize() + ) { + LazyColumn { + items( + items = artistAlbums, + key = { it.id } + ) { album -> + AlbumCard( + album = album, + modifier = Modifier + .padding(horizontal = 5.dp, vertical = 5.dp) + .clip(RoundedCornerShape(15.dp)) + .clickable { + chargePVMAlbumSongs(album.id) + onNavigate(Screen.AlbumsDetails(album.id)) + } + .size(230.dp) + ) } - Spacer(modifier = Modifier.width(5.dp)) - LazyColumn { - items(artistSongs, key = { it.mediaId }) { music -> - AlbumSong( - music = music, - onShortClick = { onClickPlay(music.mediaId) }, - - ) - } + } + Spacer(modifier = Modifier.width(5.dp)) + LazyColumn { + items(artistSongs, key = { it.mediaId }) { music -> + MusicListItem( + music = music, + currentMusicUri = currentMusicUri, + onShortClick = { onClickPlay(it) } + ) } } } - } - - } \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistsScreen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistsScreen.kt index d06064e..a213b7d 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistsScreen.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistsScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Sort import androidx.compose.material.icons.rounded.Person import androidx.compose.material.icons.rounded.Settings import androidx.compose.material3.DropdownMenu @@ -49,6 +50,7 @@ import coil3.compose.AsyncImage import com.sosauce.cutemusic.R import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.data.datastore.rememberIsLandscape +import com.sosauce.cutemusic.data.datastore.rememberSortASCArtists import com.sosauce.cutemusic.domain.model.Artist import com.sosauce.cutemusic.ui.navigation.Screen import com.sosauce.cutemusic.ui.shared_components.CuteSearchbar @@ -57,6 +59,7 @@ import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.ui.shared_components.NavigationItem import com.sosauce.cutemusic.ui.shared_components.PostViewModel import com.sosauce.cutemusic.ui.shared_components.ScreenSelection +import com.sosauce.cutemusic.ui.shared_components.SortRadioButtons import com.sosauce.cutemusic.utils.ImageUtils @Composable @@ -89,7 +92,7 @@ fun SharedTransitionScope.ArtistsScreen( } }, onHandlePlayerActions = { viewModel.handlePlayerActions(it) }, - isPlaylistEmpty = viewModel.isPlaylistEmpty() + isPlaylistEmpty = viewModel.isPlaylistEmptyAndDataNotNull() ) } else { @@ -111,7 +114,7 @@ fun SharedTransitionScope.ArtistsScreen( onHandlePlayerActions = viewModel::handlePlayerActions, isPlaying = viewModel.isCurrentlyPlaying, animatedVisibilityScope = animatedVisibilityScope, - isPlaylistEmpty = viewModel.isPlaylistEmpty() + isPlaylistEmpty = viewModel.isPlaylistEmptyAndDataNotNull() ) } @@ -133,8 +136,10 @@ private fun SharedTransitionScope.ArtistsScreenContent( ) { var query by remember { mutableStateOf("") } + var sort by rememberSortASCArtists() + var sortExpanded by remember { mutableStateOf(false) } var screenSelectionExpanded by remember { mutableStateOf(false) } - val displayArtists by remember(query) { + val displayArtists by remember { derivedStateOf { if (query.isNotEmpty()) { artist.filter { @@ -143,12 +148,14 @@ private fun SharedTransitionScope.ArtistsScreenContent( ignoreCase = true ) } - } else artist + } else { + if (sort) artist + else artist.sortedByDescending { it.name } + } } } Scaffold { values -> - Box { if (artist.isEmpty()) { Column( @@ -190,17 +197,6 @@ private fun SharedTransitionScope.ArtistsScreenContent( .fillMaxWidth(0.9f) .padding(bottom = 10.dp) .align(Alignment.BottomCenter) - .background( - color = MaterialTheme.colorScheme.surface, - shape = RoundedCornerShape(24.dp) - ) - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.surfaceContainer, - shape = RoundedCornerShape(24.dp) - ) - .clip(RoundedCornerShape(24.dp)) - .clickable { onNavigate(Screen.NowPlaying) } .sharedElement( state = rememberSharedContentState(key = "searchbar"), animatedVisibilityScope = animatedVisibilityScope, @@ -211,9 +207,8 @@ private fun SharedTransitionScope.ArtistsScreenContent( placeholder = { CuteText( text = stringResource(id = R.string.search) + " " + stringResource(R.string.artists), - color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), - - ) + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f) + ) }, leadingIcon = { IconButton(onClick = { screenSelectionExpanded = true }) { @@ -237,18 +232,41 @@ private fun SharedTransitionScope.ArtistsScreenContent( } }, trailingIcon = { - IconButton(onClick = { onNavigate(Screen.Settings) }) { - Icon( - imageVector = Icons.Rounded.Settings, - contentDescription = null - ) + Row { + IconButton(onClick = { sortExpanded = true }) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.Sort, + contentDescription = null + ) + } + IconButton( + onClick = { onNavigate(Screen.Settings) } + ) { + Icon( + imageVector = Icons.Rounded.Settings, + contentDescription = null + ) + } + DropdownMenu( + expanded = sortExpanded, + onDismissRequest = { sortExpanded = false }, + modifier = Modifier + .width(180.dp) + .background(color = MaterialTheme.colorScheme.surface) + ) { + SortRadioButtons( + sort = sort, + onChangeSort = { sort = !sort } + ) + } } }, currentlyPlaying = currentlyPlaying, onHandlePlayerActions = onHandlePlayerActions, isPlaying = isPlaying, animatedVisibilityScope = animatedVisibilityScope, - isPlaylistEmpty = isPlaylistEmpty + isPlaylistEmpty = isPlaylistEmpty, + onNavigate = { onNavigate(Screen.NowPlaying) } ) } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistsScreenLandscape.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistsScreenLandscape.kt index 20dffc0..ff95776 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistsScreenLandscape.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistsScreenLandscape.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.Sort -import androidx.compose.material.icons.rounded.MusicNote +import androidx.compose.material.icons.rounded.Person import androidx.compose.material.icons.rounded.Settings import androidx.compose.material3.DropdownMenu import androidx.compose.material3.Icon @@ -46,7 +46,7 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.sosauce.cutemusic.R import com.sosauce.cutemusic.data.actions.PlayerActions -import com.sosauce.cutemusic.data.datastore.rememberSortASC +import com.sosauce.cutemusic.data.datastore.rememberSortASCArtists import com.sosauce.cutemusic.domain.model.Artist import com.sosauce.cutemusic.ui.navigation.Screen import com.sosauce.cutemusic.ui.shared_components.CuteSearchbar @@ -69,7 +69,7 @@ fun SharedTransitionScope.ArtistsScreenLandscape( isPlaylistEmpty: Boolean ) { - val sort by rememberSortASC() + var sort by rememberSortASCArtists() var query by remember { mutableStateOf("") } var sortExpanded by remember { mutableStateOf(false) } var screenSelectionExpanded by remember { mutableStateOf(false) } @@ -90,15 +90,11 @@ fun SharedTransitionScope.ArtistsScreenLandscape( } } - - Scaffold { values -> - Box(Modifier.fillMaxSize()) { if (artists.isEmpty()) { Column( modifier = Modifier .fillMaxSize() - .padding(values) ) { CuteText( text = stringResource(id = R.string.no_artists_found), @@ -114,8 +110,7 @@ fun SharedTransitionScope.ArtistsScreenLandscape( LazyColumn( modifier = Modifier .fillMaxWidth() - .padding(values) - .padding(start = 80.dp), + .padding(start = 30.dp), verticalArrangement = Arrangement.Top ) { items( @@ -137,23 +132,10 @@ fun SharedTransitionScope.ArtistsScreenLandscape( .navigationBarsPadding() .fillMaxWidth(0.4f) .padding( - bottom = values.calculateBottomPadding() + 5.dp, - end = values.calculateEndPadding( - layoutDirection = LayoutDirection.Rtl - ) + 10.dp + bottom = 5.dp, + end = 10.dp ) .align(Alignment.BottomEnd) - .background( - color = MaterialTheme.colorScheme.surface, - shape = RoundedCornerShape(24.dp) - ) - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.surfaceContainer, - shape = RoundedCornerShape(24.dp) - ) - .clip(RoundedCornerShape(24.dp)) - .clickable { onNavigateTo(Screen.NowPlaying) } .sharedElement( state = rememberSharedContentState(key = "searchbar"), animatedVisibilityScope = animatedVisibilityScope, @@ -171,7 +153,7 @@ fun SharedTransitionScope.ArtistsScreenLandscape( leadingIcon = { IconButton(onClick = { screenSelectionExpanded = true }) { Icon( - imageVector = Icons.Rounded.MusicNote, + imageVector = Icons.Rounded.Person, contentDescription = null ) } @@ -212,7 +194,10 @@ fun SharedTransitionScope.ArtistsScreenLandscape( .width(180.dp) .background(color = MaterialTheme.colorScheme.surface) ) { - SortRadioButtons() + SortRadioButtons( + sort = sort, + onChangeSort = { sort = !sort } + ) } } }, @@ -220,10 +205,10 @@ fun SharedTransitionScope.ArtistsScreenLandscape( onHandlePlayerActions = onHandlePlayerActions, isPlaying = isCurrentlyPlaying, animatedVisibilityScope = animatedVisibilityScope, - isPlaylistEmpty = isPlaylistEmpty + isPlaylistEmpty = isPlaylistEmpty, + onNavigate = { onNavigateTo(Screen.NowPlaying) } ) } - } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/lyrics/LyricsView.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/lyrics/LyricsView.kt new file mode 100644 index 0000000..52f3853 --- /dev/null +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/lyrics/LyricsView.kt @@ -0,0 +1,219 @@ +@file:OptIn(ExperimentalFoundationApi::class, ExperimentalAnimationApi::class) + +package com.sosauce.cutemusic.ui.screens.lyrics + +import android.util.Log +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.EaseInBounce +import androidx.compose.animation.core.EaseInQuad +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOut +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.animation.with +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.times +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInParent +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.sosauce.cutemusic.data.actions.PlayerActions +import com.sosauce.cutemusic.domain.model.Lyrics +import com.sosauce.cutemusic.ui.shared_components.CuteText +import com.sosauce.cutemusic.ui.shared_components.MusicViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.math.max + +@Composable +fun LyricsView( + viewModel: MusicViewModel, + onHideLyrics: () -> Unit, + path: String, + isLandscape: Boolean = false +) { + var currentLyric by remember { mutableStateOf(Lyrics()) } + val clipboardManager = LocalClipboardManager.current + val indexToScrollTo = viewModel.currentLyrics.indexOfFirst { lyric -> + viewModel.currentPosition in lyric.timestamp until (viewModel.currentLyrics.getOrNull( + viewModel.currentLyrics.indexOf(lyric) + 1 + )?.timestamp ?: 0) + } + val lazyListState = rememberLazyListState( + initialFirstVisibleItemIndex = if (indexToScrollTo != -1) indexToScrollTo else 0 + ) + val firstVisibleIndex by remember { derivedStateOf { lazyListState.firstVisibleItemIndex } } + + + + Box( + modifier = Modifier + .statusBarsPadding() + .navigationBarsPadding() + ) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + state = lazyListState + ) { + + if (viewModel.currentLyrics.isEmpty()) { + item { + CuteText( + text = viewModel.loadEmbeddedLyrics(path), + ) + } + } else { + itemsIndexed( + items = viewModel.currentLyrics, + key = { _, item -> item.timestamp } + ) { index, lyric -> + + + val nextTimestamp = remember(index, viewModel.currentLyrics) { + if (index < viewModel.currentLyrics.size - 1) { + viewModel.currentLyrics[index + 1].timestamp + } else { + 0 + } + } + + val isCurrentLyric = remember(viewModel.currentPosition, nextTimestamp) { + viewModel.currentPosition in lyric.timestamp until nextTimestamp + } + + val color by animateColorAsState( + targetValue = if (isCurrentLyric) { + MaterialTheme.colorScheme.onBackground + } else { + MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f) + }, + label = "" + ) + + if (isCurrentLyric) { + currentLyric = lyric + LaunchedEffect(Unit) { + lazyListState.animateScrollToItem(index) + } + } + + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(5.dp) + .clip(RoundedCornerShape(24.dp)) + .combinedClickable( + onClick = { + viewModel.handlePlayerActions( + PlayerActions.SeekToSlider( + lyric.timestamp + ) + ) + }, + onLongClick = { + clipboardManager.setText( + AnnotatedString( + text = lyric.lineLyrics + ) + ) + } + ) + ) { + + CuteText( + text = lyric.lineLyrics, + fontSize = 20.sp, + modifier = Modifier.padding(15.dp), + color = color + ) + } + } + } + } + + + val horizontalArrangement = if (isLandscape) Arrangement.End else Arrangement.Center + Row( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .background( + Brush.verticalGradient( + colors = listOf( + Color.Transparent, + MaterialTheme.colorScheme.background + ) + ) + ), + horizontalArrangement = horizontalArrangement, + ) { + IconButton( + onClick = { + onHideLyrics() + } + ) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = null + ) + } + } + } + } + diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/lyrics/components/SongInfoLyrics.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/lyrics/components/SongInfoLyrics.kt new file mode 100644 index 0000000..84ae5a8 --- /dev/null +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/lyrics/components/SongInfoLyrics.kt @@ -0,0 +1,62 @@ +package com.sosauce.cutemusic.ui.screens.lyrics.components + +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.layout.statusBarsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +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.unit.dp +import coil3.compose.AsyncImage +import com.sosauce.cutemusic.ui.shared_components.CuteText + +@Composable +fun BoxScope.SongInfoLyrics( + currentTitle: String, + currentArtists: String, + currentMusicUri: Uri?, + modifier: Modifier = Modifier +) { + + Row( + modifier = modifier + .statusBarsPadding() + .fillMaxWidth() + .align(Alignment.TopStart) + .height(70.dp) + .background(MaterialTheme.colorScheme.background), + verticalAlignment = Alignment.CenterVertically, + ) { + AsyncImage( + model = currentMusicUri, + contentDescription = null, + modifier = Modifier + .padding(start = 10.dp) + .size(50.dp) + .clip(RoundedCornerShape(10.dp)) + ) + Column( + modifier = Modifier.padding(10.dp) + ) { + CuteText( + text = currentTitle + ) + CuteText( + text = currentArtists, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.85f) + ) + + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/MainScreen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/MainScreen.kt index 8dff6c0..3f6e81c 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/MainScreen.kt @@ -33,9 +33,10 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -50,7 +51,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Scaffold import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf @@ -83,6 +83,7 @@ import com.sosauce.cutemusic.ui.shared_components.NavigationItem import com.sosauce.cutemusic.ui.shared_components.ScreenSelection import com.sosauce.cutemusic.ui.shared_components.SortRadioButtons import com.sosauce.cutemusic.utils.ImageUtils +import com.sosauce.cutemusic.utils.thenIf @Composable fun SharedTransitionScope.MainScreen( @@ -90,7 +91,7 @@ fun SharedTransitionScope.MainScreen( musics: List, viewModel: MusicViewModel, animatedVisibilityScope: AnimatedVisibilityScope, - onLoadMetadata: ((String) -> Unit)? = null + onLoadMetadata: ((String) -> Unit)? = null, ) { if (rememberIsLandscape()) { @@ -130,11 +131,11 @@ fun SharedTransitionScope.MainScreen( }, animatedVisibilityScope = animatedVisibilityScope, onLoadMetadata = onLoadMetadata, - isPlaylistEmpty = viewModel.isPlaylistEmpty(), + isPlaylistEmpty = viewModel.isPlaylistEmptyAndDataNotNull(), currentMusicUri = viewModel.currentMusicUri, - onHandlePlayerAction = { viewModel.handlePlayerActions(it) } + onHandlePlayerAction = { viewModel.handlePlayerActions(it) }, - ) + ) } } @@ -152,9 +153,9 @@ private fun SharedTransitionScope.MainScreenContent( onLoadMetadata: ((String) -> Unit)? = null, isPlaylistEmpty: Boolean, currentMusicUri: String, - onHandlePlayerAction: (PlayerActions) -> Unit + onHandlePlayerAction: (PlayerActions) -> Unit, ) { - val sort by rememberSortASC() + var sort by rememberSortASC() var query by remember { mutableStateOf("") } var sortExpanded by remember { mutableStateOf(false) } val state = rememberLazyListState() @@ -175,167 +176,156 @@ private fun SharedTransitionScope.MainScreenContent( } } + Box(Modifier.fillMaxSize()) { + LazyColumn( + state = state + ) { + if (displayMusics.isEmpty()) { + item { + CuteText( + text = stringResource(id = R.string.no_musics_found), + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + textAlign = TextAlign.Center, - Scaffold { values -> - Box(Modifier.fillMaxSize()) { - LazyColumn( - modifier = Modifier.padding(values), - state = state - ) { - if (displayMusics.isEmpty()) { - item { - CuteText( - text = stringResource(id = R.string.no_musics_found), - modifier = Modifier - .padding(16.dp) - .fillMaxWidth(), - textAlign = TextAlign.Center, - - ) - } - } else { - items( - items = displayMusics, - key = { it.mediaId } - ) { music -> - Column( - modifier = Modifier.padding( - vertical = 2.dp, - horizontal = 4.dp - ) - ) { - MusicListItem( - onShortClick = { onShortClick(music.mediaId) }, - music = music, - onNavigate = { onNavigateTo(it) }, - currentMusicUri = currentMusicUri, - onLoadMetadata = onLoadMetadata, - showBottomSheet = true + ) + } + } else { + itemsIndexed( + items = displayMusics, + key = { _, music -> music.mediaId } + ) { index, music -> + Column( + modifier = Modifier.padding( + vertical = 2.dp, + horizontal = 4.dp + ) + ) { + MusicListItem( + onShortClick = { onShortClick(music.mediaId) }, + music = music, + onNavigate = { onNavigateTo(it) }, + currentMusicUri = currentMusicUri, + onLoadMetadata = onLoadMetadata, + showBottomSheet = true, + modifier = Modifier.thenIf( + index == 0, + Modifier.statusBarsPadding() ) - } + ) } } } + } - AnimatedVisibility( - visible = state.canScrollForward || displayMusics.size <= 15, - enter = fadeIn(), - exit = fadeOut(), - modifier = Modifier.align(Alignment.BottomCenter) - ) { - val transition = rememberInfiniteTransition(label = "Infinite Color Change") - val color by transition.animateColor( - initialValue = LocalContentColor.current, - targetValue = MaterialTheme.colorScheme.errorContainer, - animationSpec = infiniteRepeatable( - tween(500), - repeatMode = RepeatMode.Reverse + AnimatedVisibility( + visible = state.canScrollForward || displayMusics.size <= 15, + enter = fadeIn(), + exit = fadeOut(), + modifier = Modifier.align(Alignment.BottomCenter) + ) { + val transition = rememberInfiniteTransition(label = "Infinite Color Change") + val color by transition.animateColor( + initialValue = LocalContentColor.current, + targetValue = MaterialTheme.colorScheme.errorContainer, + animationSpec = infiniteRepeatable( + tween(500), + repeatMode = RepeatMode.Reverse + ), + label = "" + ) + var hasSeenTip by rememberHasSeenTip() + + CuteSearchbar( + query = query, + onQueryChange = { query = it }, + modifier = Modifier + .navigationBarsPadding() + .fillMaxWidth(0.85f) + .padding(bottom = 5.dp) + .sharedElement( + state = rememberSharedContentState(key = "searchbar"), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = { _, _ -> + tween(durationMillis = 500) + } ), - label = "" - ) - var hasSeenTip by rememberHasSeenTip() + placeholder = { + CuteText( + text = stringResource(id = R.string.search) + " " + stringResource(id = R.string.music), + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), - CuteSearchbar( - query = query, - onQueryChange = { query = it }, - modifier = Modifier - .navigationBarsPadding() - .fillMaxWidth(0.85f) - .padding(bottom = 5.dp) - .background( - color = MaterialTheme.colorScheme.surface, - shape = RoundedCornerShape(24.dp) - ) - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.surfaceContainer, - shape = RoundedCornerShape(24.dp) ) - .clip(RoundedCornerShape(24.dp)) - .sharedElement( - state = rememberSharedContentState(key = "searchbar"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) + }, + leadingIcon = { + IconButton( + onClick = { + screenSelectionExpanded = true + // Let's prevent writing to datastore everytime the user clicks ;) + if (!hasSeenTip) { + hasSeenTip = true } + } + ) { + Icon( + imageVector = Icons.Rounded.MusicNote, + contentDescription = null, + tint = if (!hasSeenTip) color else LocalContentColor.current ) - .then( - if (isPlaylistEmpty) { - Modifier.clickable { - onNavigateTo(Screen.NowPlaying) - } - } else Modifier - ), - placeholder = { - CuteText( - text = stringResource(id = R.string.search) + " " + stringResource(id = R.string.music), - color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), + } + + DropdownMenu( + expanded = screenSelectionExpanded, + onDismissRequest = { screenSelectionExpanded = false }, + modifier = Modifier + .width(180.dp) + .background(color = MaterialTheme.colorScheme.surface) + ) { + ScreenSelection( + onNavigationItemClicked = onNavigationItemClicked, + selectedIndex = selectedIndex + ) + } + }, + trailingIcon = { + Row { + IconButton(onClick = { sortExpanded = true }) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.Sort, + contentDescription = null ) - }, - leadingIcon = { + } IconButton( - onClick = { - screenSelectionExpanded = true - // Let's prevent writing to datastore everytime the user clicks ;) - if (!hasSeenTip) hasSeenTip = true - } + onClick = { onNavigateTo(Screen.Settings) } ) { Icon( - imageVector = Icons.Rounded.MusicNote, - contentDescription = null, - tint = if (!hasSeenTip) color else LocalContentColor.current + imageVector = Icons.Rounded.Settings, + contentDescription = null ) } - - DropdownMenu( - expanded = screenSelectionExpanded, - onDismissRequest = { screenSelectionExpanded = false }, + expanded = sortExpanded, + onDismissRequest = { sortExpanded = false }, modifier = Modifier .width(180.dp) .background(color = MaterialTheme.colorScheme.surface) ) { - ScreenSelection( - onNavigationItemClicked = onNavigationItemClicked, - selectedIndex = selectedIndex + SortRadioButtons( + sort = sort, + onChangeSort = { sort = !sort } ) } - }, - trailingIcon = { - Row { - IconButton(onClick = { sortExpanded = true }) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.Sort, - contentDescription = null - ) - } - IconButton( - onClick = { onNavigateTo(Screen.Settings) } - ) { - Icon( - imageVector = Icons.Rounded.Settings, - contentDescription = null - ) - } - DropdownMenu( - expanded = sortExpanded, - onDismissRequest = { sortExpanded = false }, - modifier = Modifier - .width(180.dp) - .background(color = MaterialTheme.colorScheme.surface) - ) { - SortRadioButtons() - } - } - }, - currentlyPlaying = currentlyPlaying, - onHandlePlayerActions = { onHandlePlayerAction(it) }, - isPlaying = isCurrentlyPlaying, - animatedVisibilityScope = animatedVisibilityScope, - isPlaylistEmpty = isPlaylistEmpty - ) - } + } + }, + currentlyPlaying = currentlyPlaying, + onHandlePlayerActions = { onHandlePlayerAction(it) }, + isPlaying = isCurrentlyPlaying, + animatedVisibilityScope = animatedVisibilityScope, + isPlaylistEmpty = isPlaylistEmpty, + onNavigate = { onNavigateTo(Screen.NowPlaying) } + ) } } } @@ -343,9 +333,10 @@ private fun SharedTransitionScope.MainScreenContent( @Composable fun MusicListItem( + modifier: Modifier = Modifier, music: MediaItem, onShortClick: (String) -> Unit, - onNavigate: (Screen) -> Unit, + onNavigate: (Screen) -> Unit = {}, currentMusicUri: String, onLoadMetadata: ((String) -> Unit)? = null, showBottomSheet: Boolean = false @@ -363,19 +354,19 @@ fun MusicListItem( ModalBottomSheet( modifier = Modifier.fillMaxHeight(), sheetState = sheetState, - onDismissRequest = { isSheetOpen = false } + onDismissRequest = { isSheetOpen = false }, ) { BottomSheetContent( music = music, onNavigate = { onNavigate(it) }, onDismiss = { isSheetOpen = false }, - onLoadMetadata = onLoadMetadata + onLoadMetadata = onLoadMetadata, ) } } Row( - modifier = Modifier + modifier = modifier .fillMaxWidth() .clip(RoundedCornerShape(24.dp)) .combinedClickable( diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/MainScreenLandscape.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/MainScreenLandscape.kt index ed5a877..f36ce4d 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/MainScreenLandscape.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/MainScreenLandscape.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding @@ -29,7 +28,6 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -40,7 +38,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.media3.common.MediaItem import com.sosauce.cutemusic.R @@ -66,7 +63,7 @@ fun SharedTransitionScope.MainScreenLandscape( isCurrentlyPlaying: Boolean, ) { - val sort by rememberSortASC() + var sort by rememberSortASC() var query by remember { mutableStateOf("") } var sortExpanded by remember { mutableStateOf(false) } var screenSelectionExpanded by remember { mutableStateOf(false) } @@ -88,119 +85,107 @@ fun SharedTransitionScope.MainScreenLandscape( } - Scaffold { values -> - Box(Modifier.fillMaxSize()) { - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .padding(values) - .padding(start = 80.dp), - verticalArrangement = Arrangement.Top - ) { - items(displayMusics) { music -> - MusicListItem( - onShortClick = { onShortClick(music.mediaId) }, - music = music, - onNavigate = { onNavigateTo(it) }, - currentMusicUri = viewModel.currentMusicUri + Box(Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(start = 30.dp), + verticalArrangement = Arrangement.Top + ) { + items(displayMusics) { music -> + MusicListItem( + onShortClick = { onShortClick(music.mediaId) }, + music = music, + onNavigate = { onNavigateTo(it) }, + currentMusicUri = viewModel.currentMusicUri - ) - } + ) } + } + + CuteSearchbar( + query = query, + onQueryChange = { query = it }, + modifier = Modifier + .navigationBarsPadding() + .fillMaxWidth(0.4f) + .padding( + bottom = 5.dp, + end = 10.dp + ) + .align(Alignment.BottomEnd) + .sharedElement( + state = rememberSharedContentState(key = "searchbar"), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = { _, _ -> + tween(durationMillis = 500) + } + ), + placeholder = { + CuteText( + text = stringResource(id = R.string.search) + " " + stringResource(id = R.string.music), + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), - CuteSearchbar( - query = query, - onQueryChange = { query = it }, - modifier = Modifier - .navigationBarsPadding() - .fillMaxWidth(0.4f) - .padding( - bottom = values.calculateBottomPadding() + 5.dp, - end = values.calculateEndPadding( - layoutDirection = LayoutDirection.Rtl - ) + 10.dp - ) - .align(Alignment.BottomEnd) - .background( - color = MaterialTheme.colorScheme.surface, - shape = RoundedCornerShape(24.dp) ) - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.surfaceContainer, - shape = RoundedCornerShape(24.dp) + }, + leadingIcon = { + IconButton(onClick = { screenSelectionExpanded = true }) { + Icon( + imageVector = Icons.Rounded.MusicNote, + contentDescription = null ) - .clip(RoundedCornerShape(24.dp)) - .clickable { onNavigateTo(Screen.NowPlaying) } - .sharedElement( - state = rememberSharedContentState(key = "searchbar"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } - ), - placeholder = { - CuteText( - text = stringResource(id = R.string.search) + " " + stringResource(id = R.string.music), - color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), + } + DropdownMenu( + expanded = screenSelectionExpanded, + onDismissRequest = { screenSelectionExpanded = false }, + modifier = Modifier + .width(180.dp) + .background(color = MaterialTheme.colorScheme.surface) + ) { + ScreenSelection( + onNavigationItemClicked = onNavigationItemClicked, + selectedIndex = selectedIndex + ) + } + }, + trailingIcon = { + Row { + IconButton(onClick = { sortExpanded = true }) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.Sort, + contentDescription = null ) - }, - leadingIcon = { - IconButton(onClick = { screenSelectionExpanded = true }) { + } + IconButton( + onClick = { onNavigateTo(Screen.Settings) } + ) { Icon( - imageVector = Icons.Rounded.MusicNote, + imageVector = Icons.Rounded.Settings, contentDescription = null ) } - DropdownMenu( - expanded = screenSelectionExpanded, - onDismissRequest = { screenSelectionExpanded = false }, + expanded = sortExpanded, + onDismissRequest = { sortExpanded = false }, modifier = Modifier .width(180.dp) .background(color = MaterialTheme.colorScheme.surface) ) { - ScreenSelection( - onNavigationItemClicked = onNavigationItemClicked, - selectedIndex = selectedIndex + SortRadioButtons( + sort = sort, + onChangeSort = { sort = !sort } ) } - }, - trailingIcon = { - Row { - IconButton(onClick = { sortExpanded = true }) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.Sort, - contentDescription = null - ) - } - IconButton( - onClick = { onNavigateTo(Screen.Settings) } - ) { - Icon( - imageVector = Icons.Rounded.Settings, - contentDescription = null - ) - } - DropdownMenu( - expanded = sortExpanded, - onDismissRequest = { sortExpanded = false }, - modifier = Modifier - .width(180.dp) - .background(color = MaterialTheme.colorScheme.surface) - ) { - SortRadioButtons() - } - } - }, - currentlyPlaying = currentlyPlaying, - onHandlePlayerActions = { viewModel.handlePlayerActions(it) }, - isPlaying = isCurrentlyPlaying, - animatedVisibilityScope = animatedVisibilityScope, - isPlaylistEmpty = viewModel.isPlaylistEmpty() - ) + } + }, + currentlyPlaying = currentlyPlaying, + onHandlePlayerActions = { viewModel.handlePlayerActions(it) }, + isPlaying = isCurrentlyPlaying, + animatedVisibilityScope = animatedVisibilityScope, + isPlaylistEmpty = viewModel.isPlaylistEmptyAndDataNotNull(), + onNavigate = { onNavigateTo(Screen.NowPlaying) } + ) - } } } \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/components/BottomSheetContent.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/components/BottomSheetContent.kt index d898544..07bf474 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/components/BottomSheetContent.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/components/BottomSheetContent.kt @@ -49,7 +49,7 @@ fun BottomSheetContent( music: MediaItem, onNavigate: (Screen) -> Unit, onDismiss: () -> Unit, - onLoadMetadata: ((String) -> Unit)? = null + onLoadMetadata: ((String) -> Unit)? = null, ) { val context = LocalContext.current val fileBitrate = @@ -69,6 +69,7 @@ fun BottomSheetContent( context.resources.getText(R.string.deleting_song_OK), Toast.LENGTH_SHORT ).show() + } else { Toast.makeText( context, @@ -79,8 +80,6 @@ fun BottomSheetContent( } Column { - - Card( modifier = Modifier .fillMaxWidth() @@ -95,7 +94,7 @@ fun BottomSheetContent( MaterialTheme.colorScheme.surfaceContainerHighest.copy( alpha = 0.5f ) - ), + ) ) { Row(verticalAlignment = Alignment.CenterVertically) { AsyncImage( diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataEditor.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataEditor.kt index ff431f1..8485b64 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataEditor.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataEditor.kt @@ -260,6 +260,17 @@ fun MetadataEditorContent( metadataState.mutablePropertiesMap[6] = disc } } + EditTextField( + value = metadataState.mutablePropertiesMap[7], + label = { + CuteText( + text = "Lyrics", + modifier = Modifier.basicMarquee() + ) + } + ) { lyrics -> + metadataState.mutablePropertiesMap[7] = lyrics + } } } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataViewModel.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataViewModel.kt index 397f1b8..17f2168 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataViewModel.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataViewModel.kt @@ -43,8 +43,10 @@ class MetadataViewModel( getFirst(FieldKey.GENRE), getFirst(FieldKey.TRACK), getFirst(FieldKey.DISC_NO), + getFirst(FieldKey.LYRICS), ) + tagList.forEach { _metadata.value.mutablePropertiesMap.add(it) } @@ -65,7 +67,8 @@ class MetadataViewModel( FieldKey.YEAR to 3, FieldKey.GENRE to 4, FieldKey.TRACK to 5, - FieldKey.DISC_NO to 6 + FieldKey.DISC_NO to 6, + FieldKey.LYRICS to 7 ) tagList.forEach { setField(it.key, _metadata.value.mutablePropertiesMap[it.value]) diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingLandscape.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingLandscape.kt index 91d46c2..cea8405 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingLandscape.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingLandscape.kt @@ -5,7 +5,9 @@ package com.sosauce.cutemusic.ui.screens.playing import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.core.animateIntAsState import androidx.compose.animation.core.tween +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -13,11 +15,15 @@ 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.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Article import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.rounded.FastForward import androidx.compose.material.icons.rounded.FastRewind @@ -39,6 +45,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -47,6 +54,7 @@ import androidx.navigation.NavController import coil3.compose.AsyncImage import com.sosauce.cutemusic.R import com.sosauce.cutemusic.data.actions.PlayerActions +import com.sosauce.cutemusic.ui.screens.lyrics.LyricsView import com.sosauce.cutemusic.ui.screens.playing.components.LoopButton import com.sosauce.cutemusic.ui.screens.playing.components.MusicSlider import com.sosauce.cutemusic.ui.screens.playing.components.ShuffleButton @@ -60,7 +68,6 @@ fun SharedTransitionScope.NowPlayingLandscape( navController: NavController, animatedVisibilityScope: AnimatedVisibilityScope ) { - NPLContent( viewModel = viewModel, onEvent = viewModel::handlePlayerActions, @@ -81,160 +88,196 @@ private fun SharedTransitionScope.NPLContent( animatedVisibilityScope: AnimatedVisibilityScope ) { var showSpeedCard by remember { mutableStateOf(false) } + var showLyrics by remember { mutableStateOf(false) } - if (showSpeedCard) SpeedCard( - viewModel = viewModel, - onDismiss = { showSpeedCard = false } - ) + if (showSpeedCard) { + SpeedCard( + viewModel = viewModel, + onDismiss = { showSpeedCard = false } + ) + } - Scaffold { _ -> + val imgSize by animateIntAsState( + targetValue = if (showLyrics) 200 else 320, + label = "Image Size" + ) Box( modifier = Modifier .fillMaxSize() - .padding(45.dp) - + .statusBarsPadding() + .navigationBarsPadding() + .padding(start = 30.dp, end = 30.dp) ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxSize() ) { - AsyncImage( - model = viewModel.currentArt, - stringResource(R.string.artwork), - modifier = Modifier - .size(300.dp) - .clip(RoundedCornerShape(10)), - contentScale = ContentScale.Crop - ) - Spacer(modifier = Modifier.width(10.dp)) - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row( - Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - IconButton( - onClick = { onNavigateUp() } - ) { - Icon( - imageVector = Icons.Default.KeyboardArrowDown, - contentDescription = null, - modifier = Modifier.size(28.dp) - ) - } - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxWidth(0.9f) + Column { + AsyncImage( + model = viewModel.currentArt, + stringResource(R.string.artwork), + modifier = Modifier + .size(imgSize.dp) + .clip(RoundedCornerShape(10)), + contentScale = ContentScale.Crop + ) + if (showLyrics) { + Spacer(Modifier.height(10.dp)) + CuteText( + text = viewModel.currentlyPlaying + ) + CuteText( + text = viewModel.currentArtist ) - { - CuteText( - text = viewModel.currentlyPlaying, - - color = MaterialTheme.colorScheme.onBackground, - fontSize = 30.sp - ) - } } - CuteText( - text = viewModel.currentArtist, - - color = MaterialTheme.colorScheme.onBackground.copy(0.85f), - fontSize = 16.sp - ) - MusicSlider( - viewModel = viewModel + } + Spacer(modifier = Modifier.width(10.dp)) + if (showLyrics) { + LyricsView( + viewModel = viewModel, + onHideLyrics = { showLyrics = false }, + path = viewModel.currentPath, + isLandscape = true ) - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, + } else { + Column( modifier = Modifier - .fillMaxWidth() - .padding(8.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally ) { - ShuffleButton( - onClick = { onClickShuffle(it) } - ) - IconButton( - onClick = { onEvent(PlayerActions.SeekToPreviousMusic) } + Row( + Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically ) { - Icon( - imageVector = Icons.Rounded.SkipPrevious, - contentDescription = null, - modifier = Modifier.sharedElement( - state = rememberSharedContentState(key = "skipPreviousButton"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } + IconButton( + onClick = { onNavigateUp() } + ) { + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = null, + modifier = Modifier.size(28.dp) ) + } + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth(0.9f) ) - } - IconButton( - onClick = { onEvent(PlayerActions.RewindTo(5000)) } - ) { - Icon( - imageVector = Icons.Rounded.FastRewind, - contentDescription = null - ) - } + { + CuteText( + text = viewModel.currentlyPlaying, - FloatingActionButton( - onClick = { onEvent(PlayerActions.PlayOrPause) } - ) { - Icon( - imageVector = if (viewModel.isCurrentlyPlaying) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, - contentDescription = "play/pause button" - ) + color = MaterialTheme.colorScheme.onBackground, + fontSize = 30.sp + ) + } } - IconButton( - onClick = { onEvent(PlayerActions.SeekTo(5000)) } + CuteText( + text = viewModel.currentArtist, + + color = MaterialTheme.colorScheme.onBackground.copy(0.85f), + fontSize = 16.sp + ) + MusicSlider( + viewModel = viewModel + ) + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) ) { - Icon( - imageVector = Icons.Rounded.FastForward, - contentDescription = null + ShuffleButton( + onClick = { onClickShuffle(it) }, + isShuffling = viewModel.isShuffling ) - } + IconButton( + onClick = { onEvent(PlayerActions.SeekToPreviousMusic) } + ) { + Icon( + imageVector = Icons.Rounded.SkipPrevious, + contentDescription = null, + modifier = Modifier.sharedElement( + state = rememberSharedContentState(key = "skipPreviousButton"), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = { _, _ -> + tween(durationMillis = 500) + } + ) + ) + } + IconButton( + onClick = { onEvent(PlayerActions.RewindTo(5000)) } + ) { + Icon( + imageVector = Icons.Rounded.FastRewind, + contentDescription = null + ) + } - IconButton( - onClick = { onEvent(PlayerActions.SeekToNextMusic) } - ) { - Icon( - imageVector = Icons.Rounded.SkipNext, - contentDescription = null, - modifier = Modifier.sharedElement( - state = rememberSharedContentState(key = "skipNextButton"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } + FloatingActionButton( + onClick = { onEvent(PlayerActions.PlayOrPause) } + ) { + Icon( + imageVector = if (viewModel.isCurrentlyPlaying) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, + contentDescription = "play/pause button" + ) + } + IconButton( + onClick = { onEvent(PlayerActions.SeekTo(5000)) } + ) { + Icon( + imageVector = Icons.Rounded.FastForward, + contentDescription = null ) + } + + IconButton( + onClick = { onEvent(PlayerActions.SeekToNextMusic) } + ) { + Icon( + imageVector = Icons.Rounded.SkipNext, + contentDescription = null, + modifier = Modifier.sharedElement( + state = rememberSharedContentState(key = "skipNextButton"), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = { _, _ -> + tween(durationMillis = 500) + } + ) + ) + } + + LoopButton( + onClick = { onClickLoop(it) }, + isLooping = viewModel.isLooping ) } - LoopButton( - onClick = { onClickLoop(it) } - ) - } - Spacer(modifier = Modifier.weight(1f)) - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - ) { - IconButton(onClick = { showSpeedCard = true }) { - Icon( - imageVector = Icons.Rounded.Speed, - contentDescription = "change speed" - ) + Spacer(modifier = Modifier.weight(1f)) + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + ) { + IconButton(onClick = { showSpeedCard = true }) { + Icon( + imageVector = Icons.Rounded.Speed, + contentDescription = "change speed" + ) + } + IconButton(onClick = { showLyrics = true }) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.Article, + contentDescription = "show lyrics" + ) + } } } } } } - } } \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingScreen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingScreen.kt index 954a6a5..b393e0f 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingScreen.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingScreen.kt @@ -5,6 +5,7 @@ package com.sosauce.cutemusic.ui.screens.playing import androidx.annotation.OptIn import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.Crossfade import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.core.animateIntAsState @@ -22,11 +23,13 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Article import androidx.compose.material.icons.rounded.FastForward import androidx.compose.material.icons.rounded.FastRewind import androidx.compose.material.icons.rounded.KeyboardArrowDown import androidx.compose.material.icons.rounded.Pause import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material.icons.rounded.RestartAlt import androidx.compose.material.icons.rounded.SkipNext import androidx.compose.material.icons.rounded.SkipPrevious import androidx.compose.material.icons.rounded.Speed @@ -54,6 +57,7 @@ import coil3.compose.AsyncImage import com.sosauce.cutemusic.R import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.data.datastore.rememberIsLandscape +import com.sosauce.cutemusic.ui.screens.lyrics.LyricsView import com.sosauce.cutemusic.ui.screens.playing.components.LoopButton import com.sosauce.cutemusic.ui.screens.playing.components.MusicSlider import com.sosauce.cutemusic.ui.screens.playing.components.ShuffleButton @@ -61,6 +65,7 @@ import com.sosauce.cutemusic.ui.screens.playing.components.SpeedCard import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.utils.ImageUtils +import com.sosauce.cutemusic.utils.thenIf @OptIn(UnstableApi::class) @@ -70,6 +75,8 @@ fun SharedTransitionScope.NowPlayingScreen( viewModel: MusicViewModel, animatedVisibilityScope: AnimatedVisibilityScope ) { + var showFullLyrics by remember { mutableStateOf(false) } + if (rememberIsLandscape()) { NowPlayingLandscape( viewModel = viewModel, @@ -77,14 +84,27 @@ fun SharedTransitionScope.NowPlayingScreen( animatedVisibilityScope = animatedVisibilityScope ) } else { - NowPlayingContent( - viewModel = viewModel, - onEvent = viewModel::handlePlayerActions, - onNavigateUp = navController::navigateUp, - onClickLoop = { viewModel.setLoop(it) }, - onClickShuffle = { viewModel.setShuffle(it) }, - animatedVisibilityScope = animatedVisibilityScope - ) + when (showFullLyrics) { + true -> { + LyricsView( + viewModel = viewModel, + onHideLyrics = { showFullLyrics = false }, + path = viewModel.currentPath + ) + } + + false -> { + NowPlayingContent( + viewModel = viewModel, + onEvent = viewModel::handlePlayerActions, + onNavigateUp = navController::navigateUp, + onClickLoop = { viewModel.setLoop(it) }, + onClickShuffle = { viewModel.setShuffle(it) }, + animatedVisibilityScope = animatedVisibilityScope, + onShowLyrics = { showFullLyrics = true } + ) + } + } } } @@ -96,12 +116,14 @@ private fun SharedTransitionScope.NowPlayingContent( onNavigateUp: () -> Unit, onClickLoop: (Boolean) -> Unit, onClickShuffle: (Boolean) -> Unit, - animatedVisibilityScope: AnimatedVisibilityScope + animatedVisibilityScope: AnimatedVisibilityScope, + onShowLyrics: () -> Unit ) { val context = LocalContext.current var showSpeedCard by remember { mutableStateOf(false) } val roundedFAB by animateIntAsState( - targetValue = if (viewModel.isCurrentlyPlaying) 30 else 50, label = "FAB Shape" + targetValue = if (viewModel.isCurrentlyPlaying) 30 else 50, + label = "FAB Shape" ) @@ -184,8 +206,6 @@ private fun SharedTransitionScope.NowPlayingContent( } ) .basicMarquee() - - ) Spacer(modifier = Modifier.height(5.dp)) CuteText( @@ -210,22 +230,41 @@ private fun SharedTransitionScope.NowPlayingContent( .padding(8.dp) ) { ShuffleButton( - onClick = { onClickShuffle(it) } + onClick = onClickShuffle, + isShuffling = viewModel.isShuffling ) IconButton( - onClick = { onEvent(PlayerActions.SeekToPreviousMusic) } + onClick = { + if (viewModel.currentPosition >= 10000) { + onEvent(PlayerActions.RestartSong) + } else { + onEvent(PlayerActions.SeekToPreviousMusic) + } + } ) { - Icon( - imageVector = Icons.Rounded.SkipPrevious, - contentDescription = null, - modifier = Modifier.sharedElement( - state = rememberSharedContentState(key = "skipPreviousButton"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } - ) - ) + Crossfade( + targetState = viewModel.currentPosition >= 10000, + label = "" + ) { + if (!it) { + Icon( + imageVector = Icons.Rounded.SkipPrevious, + contentDescription = null, + modifier = Modifier.sharedElement( + state = rememberSharedContentState(key = "skipPreviousButton"), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = { _, _ -> + tween(durationMillis = 500) + } + ) + ) + } else { + Icon( + imageVector = Icons.Rounded.RestartAlt, + contentDescription = null + ) + } + } } IconButton( onClick = { onEvent(PlayerActions.RewindTo(5000)) } @@ -277,7 +316,8 @@ private fun SharedTransitionScope.NowPlayingContent( ) } LoopButton( - onClick = { onClickLoop(it) } + onClick = { onClickLoop(it) }, + isLooping = viewModel.isLooping ) } Spacer(modifier = Modifier.weight(1f)) @@ -288,6 +328,12 @@ private fun SharedTransitionScope.NowPlayingContent( .fillMaxWidth() .navigationBarsPadding() ) { + IconButton(onClick = { onShowLyrics() }) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.Article, + contentDescription = "show lyrics" + ) + } IconButton(onClick = { showSpeedCard = true }) { Icon( imageVector = Icons.Rounded.Speed, diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/Buttons.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/Buttons.kt index 84976e3..6b39f7a 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/Buttons.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/Buttons.kt @@ -7,47 +7,40 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue @Composable fun LoopButton( - onClick: (Boolean) -> Unit + onClick: (Boolean) -> Unit, + isLooping: Boolean ) { - var shouldLoop by remember { mutableStateOf(false) } - IconButton( onClick = { - shouldLoop = !shouldLoop - onClick(shouldLoop) + onClick(!isLooping) } ) { Icon( imageVector = Icons.Rounded.Loop, contentDescription = "loop button", - tint = if (shouldLoop) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground + tint = if (isLooping) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground ) } } @Composable fun ShuffleButton( - onClick: (Boolean) -> Unit + onClick: (Boolean) -> Unit, + isShuffling: Boolean ) { - var shouldShuffle by remember { mutableStateOf(false) } IconButton( onClick = { - shouldShuffle = !shouldShuffle - onClick(shouldShuffle) + onClick(!isShuffling) } ) { Icon( imageVector = Icons.Rounded.Shuffle, contentDescription = "shuffle button", - tint = if (shouldShuffle) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground + tint = if (isShuffling) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground ) } } \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/SpeedCard.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/SpeedCard.kt index b591893..55fdecd 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/SpeedCard.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/SpeedCard.kt @@ -4,12 +4,17 @@ package com.sosauce.cutemusic.ui.screens.playing.components import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource +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.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider @@ -25,7 +30,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.sosauce.cutemusic.R +import com.sosauce.cutemusic.data.datastore.rememberSnapSpeedAndPitch import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.MusicViewModel @@ -35,9 +42,11 @@ fun SpeedCard( viewModel: MusicViewModel ) { var speed by remember { mutableFloatStateOf(viewModel.getPlaybackSpeed().speed) } + var pitch by remember { mutableFloatStateOf(viewModel.getPlaybackSpeed().pitch) } val interactionSource = remember { MutableInteractionSource() } + var snap by rememberSnapSpeedAndPitch() AlertDialog( onDismissRequest = { onDismiss() }, @@ -56,45 +65,141 @@ fun SpeedCard( ) }, text = { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Slider( - value = speed, - onValueChange = { - speed = it - viewModel.setPlaybackSpeed(speed) - }, - valueRange = 0.5f..2f, - track = { sliderState -> - SliderDefaults.Track( - sliderState = sliderState, - drawStopIndicator = null, - thumbTrackGapSize = 4.dp, - modifier = Modifier.height(8.dp) + Box { + Column { + Spacer(Modifier.height(5.dp)) + if (!snap) { + Box( + modifier = Modifier + .background( + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), + shape = RoundedCornerShape(16.dp) + ) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + CuteText( + text = "Speed: " + "%.2f".format(speed), + color = MaterialTheme.colorScheme.onBackground, + fontSize = 15.sp + ) + } + Slider( + value = speed, + onValueChange = { + speed = it + viewModel.setPlaybackSpeed(speed, pitch) + }, + valueRange = 0.5f..2f, + track = { sliderState -> + SliderDefaults.Track( + sliderState = sliderState, + drawStopIndicator = null, + thumbTrackGapSize = 4.dp, + modifier = Modifier.height(8.dp) + ) + }, + thumb = { + SliderDefaults.Thumb( + interactionSource = interactionSource, + thumbSize = DpSize(width = 4.dp, height = 25.dp) + ) + }, + interactionSource = interactionSource ) - }, - thumb = { - SliderDefaults.Thumb( - interactionSource = interactionSource, - thumbSize = DpSize(width = 4.dp, height = 25.dp) + Spacer(Modifier.height(15.dp)) + // + Box( + modifier = Modifier + .background( + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), + shape = RoundedCornerShape(16.dp) + ) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + CuteText( + text = "Pitch: " + "%.2f".format(pitch), + color = MaterialTheme.colorScheme.onBackground + ) + } + Slider( + value = pitch, + onValueChange = { + pitch = it + viewModel.setPlaybackSpeed(speed, pitch) + }, + valueRange = 0.5f..2f, + track = { sliderState -> + SliderDefaults.Track( + sliderState = sliderState, + drawStopIndicator = null, + thumbTrackGapSize = 4.dp, + modifier = Modifier.height(8.dp) + ) + }, + thumb = { + SliderDefaults.Thumb( + interactionSource = interactionSource, + thumbSize = DpSize(width = 4.dp, height = 25.dp) + ) + }, + interactionSource = interactionSource ) - }, - interactionSource = interactionSource - ) - Box( - modifier = Modifier - .background( - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), - shape = RoundedCornerShape(16.dp) + } else { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Slider( + value = speed, + onValueChange = { + speed = it + pitch = it + viewModel.setPlaybackSpeed(speed, pitch) + }, + valueRange = 0.5f..2f, + track = { sliderState -> + SliderDefaults.Track( + sliderState = sliderState, + drawStopIndicator = null, + thumbTrackGapSize = 4.dp, + modifier = Modifier.height(8.dp) + ) + }, + thumb = { + SliderDefaults.Thumb( + interactionSource = interactionSource, + thumbSize = DpSize(width = 4.dp, height = 25.dp) + ) + }, + interactionSource = interactionSource + ) + Box( + modifier = Modifier + .background( + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), + shape = RoundedCornerShape(16.dp) + ) + .padding(horizontal = 8.dp, vertical = 4.dp), + ) { + CuteText( + text = "%.2f".format(speed), + color = MaterialTheme.colorScheme.onBackground + ) + } + } + } + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + Checkbox( + checked = snap, + onCheckedChange = { snap = !snap } ) - .padding(horizontal = 8.dp, vertical = 4.dp) - ) { - CuteText( - text = "%.2f".format(speed), - - color = MaterialTheme.colorScheme.onBackground - ) + CuteText( + text = "Snap speed and pitch" + ) + } } } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/Switches.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/Switches.kt index 58434bb..1b3e26e 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/Switches.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/Switches.kt @@ -32,6 +32,7 @@ fun Misc( ) { val context = LocalContext.current var useSystemFont by rememberUseSystemFont() +// var killService by remember { rememberKillService(context) } Column { CuteText( @@ -52,6 +53,13 @@ fun Misc( topDp = 24.dp, bottomDp = 4.dp ) +// SettingsCards( +// checked = killService, +// onCheckedChange = { killService = !killService }, +// topDp = 4.dp, +// bottomDp = 4.dp, +// text = "Kill Service" +// ) SettingsCards( checked = useSystemFont, onCheckedChange = { useSystemFont = !useSystemFont }, @@ -81,7 +89,6 @@ fun ThemeManagement() { Column { CuteText( text = stringResource(id = R.string.theme), - color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(horizontal = 34.dp, vertical = 8.dp) ) diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/CuteText.kt b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/CuteText.kt index 575f664..981e392 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/CuteText.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/CuteText.kt @@ -6,8 +6,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.TextUnit import com.sosauce.cutemusic.data.datastore.rememberUseSystemFont import com.sosauce.cutemusic.ui.theme.GlobalFont @@ -20,7 +22,9 @@ fun CuteText( fontSize: TextUnit = TextUnit.Unspecified, textAlign: TextAlign? = null, maxLines: Int = Int.MAX_VALUE, - style: TextStyle = LocalTextStyle.current + style: TextStyle = LocalTextStyle.current, + onTextLayout: ((TextLayoutResult) -> Unit)? = null, + overflow: TextOverflow = TextOverflow.Clip, ) { val useSystemFont by rememberUseSystemFont() val fontFamily = if (useSystemFont) { @@ -37,6 +41,8 @@ fun CuteText( textAlign = textAlign, maxLines = maxLines, fontFamily = fontFamily, - style = style + style = style, + onTextLayout = onTextLayout, + overflow = overflow ) } \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/MusicViewModel.kt b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/MusicViewModel.kt index cea9c89..654f89b 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/MusicViewModel.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/MusicViewModel.kt @@ -19,9 +19,13 @@ import androidx.media3.session.MediaController import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.MoreExecutors import com.sosauce.cutemusic.data.actions.PlayerActions +import com.sosauce.cutemusic.domain.model.Lyrics import com.sosauce.cutemusic.ui.customs.playAtIndex import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.jaudiotagger.audio.AudioFileIO +import org.jaudiotagger.tag.FieldKey +import java.io.File class MusicViewModel( private val controllerFuture: ListenableFuture @@ -40,7 +44,12 @@ class MusicViewModel( var currentPosition by mutableLongStateOf(0L) var currentMusicDuration by mutableLongStateOf(0L) var currentMusicUri by mutableStateOf("") + var currentLyrics by mutableStateOf(listOf()) + var isLooping by mutableStateOf(false) + var isShuffling by mutableStateOf(false) + var currentPath by mutableStateOf("") + private var currentLrcFile by mutableStateOf(null) private val playerListener = object : Player.Listener { override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) { @@ -48,7 +57,10 @@ class MusicViewModel( currentlyPlaying = (mediaMetadata.title ?: currentlyPlaying).toString() currentArtist = (mediaMetadata.artist ?: currentArtist).toString() currentArt = mediaMetadata.artworkUri ?: currentArt + currentPath = (mediaMetadata.extras?.getString("path") ?: currentPath) currentMusicUri = mediaMetadata.extras?.getString("uri") ?: currentMusicUri + currentLrcFile = loadLrcFile(currentPath) + currentLyrics = parseLrcFile(currentLrcFile) } override fun onIsPlayingChanged(isPlaying: Boolean) { @@ -83,6 +95,57 @@ class MusicViewModel( ) } + private fun loadLrcFile(path: String): File? { + val lrcFilePath = path.replaceAfterLast('.', "lrc") + val lrcFile = File(lrcFilePath) + return if (lrcFile.exists()) lrcFile else null + } + + private fun parseLrcFile(file: File?): List { + val lyrics = mutableListOf() + if (file == null) { + return emptyList() + } + + file.bufferedReader().useLines { lines -> + lines.forEach { line -> + val regex = Regex("""\[(\d{2}):(\d{2})\.(\d{2})]""") + val matchResult = regex.find(line) + + if (matchResult != null) { + val (minutes, seconds, hundredths) = matchResult.destructured + val timeInMillis = + minutes.toLong() * 60_000 + seconds.toLong() * 1000 + hundredths.toLong() * 10 + val lyricText = line.substring(matchResult.range.last + 1).trim() + lyrics.add( + Lyrics( + timeInMillis, + lyricText + ) + ) + } + } + } + + return lyrics + } + + fun loadEmbeddedLyrics( + path: String + ): String { + val file = AudioFileIO.read(File(path)) + + file.tag.apply { + val embeddedLyrics = getFirst(FieldKey.LYRICS) + + return if (embeddedLyrics != "") { + embeddedLyrics + } else { + "No lyrics found !" + } + } + } + override fun onCleared() { super.onCleared() mediaController!!.removeListener(playerListener) @@ -98,7 +161,7 @@ class MusicViewModel( mediaId = mediaId ) } catch (e: Exception) { - Log.d("CuteError", "There was a problem playing the music.") + Log.d("CuteError", e.message.toString()) } } @@ -118,29 +181,42 @@ class MusicViewModel( } - fun isPlaylistEmpty(): Boolean { - return if (mediaController == null) false else mediaController!!.mediaItemCount != 0 + + fun isPlaylistEmptyAndDataNotNull(): Boolean { + return if (mediaController == null || currentlyPlaying == "") { + false + } else { + mediaController!!.mediaItemCount != 0 + } } - fun setPlaybackSpeed(speed: Float) { + fun setPlaybackSpeed( + speed: Float = 1f, + pitch: Float = 1f + ) { mediaController!!.playbackParameters = PlaybackParameters( speed, - speed - ) // Pitch and speed not being the same is just ugly so set both to be the same #besties + pitch + ) } - fun setLoop(shouldLoop: Boolean) { + fun setLoop( + shouldLoop: Boolean + ) { if (shouldLoop) { mediaController!!.repeatMode = Player.REPEAT_MODE_ONE + isLooping = true } else { mediaController!!.repeatMode = Player.REPEAT_MODE_OFF + isLooping = false } } fun setShuffle(shouldShuffle: Boolean) { mediaController!!.shuffleModeEnabled = shouldShuffle + isShuffling = mediaController!!.shuffleModeEnabled } fun quickPlay(uri: Uri?) { @@ -163,6 +239,7 @@ class MusicViewModel( is PlayerActions.SeekTo -> mediaController!!.seekTo(mediaController!!.currentPosition + action.position) is PlayerActions.SeekToSlider -> mediaController!!.seekTo(action.position) is PlayerActions.RewindTo -> mediaController!!.seekTo(mediaController!!.currentPosition - action.position) + is PlayerActions.RestartSong -> mediaController!!.seekTo(0) } } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/PostViewModel.kt b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/PostViewModel.kt index 209eab9..81833ad 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/PostViewModel.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/PostViewModel.kt @@ -5,18 +5,28 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import androidx.media3.common.MediaItem import com.sosauce.cutemusic.domain.model.Album import com.sosauce.cutemusic.domain.repository.MediaStoreHelper +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.WhileSubscribed +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn class PostViewModel( mediaStoreHelper: MediaStoreHelper ) : ViewModel() { - var musics = mediaStoreHelper - .getMusics() - .sortedBy { it.mediaMetadata.title.toString().lowercase() } + + val musics = + mediaStoreHelper + .getMusics() + .sortedBy { it.mediaMetadata.title.toString().lowercase() } var albums = mediaStoreHelper .getAlbums() diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/RadioButtons.kt b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/RadioButtons.kt index 6d18c69..59ccf6f 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/RadioButtons.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/RadioButtons.kt @@ -21,8 +21,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -31,13 +29,14 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.sosauce.cutemusic.R -import com.sosauce.cutemusic.data.datastore.rememberSortASC import com.sosauce.cutemusic.ui.navigation.Screen @Composable -fun SortRadioButtons() { +fun SortRadioButtons( + sort: Boolean, + onChangeSort: () -> Unit +) { - var sort by rememberSortASC() Column( verticalArrangement = Arrangement.Center @@ -46,7 +45,7 @@ fun SortRadioButtons() { modifier = Modifier .fillMaxWidth() .height(56.dp) - .clickable { sort = !sort } + .clickable { onChangeSort() } .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -64,7 +63,7 @@ fun SortRadioButtons() { modifier = Modifier .fillMaxWidth() .height(56.dp) - .clickable { sort = !sort } + .clickable { onChangeSort() } .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically ) { diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/Searchbar.kt b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/Searchbar.kt index 8088108..92a3313 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/Searchbar.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/Searchbar.kt @@ -6,10 +6,14 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.slideInVertically +import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -28,12 +32,15 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.dp import com.sosauce.cutemusic.data.actions.PlayerActions +import com.sosauce.cutemusic.utils.thenIf @Composable fun SharedTransitionScope.CuteSearchbar( @@ -47,7 +54,8 @@ fun SharedTransitionScope.CuteSearchbar( onHandlePlayerActions: (PlayerActions) -> Unit, isPlaying: Boolean, animatedVisibilityScope: AnimatedVisibilityScope, - isPlaylistEmpty: Boolean + isPlaylistEmpty: Boolean, + onNavigate: () -> Unit ) = CustomSearchbar( query = query, onQueryChange = onQueryChange, @@ -59,7 +67,8 @@ fun SharedTransitionScope.CuteSearchbar( onHandlePlayerActions = onHandlePlayerActions, isPlaying = isPlaying, animatedVisibilityScope = animatedVisibilityScope, - isPlaylistEmpty = isPlaylistEmpty + isPlaylistEmpty = isPlaylistEmpty, + onNavigate = onNavigate ) @@ -75,17 +84,37 @@ private fun SharedTransitionScope.CustomSearchbar( onHandlePlayerActions: (PlayerActions) -> Unit, isPlaying: Boolean, animatedVisibilityScope: AnimatedVisibilityScope, - isPlaylistEmpty: Boolean + isPlaylistEmpty: Boolean, + onNavigate: () -> Unit ) { val focusManager = LocalFocusManager.current + val roundedShape by animateDpAsState( + targetValue = if (isPlaylistEmpty) 24.dp else 50.dp, + label = "" + ) Column( modifier = modifier + .background( + color = MaterialTheme.colorScheme.surface, + shape = RoundedCornerShape(roundedShape) + ) + .clip(RoundedCornerShape(roundedShape)) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.surfaceContainer, + shape = RoundedCornerShape(roundedShape) + ) + .thenIf( + isPlaylistEmpty, + Modifier.clickable { + onNavigate() + } + ) ) { AnimatedVisibility( visible = isPlaylistEmpty, enter = fadeIn() + slideInVertically(initialOffsetY = { it }) - ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -187,7 +216,7 @@ private fun SharedTransitionScope.CustomSearchbar( singleLine = true, modifier = Modifier .fillMaxWidth() - .padding(7.dp), + .padding(6.dp), keyboardActions = KeyboardActions( onDone = { focusManager.clearFocus() } ) diff --git a/app/src/main/java/com/sosauce/cutemusic/utils/Extensions.kt b/app/src/main/java/com/sosauce/cutemusic/utils/Extensions.kt new file mode 100644 index 0000000..c37e784 --- /dev/null +++ b/app/src/main/java/com/sosauce/cutemusic/utils/Extensions.kt @@ -0,0 +1,59 @@ +package com.sosauce.cutemusic.utils + +import android.content.ContentResolver +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.compose.ui.Modifier +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.launch + +fun ContentResolver.queryAsFlow( + uri: Uri, + projection: Array? = null, + selection: String? = null, + selectionArgs: Array? = null +) = callbackFlow { + val observer = object : ContentObserver(Handler(Looper.getMainLooper())) { + override fun onChange(selfChange: Boolean) { + launch(Dispatchers.IO) { + runCatching { + trySend(query(uri, projection, selection, selectionArgs, null)) + } + } + } + } + + registerContentObserver(uri, true, observer) + + launch(Dispatchers.IO) { + runCatching { + trySend( + query(uri, projection, selection, selectionArgs, null) + ) + }.onFailure { + Log.d("CuteError", it.message.toString()) + } + } + + awaitClose { + unregisterContentObserver(observer) + } + +}.conflate() + +fun Modifier.thenIf( + condition: Boolean, + modifier: Modifier +): Modifier { + return this.then( + if (condition) { + modifier + } else Modifier + ) +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d3157b6..580a04c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,22 +1,22 @@ [versions] -agp = "8.6.0" +agp = "8.6.1" jaudiotagger = "3.0.1" -koinAndroid = "3.5.6" -koinAndroidxCompose = "3.5.6" +koinAndroid = "4.0.0" +koinAndroidxCompose = "4.0.0" kotlin = "2.0.20" activityCompose = "1.9.2" coilCompose = "3.0.0-alpha10" -composeBom = "2024.09.01" -composeAnimation = "1.7.1" +composeBom = "2024.09.02" +composeAnimation = "1.7.2" coreKtx = "1.13.1" coreSplashscreen = "1.0.1" datastorePreferences = "1.1.1" kotlinxSerializationJson = "1.7.1" -lifecycleViewmodelCompose = "2.8.5" +lifecycleViewmodelCompose = "2.8.6" media3Common = "1.4.1" media3Exoplayer = "1.4.1" media3Session = "1.4.1" -navigationCompose = "2.8.0" +navigationCompose = "2.8.1" squigglyslider = "1.0.0" serialization = "2.0.0"