From 9c5a69404643e934aae673a59c8289ba7d217ea5 Mon Sep 17 00:00:00 2001 From: sosauce2 <98750531+sosauce@users.noreply.github.com> Date: Sat, 21 Dec 2024 03:13:24 +0100 Subject: [PATCH] v2.3.4 --- app/.DS_Store | Bin 10244 -> 10244 bytes app/build.gradle.kts | 20 +- app/release/baselineProfiles/0/app-release.dm | Bin 7345 -> 7355 bytes app/release/baselineProfiles/1/app-release.dm | Bin 7319 -> 7323 bytes app/release/output-metadata.json | 4 +- app/src/main/AndroidManifest.xml | 13 ++ .../cutemusic/data/datastore/DataStore.kt | 23 ++ .../com/sosauce/cutemusic/di/AppModule.kt | 10 +- .../domain/repository/MediaStoreHelper.kt | 6 +- .../domain/repository/MediaStoreHelperImpl.kt | 34 ++- .../cutemusic/domain/repository/SafManager.kt | 124 +++++++++++ .../cutemusic/main/AutoPlaybackService.kt | 70 ++++++ .../sosauce/cutemusic/main/PlaybackService.kt | 2 + .../cutemusic/ui/navigation/Navigation.kt | 14 ++ .../sosauce/cutemusic/ui/navigation/Screen.kt | 3 + .../ui/screens/album/AlbumDetailsScreen.kt | 53 +++-- .../cutemusic/ui/screens/album/AlbumScreen.kt | 29 ++- .../screens/all_folders/AllFoldersScreen.kt | 32 ++- .../ui/screens/artist/ArtistDetails.kt | 11 +- .../screens/artist/ArtistDetailsLandscape.kt | 13 +- .../screens/blacklisted/BlacklistedScreen.kt | 199 ++++++++---------- .../components/AllFolderBottomSheet.kt | 105 --------- .../cutemusic/ui/screens/main/MainScreen.kt | 180 ++++++++++------ .../ui/screens/metadata/MetadataEditor.kt | 28 +-- .../ui/screens/playing/NowPlayingLandscape.kt | 2 +- .../ui/screens/playing/NowPlayingScreen.kt | 15 +- .../ui/screens/playing/components/Buttons.kt | 15 +- .../playing/components/QuickActionsRow.kt | 59 +++++- .../cutemusic/ui/screens/saf/SafScreen.kt | 152 +++++++++++++ .../settings/compenents/SettingsCards.kt | 8 +- .../screens/settings/compenents/Switches.kt | 18 +- .../shared_components/MusicDetailsDialog.kt | 14 ++ .../ui/shared_components/MusicViewModel.kt | 144 +++++++++---- .../ui/shared_components/PostViewModel.kt | 31 ++- .../ui/shared_components/Searchbar.kt | 38 ++-- .../com/sosauce/cutemusic/utils/Constants.kt | 4 + .../com/sosauce/cutemusic/utils/Extensions.kt | 25 ++- .../res/drawable/trash_rounded_filled.xml | 10 + app/src/main/res/values/strings.xml | 6 + gradle/libs.versions.toml | 6 +- 40 files changed, 1038 insertions(+), 482 deletions(-) create mode 100644 app/src/main/java/com/sosauce/cutemusic/domain/repository/SafManager.kt create mode 100644 app/src/main/java/com/sosauce/cutemusic/main/AutoPlaybackService.kt delete mode 100644 app/src/main/java/com/sosauce/cutemusic/ui/screens/blacklisted/components/AllFolderBottomSheet.kt create mode 100644 app/src/main/java/com/sosauce/cutemusic/ui/screens/saf/SafScreen.kt create mode 100644 app/src/main/java/com/sosauce/cutemusic/utils/Constants.kt create mode 100644 app/src/main/res/drawable/trash_rounded_filled.xml diff --git a/app/.DS_Store b/app/.DS_Store index 693cf22cfa9232e3b0457eb7f2ab5938e8d8d71a..b435c5941b2f210da21a1a2bd67273973451c6c6 100644 GIT binary patch delta 53 zcmV-50LuS_P=rvhCJ_P3lff5&3>kZSGBz?eEFdj3GLx|o9RZq?&k!F0o3ru}6%Mlo LAo~Qf0u=)T;Or30 delta 53 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 S92^1cla(AG1|}Q;0002&Luf(( diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm index 05d5a95c458c2aeb76f4b34fa21033181cba8134..c46f92063556a0ea40176a705d415edcbcbc4695 100644 GIT binary patch delta 6463 zcmV-F8NlY3Ih#2QP)h>@6aWYa2msK8kqjSyml$a0aB^<|FflLy0o|0BkB1DxiRJ3)I6psO6tBLA$oYqUcp4anx@BLbg?%aFNJzw|SbI-l^%^Q2BxwEbCRbzg(*80w;A2a5@HTOR9$ODgD zdH)0JjHFndIbJ7Tz5LDuJZqdbn+%)J5uGWw&DVdnU$&|~%wI;b9%GBN&y$Qk&|WMV z{p43`&gTkVuwT=GcQOPgNHdl;d0)PU(XZf1U@*G$2wWMX`3~o!&vDWk%a^Hq=DT323sH*@d_DdDcFN&v*~Vsn0-;~I3{*)i1IJoL4vk_j=~M%p^fi$g7Bb!b3Fhr*cYU&)ow(uT6`v>kK<+; z)B*4rzAZ7%C_K>dz8B=tY>)KC$+QXm72ij(PJJ+kh}k-B*P3(EO|>#DW5Dwr+%uwd zo6K2fm0H<>(7mTf1J??m#NC|ggI zPGNk4Fg}Zi?uY^BN=6;)pU=<%*RcR~@WJ~Z8VyQi|9 z7DiVuj6PZ~{$%0t8Pds;U(3#1dz3A2`?`LDdA?Qh(hwiPcfrJeAFZU1j)sSRz23gq zfQ-vSaCv^jI4%?efd5v>um$2f>-Yy*!z&iIEm`kC2JIBBpqo#WFJ2Yt1&9&=C! z=!*W}9BF=-!{ewLHS= zDfPRuup=^Tb*&Hw<$@pN(3X^ml6ByqAMHbu(T4p1!#Ry-HNdgfbjkO^u~q=rnYcG7 z_?4=TH646+mwJ*9eFSalj1p@oY#mW1O3)$)W0oE_`0|W@qQ?M#lwVA^ZwbdYaoL-%cXv~WR1gmaJ+zjA_Q0KtjD9_0MllF5ht z)R!gUVLGvYkENzT9rhvrc~S=+al%T%aK^*hpiRhM&oyA$M~Q)#D??oo;2+oCSRnHmhfjxGu(O2gaa!={YxHa~d@^@@79v z!W%8aWNRMOQNvi*vUQ#PQKiP!^?LAuQ;TN2a~P$6gN8@RC!A$1+i8AdsYme+o;j$? zSY$M5rvAQ#IXa!^I2g;>=9!p2T2Fi@F3RBg(fsC1zs9XqkBXmnHo)WGL*|ftSwaWQ zGww55chrG{a~Ev`T;6+7*Z8%@((J2$-Y3gexT|tL*Ghe|^f*)H()RU?pSX-oD0Qqx zKVZ0j5A%*m9Maw-eg7#r_+AwHd9Om>&61%L_*2G0ABOura0qYYoJtywmoWzNb&Nqj z?|z^G9>+~uU6L>Pp^szMbqxc&mhL)+YgyB&{x_m*l)g{P_-VJGU7~23@MwEZRokqP zdFC@R`1?Z1(DfF{oPU(`0o0MP-mso<2f&$spwYX~{X(Q|0($B}UX%$utbbcNP^Vt( z2R?LRjDl{AZNTNaXN*7_wB!?9=lWR{aUd7ZFsYl?L!aS@&l0kz3uu^OGt|KkxWFR~ zaoDEbXmiY-Ea5mtPaF;xA2GOpYz|_e-_rV3DkU zyRd$Kr+~g-oC(99N-(YaWaw2Z7KPdJeF=up6ETr7neh^E#$m=xj)^#+(eWK;Je+0O zPkPd^)VeSZ6GmAqNkg7&N0o@tLE1kTSQP%Lf-elw;d>$+@;UZKb%A}uXuj;jX$O3- z!8*H}Cu-}8M;e+P{?s_*OIuh$pbSgSgA9Q1hwaj75II@{B^{DdeUrN16O z>WpcKS*Qo^7?=mm3wYd1b*$4kIiH5S5tER^&Og@)9C~1H!kvqHERZ_q?~v4gv3Eko zJt9=gVjZ}kXUrvhDwG%bx^rKc3OY@c4y$Brn7>Q>F=8F@c^2j$V}_rgPG9;{sZW&r zCZDD)r-BdEv$nBLrh=}gqYhidw%nIsXT%%qDcBbncQ9_i7>}@5pbmV_4d{@8d!uAL zzYESSaB=?Cd?6ccd}l%ap2$sQ%QsP zcD3Y*kcqWe@7Rf_*DvM{@Zj&{h>MbUeCSI!=bkXa>mAbr!Yd3n7<16>k-SFoa>;08 zKY|VUOqITkK4j>EyDoKq1TJs@Cl30!7Xrp{u+-~;_5wXJ`H~JagrSbLfIgl9LC=2B zXC((|bU*r-DI0Cj65eR}k3K#0f<2<>!+wjSaW4wcQa|bsT-t>;1dL|{{)UV=ns?Z9 zDBZ8mC4i5|9-42n9_ z@~q81%?IN^`x422s528cYz^siJmv729qj^oErS6peKI@+pT-I4G z5}1~ud1*NDFAU+qzGgYnzD=7nZPN6CrVpG7AHW`u@362B@qJ3f^KCv1@tn_@q zt})pk8~18|&vIHGS8s-NTT7ysdz`?nk~N-J56VtVp;;KI zt%kI_nnM4Uvd*xH=e^#z*b^Kj`HX~DkAGf`SOHH*gd(q#UACD{IV-2kqjFkh^m5pl z*{eY&qPkZ?BMq-#qtP*8%`1d^O2*sb|5@_>+dQcwW_oksX@Gu=#?wC`|4RkxEn=sH z{7agaa0ru!8Lf=Y8&S-+fF2cOX}nf*sVlurekEdwelhdA@_qy4RHzt@A!DZ4yi3Z7 z`?JJ7RpRx{=FIS`~WjHzP}EkZ;@LxW;pr}Yb7l+tmne>H_Uxf z{0W+Pej3ijJ8jJvMNT+vH8f)ui6l$@N9SsISSv2_X3IO^mA-cKoH;6Qe!njMP5({E zxW;eEii}2TsZiO}%;N;>Um^c$D&`1wWE|jsN`&r=YFaknm#eYQaocB${WO-Y&led9 zKbkXg(lTDW1hxlZzou`j-X)Ue3O`H!=cBXAI>Eog{>~SA~=`{&RJr=UFk{L8S0f=Ei^mJwc);G)AxF=435ogS#xjb>#KJLeGvyuWFs{xq zhv{Rt9y90lnVv(&*q!oZE0dKcW@eydnW5Pm9Os_WpB*n#ByBtjLwefS_=Y2Ab=Yso ztJs6&c-%R`D#~4MV}E}B^D~CaO&3<k7h0?)T((eH&gFcJ@>uNTav6crEj=fWXsq6iOPB9-0cn%4%@CA4nv?H--pH368KC`v<6PeZ$-S?NC{u9cU$UC} zuu1JKQI%YQxh?H|=-` zh3zqodElP23Q7E=2~*fzKpDfE?TsY5L?bj~!xa}b!^vT1F??hdvvB|1ajGCx2m7 z^vET$^FG{s^S{1|e@9`5bKvexrqu7u`rpTX@Wl_m^_8=KWU}+KR_n|5_GS0qdh{l< z@?ftS+yX28NDR|2M2WK3RG5a;azpXYCGomhWZIS!%S&&v&$Jq=AcdFu-9kR)3)FSR zFQ1(=-FEDm)O%K+S(5XAP|BZ~kIb;l_9YTzy(BbZ{q%bI9Gd+<*6{5T%1tJ>Zhp^> z!Pu;KE^qyim>xbq_la-&Aq?TGM0ovWUzOK&y!V20Wy&(Idyi!RZ}zT)!5g;)bELcV%{(?8P1CY0Ez9s?3kPuh~F_5%Ae9 zd6ADY?-%TU69IllxoeE;(B^i)3MFhVF6C7w6A@cF?}mr z?i*;8ABg?ZY?7bR|DyFBYsIp)zhlL2#Sg=Vx`&DmLUds6eILk6M8}Y^L}t6Yf%rOn zEbWcTEX=&mb@@lOiMjuNwh%7%oXG#GTCZXMW2JBP&~0+~k&|WPVaV zjoV`1ZzSh-tje8tn{~)4pF#7C-Gn5!XF8tVuu=|6$?V411|f@e#&U)0;v3iA3En-q zt-{*f;gwJ2ANuD5hbsDs;|&PTI^&^kOPJfLJ$E2~_LpKG$aLkV<6tRH+Wp)8{r<_r ze$AN0|4}iS^cTK)lZ%&gKlvf5x~U_U>*&kQY00&2STNWA@%)~9ANVWFOjej4?eak&e{!omf*;RJ)PFFjTPX7>KS zIQ+UlmLafy^MD)R)O^&?$M3STGKVR%S*zyNZ98GUYZbdq3C2&o0k^f2PTH1>N{@N8 zGM_KRVb2vNN3H5H~Iwe0#0-?8vgXk=QQ6_*pY zvU-eV%MGr_$6Vr;@+on3>EO;8Svy@YD-(z1K}n~ORHgPu+)L$IHkEQ?aWQ#=V69Ic zi#>IzEwk00T{<&!$B@1G(An01uSZzKWw<6Ii_T+>@P$azV0xtrKecvm{+vxQv+uhI zLf$K}#$GpQ24nt}uH*~_JgkZV-j(%I(diz_yMFCBPYvdVw>bHNbIst1pUVw)@BS0} zPaG@B{*cttZzAGr$Lz`T@y*&Qo{KdpPS z!bobeC6v=aXX`u9n%J{f6GP%rRLYuy{6k(DlO=+EEow4P9kPo!$NJ)S(r!7jc*%oZ zgGo=w7^X+USa9aQ;2V`%Q+^@?ZOblYzHrx^L`TZKAQ0trFYPU6PbU`Ov`T9>_n27E zORrzI%IY$O$IR=p!2V=^VD5b%=ru*xx3EOy!KWnI zI^lD@Ll$Dn$x60FO6Qn}P9<{*`x`eD7r%17WBHr877CrP!(j6nOKKl-j#QJbsj#`K z!(MKFzqQ2{(2XnFix6bvO4BX@MhxK^1V*W(6%}e-!{SNi%)UQ=h`YC4-KFclN)|m< z8W>R@eEi(Dmk!KnKO(H%bWP4SwjV-o%SyCB$jtV+eGwlywW=dfrV!ZfmN=AhErcnv z{qH(Be&g=0?S;hZO|sDhQq^g*GF?ON*BoAZ#O{;0C7bO`wPoMEpUgUB%Ziz7=OzJc z`cf<>$ND#{w$!43?5v%!-^9vndrj7(AjG<@J+(WxATFExz-}94@}^C;*tuc%YnKBv zlfBX15G24TpW5}w_xgWB7AU({3GPza1|%g`$IT9yp_1OTSBrfFFAroV=^0_zNuOA| z#I=nq-Om~OTfeN@j(iVuAQ2 z_k;T*|JRQvHeu=OMcC6pSMlEGV#!wXM$!yh>s-~;hkHDfU06V$|8E!m52`n+8k9J2 zz#3|M?g=xzxO`5qo!GF1DMwDE&Oyg{va8GN-RL;8(rH=1g|zcvJbjKk%D=sD_7ZF5 zmc#$@_yK=tEbbNc!+tlwsriUfrUH-2)FcvSMNmCC*yZBp=dr=Galbc8$U~NxDQt9- zh2u^>W4>P@i0AAIzp&ajeT(@d?>ook+qnM+l*3IeP)i30;B@ z6aWYa2msK8v)&jw0}arGiCN%ue+`@%005VhB^)gu(1eLuKz&LF{{jF2`T_s|4FCWD Z0000000000007pLaU3895F7vi0039V5Fr2n delta 6463 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 Z94!J|fs*Le0036#0(bxb diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index cb11106..a2adf5f 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -11,8 +11,8 @@ "type": "SINGLE", "filters": [], "attributes": [], - "versionCode": 19, - "versionName": "2.3.2", + "versionCode": 20, + "versionName": "2.3.3", "outputFile": "app-release.apk" } ], diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ea3033c..5248757 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ + + + + + + + + \ No newline at end of file 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 7c6de4a..41b6306 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 @@ -1,8 +1,10 @@ package com.sosauce.cutemusic.data.datastore import android.content.Context +import android.util.Log import androidx.compose.runtime.Composable import androidx.datastore.core.DataStore +import androidx.datastore.core.IOException import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey @@ -11,6 +13,7 @@ import com.sosauce.cutemusic.data.datastore.PreferencesKeys.APPLY_LOOP 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.SAF_TRACKS import com.sosauce.cutemusic.data.datastore.PreferencesKeys.SHOW_ALBUMS_TAB import com.sosauce.cutemusic.data.datastore.PreferencesKeys.SHOW_ARTISTS_TAB import com.sosauce.cutemusic.data.datastore.PreferencesKeys.SHOW_FOLDERS_TAB @@ -21,7 +24,10 @@ import com.sosauce.cutemusic.data.datastore.PreferencesKeys.USE_ART_THEME import com.sosauce.cutemusic.data.datastore.PreferencesKeys.USE_CLASSIC_SLIDER import com.sosauce.cutemusic.data.datastore.PreferencesKeys.USE_DARK_MODE import com.sosauce.cutemusic.data.datastore.PreferencesKeys.USE_SYSTEM_FONT +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map private const val PREFERENCES_NAME = "settings" @@ -43,6 +49,7 @@ private data object PreferencesKeys { val SHOW_ALBUMS_TAB = booleanPreferencesKey("show_albums_tab") val SHOW_ARTISTS_TAB = booleanPreferencesKey("show_artists_tab") val SHOW_FOLDERS_TAB = booleanPreferencesKey("show_folders_tab") + val SAF_TRACKS = stringSetPreferencesKey("saf_tracks") } @Composable @@ -103,9 +110,25 @@ fun rememberShowArtistsTab() = fun rememberShowFoldersTab() = rememberPreference(key = SHOW_FOLDERS_TAB, defaultValue = true) +@Composable +fun rememberAllSafTracks() = + rememberPreference(key = SAF_TRACKS, defaultValue = emptySet()) + suspend fun getBlacklistedFolder(context: Context): Set { val preferences = context.dataStore.data.first() return preferences[BLACKLISTED_FOLDERS] ?: emptySet() } +fun getSafTracks(context: Context): Flow> = + + context.dataStore.data + .catch { exception -> + if (exception is IOException) { + Log.d("CuteError", "getSafTracks: ${exception.message}") + } else throw exception + } + .map { preference -> + preference[SAF_TRACKS] ?: emptySet() + } + 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 4656c8d..f398c81 100644 --- a/app/src/main/java/com/sosauce/cutemusic/di/AppModule.kt +++ b/app/src/main/java/com/sosauce/cutemusic/di/AppModule.kt @@ -2,6 +2,7 @@ package com.sosauce.cutemusic.di import com.sosauce.cutemusic.domain.repository.MediaStoreHelper import com.sosauce.cutemusic.domain.repository.MediaStoreHelperImpl +import com.sosauce.cutemusic.domain.repository.SafManager import com.sosauce.cutemusic.ui.screens.metadata.MetadataViewModel import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.ui.shared_components.PostViewModel @@ -14,11 +15,16 @@ val appModule = module { single { MediaStoreHelperImpl(androidContext()) } + + single { + SafManager(androidContext()) + } + viewModel { - PostViewModel(get()) + PostViewModel(get(), get()) } viewModel { - MusicViewModel(androidApplication(), get()) + MusicViewModel(androidApplication(), get(), get()) } viewModel { MetadataViewModel(androidApplication()) 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 dbf2943..b5ad9d5 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 @@ -7,7 +7,7 @@ import androidx.media3.common.MediaItem import com.sosauce.cutemusic.domain.model.Album import com.sosauce.cutemusic.domain.model.Artist import com.sosauce.cutemusic.domain.model.Folder -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow interface MediaStoreHelper { @@ -17,10 +17,10 @@ interface MediaStoreHelper { val folders: List fun fetchMusics(): List - fun fetchLatestMusics(): Flow> + fun fetchLatestMusics(): StateFlow> fun fetchAlbums(): List - fun fetchLatestAlbums(): Flow> + fun fetchLatestAlbums(): StateFlow> fun fetchArtists(): List diff --git a/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl.kt b/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl.kt index eae9bfb..d634638 100644 --- a/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl.kt +++ b/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl.kt @@ -21,9 +21,13 @@ 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.observe +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.runBlocking @SuppressLint("UnsafeOptInUsageError") @@ -31,26 +35,41 @@ class MediaStoreHelperImpl( private val context: Context ) : MediaStoreHelper { + private fun getBlacklistedFoldersAsync(): Set = runBlocking { getBlacklistedFolder(context) } + private val blacklistedFolders = getBlacklistedFoldersAsync() private val selection = blacklistedFolders.joinToString(" AND ") { "${MediaStore.Audio.Media.DATA} NOT LIKE ?" } private val selectionArgs = blacklistedFolders.map { "$it%" }.toTypedArray() - override fun fetchLatestMusics(): Flow> = + override fun fetchLatestMusics(): StateFlow> = context.contentResolver.observe(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI) - .map { fetchMusics() } + .map { + fetchMusics() + } + .stateIn( + CoroutineScope(Dispatchers.IO), + SharingStarted.WhileSubscribed(5000), + listOf() + ) - override fun fetchLatestAlbums(): Flow> = + override fun fetchLatestAlbums(): StateFlow> = context.contentResolver.observe(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI) .map { fetchAlbums() } + .stateIn( + CoroutineScope(Dispatchers.IO), + SharingStarted.WhileSubscribed(5000), + listOf() + ) @UnstableApi override fun fetchMusics(): List { + val musics = mutableListOf() val projection = arrayOf( @@ -62,7 +81,8 @@ class MediaStoreHelperImpl( MediaStore.Audio.Media.ALBUM_ID, MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.SIZE, - MediaStore.Audio.Media.DURATION + MediaStore.Audio.Media.DURATION, + MediaStore.Audio.Media.TRACK, ) @@ -82,6 +102,7 @@ class MediaStoreHelperImpl( val folderColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA) val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE) val durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION) + val trackNbColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TRACK) //val isFavColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.IS_FAVORITE) while (cursor.moveToNext()) { @@ -95,6 +116,7 @@ class MediaStoreHelperImpl( val folder = filePath.substring(0, filePath.lastIndexOf('/')) val size = cursor.getLong(sizeColumn) val duration = cursor.getLong(durationColumn) + val trackNumber = cursor.getInt(trackNbColumn) //val isFavorite = cursor.getInt(isFavColumn) // 1 = is favorite, 0 = no val uri = ContentUris.withAppendedId( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, @@ -119,6 +141,7 @@ class MediaStoreHelperImpl( .setAlbumTitle(album) .setArtworkUri(artUri) .setDurationMs(duration) + .setTrackNumber(trackNumber) .setExtras( Bundle() .apply { @@ -128,6 +151,7 @@ class MediaStoreHelperImpl( putString("uri", uri.toString()) putLong("album_id", albumId) putLong("artist_id", artistId) + putBoolean("is_saf", false) // putInt("isFavorite", isFavorite) }).build() ) diff --git a/app/src/main/java/com/sosauce/cutemusic/domain/repository/SafManager.kt b/app/src/main/java/com/sosauce/cutemusic/domain/repository/SafManager.kt new file mode 100644 index 0000000..2de943f --- /dev/null +++ b/app/src/main/java/com/sosauce/cutemusic/domain/repository/SafManager.kt @@ -0,0 +1,124 @@ +package com.sosauce.cutemusic.domain.repository + +import android.content.Context +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.os.Bundle +import android.util.Log +import androidx.core.net.toUri +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.media3.common.util.UnstableApi +import com.sosauce.cutemusic.data.datastore.getSafTracks +import com.sosauce.cutemusic.utils.getUriFromByteArray +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext +import java.util.UUID + +class SafManager( + private val context: Context +) { + + + @UnstableApi + fun fetchLatestSafTracks(): StateFlow> = getSafTracks(context) + .map { tracks -> + tracks.map { uri -> + uriToMediaItem(uri.toUri()) + } + } + .stateIn( + CoroutineScope(Dispatchers.IO), + SharingStarted.WhileSubscribed(5000), + listOf() + ) + + + @UnstableApi + private suspend fun uriToMediaItem(uri: Uri): MediaItem = withContext(Dispatchers.IO) { + + val retriever = MediaMetadataRetriever() + + try { + retriever.setDataSource(context, uri) + + val id = uri.hashCode() + val title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE) + val artist = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST) + val album = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM) + val size = + context.contentResolver.openAssetFileDescriptor(uri, "r")?.use { it.length } ?: 0 + val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) + val artUri = retriever.embeddedPicture?.getUriFromByteArray(context) + + return@withContext MediaItem + .Builder() + .setUri(uri) + .setMediaId(id.toString()) + .setMediaMetadata( + MediaMetadata + .Builder() + .setIsBrowsable(false) + .setIsPlayable(true) + .setTitle(title) + .setArtist(artist) + .setAlbumTitle(album) + .setArtworkUri(artUri) + .setDurationMs(duration?.toLong() ?: 0) + .setExtras( + Bundle() + .apply { + putString("folder", "SAF") + putLong("size", size) + putString("path", "${uri.path}") + putString("uri", uri.toString()) + putLong("album_id", 0) + putLong("artist_id", 0) + putBoolean("is_saf", true) + // putInt("isFavorite", isFavorite) + }).build() + ) + .build() + + } catch (e: Exception) { + Log.d("FAILED_SAF", "uriToMediaItem: ${e.stackTrace} ${e.message}") + } finally { + retriever.release() + } + + return@withContext MediaItem + .Builder() + .setUri(uri) + .setMediaId(UUID.randomUUID().toString()) + .setMediaMetadata( + MediaMetadata + .Builder() + .setIsBrowsable(false) + .setIsPlayable(true) + .setTitle("No title") + .setArtist("No artist") + .setAlbumTitle("No album") + .setArtworkUri(Uri.EMPTY) + .setDurationMs(0) + .setExtras( + Bundle() + .apply { + putString("folder", "SAF") + putLong("size", 0) + putString("path", "${uri.path}") + putString("uri", uri.toString()) + putLong("album_id", 0) + putLong("artist_id", 0) + putBoolean("is_saf", true) + // putInt("isFavorite", isFavorite) + }).build() + ) + .build() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/main/AutoPlaybackService.kt b/app/src/main/java/com/sosauce/cutemusic/main/AutoPlaybackService.kt new file mode 100644 index 0000000..1ce2d97 --- /dev/null +++ b/app/src/main/java/com/sosauce/cutemusic/main/AutoPlaybackService.kt @@ -0,0 +1,70 @@ +package com.sosauce.cutemusic.main + +import android.content.Intent +import android.media.MediaDescription +import android.media.browse.MediaBrowser +import android.net.Uri +import android.os.Bundle +import android.service.media.MediaBrowserService +import androidx.media3.common.util.UnstableApi +import com.sosauce.cutemusic.domain.repository.MediaStoreHelperImpl +import com.sosauce.cutemusic.utils.ROOT_ID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@UnstableApi +class AutoPlaybackService : MediaBrowserService() { + + + val mediaStoreHelper by lazy { MediaStoreHelperImpl(this) } + private val job = SupervisorJob() + private val scope = CoroutineScope(Dispatchers.IO + job) + + + override fun onGetRoot( + clientPackageName: String, + clientUid: Int, + rootHints: Bundle? + ): BrowserRoot? = BrowserRoot(ROOT_ID, null) + + override fun onLoadChildren( + parentId: String, + result: Result?> + ) { + + val mediaItems: MutableList = mutableListOf() + + if (ROOT_ID == parentId) { + scope.launch { + mediaStoreHelper.fetchLatestMusics().collectLatest { list -> + list.forEach { mediaItem -> + mediaItems.add( + MediaBrowser.MediaItem( + MediaDescription.Builder() + .setMediaId(mediaItem.mediaId) + .setTitle(mediaItem.mediaMetadata.title ?: "No title") + .setIconUri(mediaItem.mediaMetadata.artworkUri ?: Uri.EMPTY) + .build(), + MediaBrowser.MediaItem.FLAG_PLAYABLE + ) + ) + } + } + result.sendResult(mediaItems) + } + } else result.sendResult(listOf()) + } + + override fun onDestroy() { + super.onDestroy() + job.cancel() + } + + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + job.cancel() + } +} \ No newline at end of file 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 574dd61..1c68172 100644 --- a/app/src/main/java/com/sosauce/cutemusic/main/PlaybackService.kt +++ b/app/src/main/java/com/sosauce/cutemusic/main/PlaybackService.kt @@ -13,6 +13,7 @@ import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaLibraryService.MediaLibrarySession import androidx.media3.session.MediaSession import com.sosauce.cutemusic.R +import com.sosauce.cutemusic.utils.CUTE_MUSIC_ID class PlaybackService : MediaLibraryService(), @@ -45,6 +46,7 @@ class PlaybackService : MediaLibraryService(), .build() mediaLibrarySession = MediaLibrarySession .Builder(this, player, this) + .setId(CUTE_MUSIC_ID) .setShowPlayButtonIfPlaybackIsSuppressed(false) // .setBitmapLoader(object : BitmapLoader { 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 2122a1d..7a74dc8 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 @@ -23,6 +23,7 @@ import com.sosauce.cutemusic.ui.screens.main.MainScreen import com.sosauce.cutemusic.ui.screens.metadata.MetadataEditor import com.sosauce.cutemusic.ui.screens.metadata.MetadataViewModel import com.sosauce.cutemusic.ui.screens.playing.NowPlayingScreen +import com.sosauce.cutemusic.ui.screens.saf.SafScreen import com.sosauce.cutemusic.ui.screens.settings.SettingsScreen import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.ui.shared_components.PostViewModel @@ -222,6 +223,19 @@ fun Nav() { animatedVisibilityScope = this, ) } + + composable { + val latestSafTracks by postViewModel.safTracks.collectAsStateWithLifecycle() + + SafScreen( + onNavigateUp = navController::navigateUp, + latestSafTracks = latestSafTracks, + onNavigate = { navController.navigate(it) }, + onShortClick = { viewModel.handlePlayerActions(PlayerActions.StartPlayback(it)) }, + isPlayerReady = musicState.isPlayerReady, + currentMusicUri = musicState.currentMusicUri, + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/navigation/Screen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/navigation/Screen.kt index a4c083d..597466e 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/navigation/Screen.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/navigation/Screen.kt @@ -25,6 +25,9 @@ sealed class Screen { @Serializable data object AllFolders : Screen() + @Serializable + data object Saf : Screen() + @Serializable data class AlbumsDetails( val id: Long 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 b999915..604e2e7 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 @@ -144,7 +144,11 @@ private fun SharedTransitionScope.AlbumDetailsContent( contentDescription = stringResource(R.string.artwork), modifier = Modifier .size(150.dp) - .clip(RoundedCornerShape(12.dp)), + .sharedElement( + state = rememberSharedContentState(key = album.id), + animatedVisibilityScope = animatedVisibilityScope, + ) + .clip(RoundedCornerShape(24.dp)), contentScale = ContentScale.Crop ) Spacer(Modifier.width(10.dp)) @@ -154,13 +158,23 @@ private fun SharedTransitionScope.AlbumDetailsContent( CuteText( text = album.name, fontSize = 22.sp, - modifier = Modifier.basicMarquee() + modifier = Modifier + .sharedElement( + state = rememberSharedContentState(key = album.name + album.id), + animatedVisibilityScope = animatedVisibilityScope, + ) + .basicMarquee() ) CuteText( text = album.artist, fontSize = 22.sp, color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.85f), - modifier = Modifier.basicMarquee() + modifier = Modifier + .sharedElement( + state = rememberSharedContentState(key = album.artist + album.id), + animatedVisibilityScope = animatedVisibilityScope, + ) + .basicMarquee() ) Spacer(Modifier.height(60.dp)) CuteText( @@ -172,21 +186,26 @@ private fun SharedTransitionScope.AlbumDetailsContent( } Spacer(Modifier.height(10.dp)) Column { - albumSongs.forEach { music -> - MusicListItem( - music = music, - onShortClick = { - viewModel.handlePlayerActions( - PlayerActions.StartAlbumPlayback( - albumName = music.mediaMetadata.albumTitle.toString(), - mediaId = it + albumSongs.sortedWith(compareBy( + { it.mediaMetadata.trackNumber == null || it.mediaMetadata.trackNumber == 0 }, + { it.mediaMetadata.trackNumber } + )) + .forEach { music -> + MusicListItem( + music = music, + onShortClick = { + viewModel.handlePlayerActions( + PlayerActions.StartAlbumPlayback( + albumName = music.mediaMetadata.albumTitle.toString(), + mediaId = it + ) ) - ) - }, - currentMusicUri = musicState.currentMusicUri, - isPlayerReady = musicState.isPlayerReady - ) - } + }, + currentMusicUri = musicState.currentMusicUri, + isPlayerReady = musicState.isPlayerReady, + showTrackNumber = true + ) + } } } } 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 c3a1dd2..e3b8dcb 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 @@ -142,9 +142,11 @@ fun SharedTransitionScope.AlbumsScreen( .thenIf( if (isLandscape) index == 0 || index == 1 || index == 2 || index == 3 - else index == 0 || index == 1, + else index == 0 || index == 1 + ) { Modifier.statusBarsPadding() - ) + }, + animatedVisibilityScope = animatedVisibilityScope ) } } @@ -223,9 +225,10 @@ fun SharedTransitionScope.AlbumsScreen( @Composable -fun AlbumCard( +fun SharedTransitionScope.AlbumCard( album: Album, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + animatedVisibilityScope: AnimatedVisibilityScope, ) { val context = LocalContext.current @@ -241,6 +244,10 @@ fun AlbumCard( contentDescription = stringResource(id = R.string.artwork), modifier = Modifier .size(160.dp) + .sharedElement( + state = rememberSharedContentState(key = album.id), + animatedVisibilityScope = animatedVisibilityScope, + ) .clip(RoundedCornerShape(24.dp)), contentScale = ContentScale.Crop ) @@ -249,12 +256,22 @@ fun AlbumCard( CuteText( text = album.name, maxLines = 1, - modifier = Modifier.basicMarquee() + modifier = Modifier + .sharedElement( + state = rememberSharedContentState(key = album.name + album.id), + animatedVisibilityScope = animatedVisibilityScope, + ) + .basicMarquee() ) CuteText( text = album.artist, color = MaterialTheme.colorScheme.onBackground.copy(0.85f), - modifier = Modifier.basicMarquee() + modifier = Modifier + .sharedElement( + state = rememberSharedContentState(key = album.artist + album.id), + animatedVisibilityScope = animatedVisibilityScope, + ) + .basicMarquee() ) } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/all_folders/AllFoldersScreen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/all_folders/AllFoldersScreen.kt index 5208adb..49054bb 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/all_folders/AllFoldersScreen.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/all_folders/AllFoldersScreen.kt @@ -16,7 +16,6 @@ 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 @@ -44,9 +43,8 @@ import androidx.compose.ui.unit.dp import androidx.media3.common.MediaItem import com.sosauce.cutemusic.R import com.sosauce.cutemusic.data.actions.PlayerActions -import com.sosauce.cutemusic.domain.model.Folder import com.sosauce.cutemusic.ui.navigation.Screen -import com.sosauce.cutemusic.ui.screens.blacklisted.components.FolderItem +import com.sosauce.cutemusic.ui.screens.blacklisted.FolderItem import com.sosauce.cutemusic.ui.screens.main.MusicListItem import com.sosauce.cutemusic.ui.shared_components.CuteSearchbar import com.sosauce.cutemusic.ui.shared_components.CuteText @@ -118,22 +116,22 @@ fun SharedTransitionScope.AllFoldersScreen( ) FolderItem( - folder = Folder( - name = folder?.substring(folder.lastIndexOf('/') + 1) ?: "No Name", - path = folder.toString() - ), - onClick = { areMusicsVisible[folder ?: "No Name"] = !isExpanded }, + folder = folder ?: " No name", topDp = topDp, bottomDp = bottomDp, - icon = { - Icon( - imageVector = Icons.Rounded.ArrowBackIosNew, - contentDescription = null, - modifier = Modifier - .size(30.dp) - .rotate(rotation), - tint = MaterialTheme.colorScheme.onBackground - ) + modifier = Modifier.animateItem(), + actionButton = { + IconButton( + onClick = { + areMusicsVisible[folder ?: "No name"] = !isExpanded + } + ) { + Icon( + imageVector = Icons.Rounded.ArrowBackIosNew, + contentDescription = null, + modifier = Modifier.rotate(rotation) + ) + } } ) } 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 01893ad..f8e0702 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 @@ -21,7 +21,7 @@ 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 @@ -79,7 +79,8 @@ fun SharedTransitionScope.ArtistDetails( chargePVMAlbumSongs = { postViewModel.albumSongs(it) }, artist = artist, currentMusicUri = musicState.currentMusicUri, - isPlayerReady = musicState.isPlayerReady + isPlayerReady = musicState.isPlayerReady, + animatedVisibilityScope = animatedVisibilityScope ) } else { Scaffold( @@ -96,7 +97,6 @@ fun SharedTransitionScope.ArtistDetails( ) CuteText( text = "${artistSongs.size} ${if (artistSongs.size <= 1) "song" else "songs"}", - fontSize = 20.sp ) } @@ -106,7 +106,7 @@ fun SharedTransitionScope.ArtistDetails( onClick = navController::navigateUp ) { Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back arrow" ) } @@ -137,7 +137,8 @@ fun SharedTransitionScope.ArtistDetails( .clickable { postViewModel.albumSongs(album.name) onNavigate(Screen.AlbumsDetails(album.id)) - } + }, + animatedVisibilityScope = animatedVisibilityScope ) } } 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 2e83c40..1e8e05c 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 @@ -1,5 +1,10 @@ +@file:OptIn(ExperimentalSharedTransitionApi::class) + package com.sosauce.cutemusic.ui.screens.artist +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -32,7 +37,7 @@ import com.sosauce.cutemusic.ui.screens.main.MusicListItem import com.sosauce.cutemusic.ui.shared_components.CuteText @Composable -fun ArtistDetailsLandscape( +fun SharedTransitionScope.ArtistDetailsLandscape( onNavigateUp: () -> Unit, artistAlbums: List, artistSongs: List, @@ -41,7 +46,8 @@ fun ArtistDetailsLandscape( chargePVMAlbumSongs: (String) -> Unit, artist: Artist, currentMusicUri: String, - isPlayerReady: Boolean + isPlayerReady: Boolean, + animatedVisibilityScope: AnimatedVisibilityScope, ) { Column( modifier = Modifier @@ -87,7 +93,8 @@ fun ArtistDetailsLandscape( chargePVMAlbumSongs(album.name) onNavigate(Screen.AlbumsDetails(album.id)) } - .size(230.dp) + .size(230.dp), + animatedVisibilityScope = animatedVisibilityScope ) } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/blacklisted/BlacklistedScreen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/blacklisted/BlacklistedScreen.kt index e5d2e55..6d50340 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/blacklisted/BlacklistedScreen.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/blacklisted/BlacklistedScreen.kt @@ -2,15 +2,12 @@ package com.sosauce.cutemusic.ui.screens.blacklisted -import android.widget.Toast import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -19,27 +16,21 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.FolderOpen +import androidx.compose.material.icons.rounded.Add import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -48,7 +39,6 @@ import androidx.navigation.NavController import com.sosauce.cutemusic.R import com.sosauce.cutemusic.data.datastore.rememberAllBlacklistedFolders import com.sosauce.cutemusic.domain.model.Folder -import com.sosauce.cutemusic.ui.screens.blacklisted.components.AllFoldersBottomSheet import com.sosauce.cutemusic.ui.shared_components.AppBar import com.sosauce.cutemusic.ui.shared_components.CuteText import java.io.File @@ -58,51 +48,6 @@ fun BlacklistedScreen( navController: NavController, folders: List, ) { - var isSheetOpen by remember { mutableStateOf(false) } - val context = LocalContext.current - var blacklistedFolders by rememberAllBlacklistedFolders() - - if (isSheetOpen) { - ModalBottomSheet( - onDismissRequest = { isSheetOpen = false }, - modifier = Modifier.fillMaxHeight() - ) { - AllFoldersBottomSheet( - folders = folders, - onClick = { path -> - if (path in blacklistedFolders) { - Toast.makeText( - context, - context.resources.getText(R.string.alrdy_blacklisted), - Toast.LENGTH_SHORT - ).show() - } else { - blacklistedFolders = blacklistedFolders.toMutableSet().apply { - add(path) - } - isSheetOpen = false - Toast.makeText( - context, - context.resources.getText(R.string.pls_restart), - Toast.LENGTH_SHORT - ).show() - } - } - ) - } - } - - BlacklistedScreenContent( - onAddFolder = { isSheetOpen = true }, - onPopBackStack = navController::navigateUp - ) -} - -@Composable -private fun BlacklistedScreenContent( - onAddFolder: () -> Unit, - onPopBackStack: () -> Unit, -) { var blacklistedFolders by rememberAllBlacklistedFolders() @@ -111,18 +56,8 @@ private fun BlacklistedScreenContent( AppBar( title = stringResource(id = R.string.blacklisted_folders), showBackArrow = true, - onPopBackStack = { onPopBackStack() } + onPopBackStack = navController::navigateUp ) - }, - floatingActionButton = { - FloatingActionButton( - onClick = { onAddFolder() } - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null - ) - } } ) { values -> LazyColumn( @@ -130,54 +65,97 @@ private fun BlacklistedScreenContent( .fillMaxSize() .padding(values) ) { - itemsIndexed( - items = blacklistedFolders.toList(), - key = { _, folder -> folder } - ) { index, folder -> + folders.sortedBy { it.name } + .groupBy { it.path in blacklistedFolders } + .toSortedMap(compareByDescending { it }) + .forEach { (isBlacklisted, allFolders) -> + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 34.dp, + vertical = 8.dp + ) + ) { + CuteText( + text = if (isBlacklisted) stringResource(R.string.blacklisted) else stringResource( + R.string.not_blacklisted + ), + color = MaterialTheme.colorScheme.primary + ) + } + } - val topDp by animateDpAsState( - targetValue = if (index == 0) 24.dp else 4.dp, - label = "Top Dp", - animationSpec = tween(500) - ) - val bottomDp by animateDpAsState( - targetValue = if (index == blacklistedFolders.size - 1) 24.dp else 4.dp, - label = "Bottom Dp", - animationSpec = tween(500) - ) + itemsIndexed( + items = allFolders, + key = { _, folder -> folder.path } + ) { index, folder -> + val topDp by animateDpAsState( + targetValue = if (index == 0) 24.dp else 4.dp, + label = "Top Dp" + ) + val bottomDp by animateDpAsState( + targetValue = if (index == allFolders.size - 1) 24.dp else 4.dp, + label = "Bottom Dp" + ) - BlackFolderItem( - folder = folder, - onClick = { - blacklistedFolders = blacklistedFolders.toMutableSet().apply { - remove(folder) - } - }, - topDp = topDp, - bottomDp = bottomDp, - modifier = Modifier.animateItem() - ) - } + FolderItem( + folder = folder.path, + topDp = topDp, + bottomDp = bottomDp, + modifier = Modifier.animateItem(), + actionButton = { + if (isBlacklisted) { + IconButton( + onClick = { + blacklistedFolders = + blacklistedFolders.toMutableSet().apply { + remove(folder.path) + } + } + ) { + Icon( + painter = painterResource(R.drawable.trash_rounded_filled), + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) + } + } else { + IconButton( + onClick = { + blacklistedFolders = + blacklistedFolders.toMutableSet().apply { + add(folder.path) + } + } + ) { + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = null + ) + } + } + } + ) + } + } } } } @Composable -private fun BlackFolderItem( +fun FolderItem( modifier: Modifier = Modifier, folder: String, - onClick: () -> Unit, topDp: Dp, bottomDp: Dp, + actionButton: @Composable () -> Unit ) { Card( modifier = modifier - .padding( - start = 13.dp, - end = 13.dp, - bottom = 8.dp - ), + .padding(horizontal = 16.dp, vertical = 2.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceContainer ), @@ -196,7 +174,7 @@ private fun BlackFolderItem( horizontalArrangement = Arrangement.SpaceBetween ) { Image( - imageVector = Icons.Default.FolderOpen, + painter = painterResource(R.drawable.folder_rounded), contentDescription = null, modifier = Modifier.size(33.dp), colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground) @@ -208,7 +186,7 @@ private fun BlackFolderItem( horizontalAlignment = Alignment.Start ) { CuteText( - text = getFileName(folder), + text = File(folder).name, fontSize = 18.sp ) CuteText( @@ -218,20 +196,7 @@ private fun BlackFolderItem( modifier = Modifier.basicMarquee() ) } - IconButton( - onClick = onClick - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = null, - tint = MaterialTheme.colorScheme.error - ) - } + actionButton() } } -} - -private fun getFileName(filePath: String): String { - val file = File(filePath) - return file.name } \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/blacklisted/components/AllFolderBottomSheet.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/blacklisted/components/AllFolderBottomSheet.kt deleted file mode 100644 index 8b4704d..0000000 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/blacklisted/components/AllFolderBottomSheet.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.sosauce.cutemusic.ui.screens.blacklisted.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -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.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Folder -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.sosauce.cutemusic.domain.model.Folder -import com.sosauce.cutemusic.ui.shared_components.CuteText - -@Composable -fun AllFoldersBottomSheet( - folders: List, - onClick: (path: String) -> Unit, -) { - - LazyColumn { - itemsIndexed( - items = folders, - key = { _, folder -> folder.path } - ) { index, folder -> - FolderItem( - folder = folder, - onClick = { path -> - onClick(path) - }, - topDp = if (index == 0) 24.dp else 4.dp, - bottomDp = if (index == folders.size - 1) 24.dp else 4.dp, - icon = { - Icon( - imageVector = Icons.Rounded.Folder, - contentDescription = null, - modifier = Modifier.size(33.dp), - tint = MaterialTheme.colorScheme.onBackground - ) - } - ) - } - } -} - - -@Composable -fun FolderItem( - folder: Folder, - onClick: (path: String) -> Unit, - topDp: Dp, - bottomDp: Dp, - icon: @Composable () -> Unit -) { - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHighest.copy( - alpha = 0.5f - ) - ), - modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp), - shape = RoundedCornerShape( - topStart = topDp, - topEnd = topDp, - bottomStart = bottomDp, - bottomEnd = bottomDp - ), - onClick = { onClick(folder.path) } - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(10.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - icon() - Column( - verticalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxWidth() - .padding(start = 10.dp) - ) { - CuteText( - text = folder.name - ) - CuteText( - text = folder.path, - color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f) - ) - } - } - } -} \ 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 2bf51b1..49d250d 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 @@ -7,6 +7,7 @@ package com.sosauce.cutemusic.ui.screens.main import android.app.Activity import android.content.Intent +import android.graphics.Paint import android.net.Uri import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult @@ -65,7 +66,11 @@ 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.draw.drawWithContent import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -190,10 +195,9 @@ fun SharedTransitionScope.MainScreen( onChargeAlbumSongs = onChargeAlbumSongs, onChargeArtistLists = onChargeArtistLists, modifier = Modifier - .thenIf( - index == 0, + .thenIf(index == 0) { Modifier.statusBarsPadding() - ), + }, isPlayerReady = isPlayerReady ) } @@ -317,7 +321,9 @@ fun MusicListItem( onDeleteMusic: (List, ActivityResultLauncher) -> Unit = { _, _ -> }, onChargeAlbumSongs: (String) -> Unit = {}, onChargeArtistLists: (String) -> Unit = {}, - isPlayerReady: Boolean + isPlayerReady: Boolean, + onDeleteSafTrack: () -> Unit = {}, + showTrackNumber: Boolean = false ) { val context = LocalContext.current @@ -336,6 +342,8 @@ fun MusicListItem( label = "Background Color", animationSpec = tween(500) ) + val materialSurfaceContainer = MaterialTheme.colorScheme.surfaceContainer + val materialOnSurface = MaterialTheme.colorScheme.onSurface val deleteSongLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { if (it.resultCode == Activity.RESULT_OK) { @@ -397,7 +405,51 @@ fun MusicListItem( stringResource(R.string.artwork), modifier = Modifier .padding(start = 10.dp) - .size(45.dp), + .size(45.dp) + .drawWithContent { + drawContent() + if (showTrackNumber && music.mediaMetadata.trackNumber != null && music.mediaMetadata.trackNumber != 0) { + val circleCenter = Offset(size.width, size.height / 12) + drawCircle( + color = materialSurfaceContainer, + center = circleCenter, + radius = 25f + ) + val text = Paint().apply { + color = materialOnSurface.toArgb() + textSize = 30f + textAlign = Paint.Align.CENTER + } + drawContext.canvas.nativeCanvas.drawText( + music.mediaMetadata.trackNumber.toString(), + circleCenter.x, + circleCenter.y - (text.ascent() + text.descent()) / 2, + text + ) + } + }, +// .thenIf(showTrackNumber && music.mediaMetadata.trackNumber != null && music.mediaMetadata.trackNumber != 0) { +// Modifier.drawWithContent { +// val circleCenter = Offset(size.width, size.height / 12) +// drawContent() +// drawCircle( +// color = materialSurfaceContainer, +// center = circleCenter, +// radius = 25f +// ) +// val text = Paint().apply { +// color = materialOnSurface.toArgb() +// textSize = 30f +// textAlign = Paint.Align.CENTER +// } +// drawContext.canvas.nativeCanvas.drawText( +// music.mediaMetadata.trackNumber.toString(), +// circleCenter.x, +// circleCenter.y - (text.ascent() + text.descent()) / 2, +// text +// ) +// } +// }, contentScale = ContentScale.Crop, ) @@ -445,62 +497,64 @@ fun MusicListItem( ) } ) - DropdownMenuItem( - onClick = { - isDropDownExpanded = false - onLoadMetadata(path ?: "", uri) - onNavigate(Screen.MetadataEditor(music.mediaId)) - }, - text = { - CuteText(stringResource(R.string.edit)) - }, - leadingIcon = { - Icon( - painter = painterResource(R.drawable.edit_rounded), - contentDescription = null - ) - } - ) - DropdownMenuItem( - onClick = { - isDropDownExpanded = false - onChargeAlbumSongs(music.mediaMetadata.albumTitle.toString()) - onNavigate( - Screen.AlbumsDetails( - music.mediaMetadata.extras?.getLong("album_id") ?: 0 + if (music.mediaMetadata.extras?.getBoolean("is_saf") == false) { + DropdownMenuItem( + onClick = { + isDropDownExpanded = false + onLoadMetadata(path ?: "", uri) + onNavigate(Screen.MetadataEditor(music.mediaId)) + }, + text = { + CuteText(stringResource(R.string.edit)) + }, + leadingIcon = { + Icon( + painter = painterResource(R.drawable.edit_rounded), + contentDescription = null ) - ) - }, - text = { - CuteText(stringResource(R.string.go_to) + music.mediaMetadata.albumTitle) - }, - leadingIcon = { - Icon( - painter = painterResource(androidx.media3.session.R.drawable.media3_icon_album), - contentDescription = null - ) - } - ) - DropdownMenuItem( - onClick = { - isDropDownExpanded = false - onChargeArtistLists(music.mediaMetadata.artist.toString()) - onNavigate( - Screen.ArtistsDetails( - music.mediaMetadata.extras?.getLong("artist_id") ?: 0 + } + ) + DropdownMenuItem( + onClick = { + isDropDownExpanded = false + onChargeAlbumSongs(music.mediaMetadata.albumTitle.toString()) + onNavigate( + Screen.AlbumsDetails( + music.mediaMetadata.extras?.getLong("album_id") ?: 0 + ) ) - ) - }, - text = { - CuteText(stringResource(R.string.go_to) + music.mediaMetadata.artist) - }, - leadingIcon = { - Icon( - painter = painterResource(R.drawable.artist_rounded), - contentDescription = null - ) - } - ) + }, + text = { + CuteText("${stringResource(R.string.go_to)} ${music.mediaMetadata.albumTitle}") + }, + leadingIcon = { + Icon( + painter = painterResource(androidx.media3.session.R.drawable.media3_icon_album), + contentDescription = null + ) + } + ) + DropdownMenuItem( + onClick = { + isDropDownExpanded = false + onChargeArtistLists(music.mediaMetadata.artist.toString()) + onNavigate( + Screen.ArtistsDetails( + music.mediaMetadata.extras?.getLong("artist_id") ?: 0 + ) + ) + }, + text = { + CuteText("${stringResource(R.string.go_to)} ${music.mediaMetadata.artist}") + }, + leadingIcon = { + Icon( + painter = painterResource(R.drawable.artist_rounded), + contentDescription = null + ) + } + ) + } DropdownMenuItem( onClick = { val shareIntent = Intent().apply { @@ -530,7 +584,13 @@ fun MusicListItem( } ) DropdownMenuItem( - onClick = { onDeleteMusic(listOf(uri), deleteSongLauncher) }, + onClick = { + if (music.mediaMetadata.extras?.getBoolean("is_saf") == false) { + onDeleteMusic(listOf(uri), deleteSongLauncher) + } else { + onDeleteSafTrack() + } + }, text = { CuteText( text = stringResource(R.string.delete), 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 dca8e60..367d81d 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 @@ -59,30 +59,15 @@ fun MetadataEditor( val metadataState by metadataViewModel.metadataState.collectAsStateWithLifecycle() - MetadataEditorContent( - music = music, - onPopBackStack = onPopBackStack, - onNavigate = onNavigate, - metadataState = metadataState, - onMetadataAction = { metadataViewModel.onHandleMetadataActions(it) }, - onEditMusic = onEditMusic - ) -} - -@Composable -fun MetadataEditorContent( - music: MediaItem, - onPopBackStack: () -> Unit, - onNavigate: (Screen) -> Unit, - metadataState: MetadataState, - onMetadataAction: (MetadataActions) -> Unit, - onEditMusic: (List, ActivityResultLauncher) -> Unit -) { val context = LocalContext.current val uri = Uri.parse(music.mediaMetadata.extras?.getString("uri")) val photoPickerLauncher = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { - onMetadataAction(MetadataActions.UpdateAudioArt(it ?: Uri.EMPTY)) + metadataViewModel.onHandleMetadataActions( + MetadataActions.UpdateAudioArt( + it ?: Uri.EMPTY + ) + ) } val editSongLauncher = @@ -90,7 +75,7 @@ fun MetadataEditorContent( contract = ActivityResultContracts.StartIntentSenderForResult() ) { if (it.resultCode == Activity.RESULT_OK) { - onMetadataAction(MetadataActions.SaveChanges) + metadataViewModel.onHandleMetadataActions(MetadataActions.SaveChanges) Toast.makeText( context, context.getString(R.string.success), @@ -273,6 +258,7 @@ fun MetadataEditorContent( } } + @Composable private fun EditTextField( modifier: Modifier = Modifier, 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 36d12a1..2753e06 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 @@ -178,7 +178,7 @@ fun SharedTransitionScope.NowPlayingLandscape( onChargeAlbumSongs = onChargeAlbumSongs, onShowSpeedCard = { showSpeedCard = true }, onChargeArtistLists = onChargeArtistLists, - onHandlePlayerActions = onEvent + onHandlePlayerActions = onEvent, ) } } 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 439ea99..4b14079 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 @@ -6,7 +6,6 @@ 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.tween import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -153,7 +152,7 @@ private fun SharedTransitionScope.NowPlayingContent( horizontalArrangement = Arrangement.Start ) { IconButton( - onClick = onNavigateUp + onClick = onNavigateUp, ) { Icon( imageVector = Icons.Rounded.KeyboardArrowDown, @@ -193,14 +192,12 @@ private fun SharedTransitionScope.NowPlayingContent( color = MaterialTheme.colorScheme.onBackground, fontSize = 20.sp, modifier = Modifier - .sharedElement( - state = rememberSharedContentState(key = "currentlyPlaying"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } - ) .basicMarquee() + .sharedBounds( + sharedContentState = rememberSharedContentState(key = "currentlyPlaying"), + animatedVisibilityScope = animatedVisibilityScope + ) + ) //Spacer(modifier = Modifier.height(5.dp)) CuteText( 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 015fb3f..9888703 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 @@ -219,10 +219,7 @@ fun SharedTransitionScope.ActionsButtonsRow( } .sharedElement( state = rememberSharedContentState(key = "skipPreviousButton"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } + animatedVisibilityScope = animatedVisibilityScope ) ) } else { @@ -269,10 +266,7 @@ fun SharedTransitionScope.ActionsButtonsRow( contentDescription = "pause/play button", modifier = Modifier.sharedElement( state = rememberSharedContentState(key = "playPauseIcon"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } + animatedVisibilityScope = animatedVisibilityScope ) ) } @@ -349,10 +343,7 @@ fun SharedTransitionScope.ActionsButtonsRow( } .sharedElement( state = rememberSharedContentState(key = "skipNextButton"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } + animatedVisibilityScope = animatedVisibilityScope ) ) } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/QuickActionsRow.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/QuickActionsRow.kt index 383d952..260db19 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/QuickActionsRow.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/QuickActionsRow.kt @@ -1,7 +1,13 @@ package com.sosauce.cutemusic.ui.screens.playing.components +import android.content.Context import android.content.Intent +import android.media.audiofx.AudioEffect import android.net.Uri +import android.util.Log +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.result.launch import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -12,11 +18,13 @@ import androidx.compose.foundation.layout.size 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.automirrored.rounded.OpenInNew import androidx.compose.material.icons.rounded.ErrorOutline import androidx.compose.material.icons.rounded.MoreVert import androidx.compose.material.icons.rounded.Speed import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -39,6 +47,7 @@ import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.ui.navigation.Screen import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.MusicStateDetailsDialog +import com.sosauce.cutemusic.utils.CUTE_MUSIC_ID @Composable fun QuickActionsRow( @@ -48,7 +57,7 @@ fun QuickActionsRow( onChargeAlbumSongs: (String) -> Unit, onNavigate: (Screen) -> Unit, onChargeArtistLists: (String) -> Unit, - onHandlePlayerActions: (PlayerActions) -> Unit + onHandlePlayerActions: (PlayerActions) -> Unit, ) { val context = LocalContext.current var isDropDownExpanded by remember { mutableStateOf(false) } @@ -56,6 +65,7 @@ fun QuickActionsRow( val uri = remember { Uri.parse(musicState.currentMusicUri) } var showTimePicker by remember { mutableStateOf(false) } val onBackground = MaterialTheme.colorScheme.onBackground + val eqIntent = rememberLauncherForActivityResult(equalizerActivityContract()) { } if (showDetailsDialog) { MusicStateDetailsDialog( @@ -131,6 +141,32 @@ fun QuickActionsRow( onDismissRequest = { isDropDownExpanded = false }, shape = RoundedCornerShape(24.dp) ) { + DropdownMenuItem( + onClick = { + try { + eqIntent.launch() + } catch (e: Exception) { + Log.d( + "CuteError", + "Couldn't open EQ: ${e.stackTrace}, ${e.message}" + ) + } + }, + text = { + CuteText(stringResource(R.string.open_eq)) + }, + leadingIcon = { + Icon( + imageVector = Icons.AutoMirrored.Rounded.OpenInNew, + contentDescription = null + ) + } + ) + HorizontalDivider( + modifier = Modifier + .fillMaxWidth(0.9f) + .align(Alignment.CenterHorizontally) + ) DropdownMenuItem( onClick = { showDetailsDialog = true }, text = { @@ -151,7 +187,7 @@ fun QuickActionsRow( onNavigate(Screen.AlbumsDetails(musicState.currentAlbumId)) }, text = { - CuteText(stringResource(R.string.go_to) + musicState.currentAlbum) + CuteText("${stringResource(R.string.go_to)} ${musicState.currentAlbum}") }, leadingIcon = { Icon( @@ -167,7 +203,7 @@ fun QuickActionsRow( onNavigate(Screen.ArtistsDetails(musicState.currentArtistId)) }, text = { - CuteText(stringResource(R.string.go_to) + musicState.currentArtist) + CuteText("${stringResource(R.string.go_to)} ${musicState.currentArtist}") }, leadingIcon = { Icon( @@ -205,4 +241,21 @@ fun QuickActionsRow( } } } +} + +private fun equalizerActivityContract() = object : ActivityResultContract() { + override fun createIntent( + context: Context, + input: Unit, + ) = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply { + putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName) + putExtra(AudioEffect.EXTRA_AUDIO_SESSION, CUTE_MUSIC_ID) + putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC) + } + + override fun parseResult( + resultCode: Int, + intent: Intent?, + ) { + } } \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/saf/SafScreen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/saf/SafScreen.kt new file mode 100644 index 0000000..27f4f37 --- /dev/null +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/saf/SafScreen.kt @@ -0,0 +1,152 @@ +package com.sosauce.cutemusic.ui.screens.saf + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.result.launch +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.OpenInNew +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.media3.common.MediaItem +import com.sosauce.cutemusic.R +import com.sosauce.cutemusic.data.datastore.rememberAllSafTracks +import com.sosauce.cutemusic.ui.navigation.Screen +import com.sosauce.cutemusic.ui.screens.main.MusicListItem +import com.sosauce.cutemusic.ui.shared_components.AppBar +import com.sosauce.cutemusic.ui.shared_components.CuteText + +@Composable +fun SafScreen( + onNavigateUp: () -> Unit, + latestSafTracks: List, + onNavigate: (Screen) -> Unit, + onShortClick: (String) -> Unit, + isPlayerReady: Boolean, + currentMusicUri: String, +) { + + val context = LocalContext.current + var safTracks by rememberAllSafTracks() + + val safAudioPicker = rememberLauncherForActivityResult(safActivityContract()) { + safTracks = safTracks.toMutableSet().apply { + add(it.toString()) + } + + context.contentResolver.takePersistableUriPermission( + it ?: Uri.EMPTY, + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + } + + Scaffold( + topBar = { + AppBar( + title = stringResource(R.string.saf_manager), + showBackArrow = true, + onPopBackStack = onNavigateUp + ) + } + ) { values -> + + Column( + modifier = Modifier + .fillMaxSize() + .padding(values) + ) { + Card( + colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer), + modifier = Modifier.align(Alignment.CenterHorizontally), + onClick = { + safAudioPicker.launch() + } + ) { + Row( + modifier = Modifier.padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.OpenInNew, + contentDescription = null + ) + Spacer(Modifier.width(10.dp)) + CuteText(stringResource(R.string.open_saf)) + } + } + Spacer(Modifier.height(10.dp)) + LazyColumn { + items( + items = latestSafTracks.toList(), + key = { it.mediaId } + ) { safTrack -> + + Column( + modifier = Modifier + .animateItem() + .padding( + vertical = 2.dp, + horizontal = 4.dp + ) + ) { + MusicListItem( + onShortClick = { onShortClick(safTrack.mediaId) }, + music = safTrack, + onNavigate = { onNavigate(it) }, + currentMusicUri = currentMusicUri, + showBottomSheet = true, + isPlayerReady = isPlayerReady, + onDeleteSafTrack = { + safTracks = safTracks.toMutableSet().apply { + remove(safTrack.mediaMetadata.extras?.getString("uri")) + } + } + ) + } + + } + } + } + + + } + +} + +private fun safActivityContract() = object : ActivityResultContract() { + override fun createIntent( + context: Context, + input: Unit, + ) = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "audio/*" + } + + override fun parseResult(resultCode: Int, intent: Intent?): Uri? { + return intent?.data + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/SettingsCards.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/SettingsCards.kt index e871579..8c77399 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/SettingsCards.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/SettingsCards.kt @@ -27,15 +27,15 @@ import androidx.compose.ui.unit.sp import com.sosauce.cutemusic.ui.shared_components.CuteText @Composable -inline fun SettingsCards( +fun SettingsCards( hasInfoDialog: Boolean = false, checked: Boolean, topDp: Dp, bottomDp: Dp, text: String, - crossinline onCheckedChange: () -> Unit, - crossinline onClick: () -> Unit = {}, - crossinline optionalDescription: @Composable () -> Unit = {} + onCheckedChange: () -> Unit, + onClick: () -> Unit = {}, + optionalDescription: @Composable () -> Unit = {} ) { Card( colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer), 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 0edf924..8e95dde 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 @@ -16,6 +16,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.sosauce.cutemusic.R @@ -36,6 +37,7 @@ fun Misc( onNavigate: (Screen) -> Unit ) { //var killService by remember { rememberKillService(context) } + val context = LocalContext.current Column { CuteText( @@ -56,6 +58,19 @@ fun Misc( topDp = 24.dp, bottomDp = 24.dp ) +// TextSettingsCards( +// text = stringResource(id = R.string.saf_manager), +// onClick = { onNavigate(Screen.Saf) }, +// modifier = Modifier +// .padding( +// top = 25.dp, +// start = 15.dp, +// bottom = 25.dp +// ) +// .fillMaxWidth(), +// topDp = 4.dp, +// bottomDp = 24.dp +// ) // SettingsCards( // checked = killService, // onCheckedChange = { killService = !killService }, @@ -87,7 +102,8 @@ fun ThemeManagement() { text = stringResource(id = R.string.follow_sys) ) AnimatedContent( - targetState = !followSys, label = "", + targetState = !followSys, + label = "", transitionSpec = { (slideInHorizontally() + fadeIn()).togetherWith(slideOutHorizontally() + fadeOut()) } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/MusicDetailsDialog.kt b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/MusicDetailsDialog.kt index 678c6d3..00e0073 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/MusicDetailsDialog.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/MusicDetailsDialog.kt @@ -117,6 +117,20 @@ fun MusicDetailsDialog( text = "${stringResource(id = R.string.duration)}: ${music.mediaMetadata.durationMs?.formatToReadableTime() ?: 0}", modifier = Modifier.padding(bottom = 5.dp) ) + if (music.mediaMetadata.extras?.getBoolean("is_saf") == true) { + Spacer(Modifier.height(5.dp)) + Card( + colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainerHighest), + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Row( + modifier = Modifier.padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + CuteText(stringResource(R.string.from_saf)) + } + } + } } } ) 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 39caa7b..7b2235a 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 @@ -29,6 +29,7 @@ import com.sosauce.cutemusic.data.MusicState import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.domain.model.Lyrics import com.sosauce.cutemusic.domain.repository.MediaStoreHelper +import com.sosauce.cutemusic.domain.repository.SafManager import com.sosauce.cutemusic.main.PlaybackService import com.sosauce.cutemusic.utils.applyLoop import com.sosauce.cutemusic.utils.applyPlaybackSpeed @@ -37,16 +38,24 @@ import com.sosauce.cutemusic.utils.playAtIndex import com.sosauce.cutemusic.utils.playFromAlbum import com.sosauce.cutemusic.utils.playFromArtist import com.sosauce.cutemusic.utils.playRandom +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.io.File import java.io.FileNotFoundException +@OptIn(FlowPreview::class) +@SuppressLint("UnsafeOptInUsageError") class MusicViewModel( private val application: Application, - private val mediaStoreHelper: MediaStoreHelper + private val mediaStoreHelper: MediaStoreHelper, + private val safManager: SafManager ) : AndroidViewModel(application) { private var mediaController: MediaController? by mutableStateOf(null) @@ -58,7 +67,8 @@ class MusicViewModel( var sleepCountdownTimer: CountDownTimer? = null - private val playerListener = object : Player.Listener { + private val playerListener = @UnstableApi + object : Player.Listener { @UnstableApi override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) { super.onMediaMetadataChanged(mediaMetadata) @@ -67,8 +77,8 @@ class MusicViewModel( currentArtist = mediaMetadata.artist.toString(), currentArtistId = mediaMetadata.extras?.getLong("artist_id") ?: 0, currentArt = mediaMetadata.artworkUri, - currentPath = mediaMetadata.extras?.getString("path") ?: "No Path Found!", - currentMusicUri = mediaMetadata.extras?.getString("uri") ?: "No Uri Found!", + currentPath = mediaMetadata.extras?.getString("path") ?: "No path found!", + currentMusicUri = mediaMetadata.extras?.getString("uri") ?: "No uri found!", currentLrcFile = getLrcFile(), currentAlbum = mediaMetadata.albumTitle.toString(), currentAlbumId = mediaMetadata.extras?.getLong("album_id") ?: 0, @@ -81,46 +91,58 @@ class MusicViewModel( override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { super.onPlaybackParametersChanged(playbackParameters) - _musicState.value = _musicState.value.copy( - playbackParameters = playbackParameters - ) + _musicState.update { + it.copy( + playbackParameters = playbackParameters + ) + } } override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) - _musicState.value = _musicState.value.copy( - isCurrentlyPlaying = isPlaying - ) + _musicState.update { + it.copy( + isCurrentlyPlaying = isPlaying + ) + } } override fun onRepeatModeChanged(repeatMode: Int) { super.onRepeatModeChanged(repeatMode) when (repeatMode) { Player.REPEAT_MODE_ONE -> { - _musicState.value = _musicState.value.copy( - isLooping = true - ) + _musicState.update { + it.copy( + isLooping = true + ) + } } Player.REPEAT_MODE_OFF -> { - _musicState.value = _musicState.value.copy( - isLooping = false - ) + _musicState.update { + it.copy( + isLooping = false + ) + } } else -> { - _musicState.value = _musicState.value.copy( - isLooping = false - ) + _musicState.update { + it.copy( + isLooping = false + ) + } } } } override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { super.onShuffleModeEnabledChanged(shuffleModeEnabled) - _musicState.value = _musicState.value.copy( - isShuffling = shuffleModeEnabled - ) + _musicState.update { + it.copy( + isShuffling = shuffleModeEnabled + ) + } } @@ -128,10 +150,12 @@ class MusicViewModel( super.onEvents(player, events) viewModelScope.launch { while (player.isPlaying) { - _musicState.value = _musicState.value.copy( - currentMusicDuration = player.duration, - currentPosition = player.currentPosition - ) + _musicState.update { + it.copy( + currentMusicDuration = player.duration, + currentPosition = player.currentPosition + ) + } delay(500) } } @@ -141,27 +165,55 @@ class MusicViewModel( super.onPlaybackStateChanged(playbackState) when (playbackState) { Player.STATE_IDLE -> { - _musicState.value = _musicState.value.copy( - isPlayerReady = false - ) + _musicState.update { + it.copy( + isPlayerReady = false + ) + } } Player.STATE_READY -> { - _musicState.value = _musicState.value.copy( - isPlayerReady = true - ) + _musicState.update { + it.copy( + isPlayerReady = true + ) + } } else -> { - _musicState.value = _musicState.value.copy( - isPlayerReady = true - ) + _musicState.update { + it.copy( + isPlayerReady = true + ) + } } } } } init { +// if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_CAR) { +// MediaController +// .Builder( +// application, +// SessionToken( +// application, +// ComponentName(application, AutoPlaybackService::class.java) +// ) +// ) +// .buildAsync() +// .apply { +// addListener( +// { +// mediaController = get() +// mediaController!!.addListener(playerListener) +// }, +// MoreExecutors.directExecutor() +// ) +// +// } +// } else { +// } MediaController .Builder( application, @@ -176,7 +228,24 @@ class MusicViewModel( { mediaController = get() mediaController!!.addListener(playerListener) - mediaController!!.setMediaItems(mediaStoreHelper.musics) + viewModelScope.launch { + combine( + mediaStoreHelper.fetchLatestMusics(), + safManager.fetchLatestSafTracks() + ) { musics, safTracks -> + val combinedList = musics + safTracks + combinedList + } + .debounce(500) + .collectLatest { combinedList -> + mediaController!!.replaceMediaItems( + 0, + combinedList.size - 1, + combinedList + ) + } + } + }, MoreExecutors.directExecutor() ) @@ -219,7 +288,8 @@ class MusicViewModel( val fd = getFileDescriptorFromPath(application, musicState.value.currentPath) return fd?.dup()?.detachFd()?.let { - TagLib.getMetadata(it)?.propertyMap["LYRICS"]?.getOrNull(0) ?: application.getString(R.string.no_lyrics_note) + TagLib.getMetadata(it)?.propertyMap["LYRICS"]?.getOrNull(0) + ?: application.getString(R.string.no_lyrics_note) } ?: application.getString(R.string.no_lyrics_note) } 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 c912c09..def2896 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 @@ -1,5 +1,6 @@ package com.sosauce.cutemusic.ui.shared_components +import android.annotation.SuppressLint import android.net.Uri import android.util.Log import androidx.activity.result.ActivityResultLauncher @@ -12,24 +13,40 @@ import androidx.lifecycle.viewModelScope import androidx.media3.common.MediaItem import com.sosauce.cutemusic.domain.model.Album import com.sosauce.cutemusic.domain.repository.MediaStoreHelper +import com.sosauce.cutemusic.domain.repository.SafManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlin.collections.filter class PostViewModel( - private val mediaStoreHelper: MediaStoreHelper + private val mediaStoreHelper: MediaStoreHelper, + private val safManager: SafManager ) : ViewModel() { + @SuppressLint("UnsafeOptInUsageError") + val safTracks = safManager.fetchLatestSafTracks() + +// @SuppressLint("UnsafeOptInUsageError") +// var musics = combine(safTracks, mediaStoreHelper.fetchLatestMusics()) { safList, trackList -> +// safList + trackList +// }.stateIn( +// CoroutineScope(Dispatchers.IO), +// SharingStarted.WhileSubscribed(5000), +// mediaStoreHelper.musics +// ) + var musics = mediaStoreHelper.fetchLatestMusics().stateIn( - viewModelScope, + CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), mediaStoreHelper.musics ) + var albums = mediaStoreHelper.fetchLatestAlbums().stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000), @@ -56,9 +73,7 @@ class PostViewModel( fun albumSongs(album: String) { try { viewModelScope.launch { - musics.collectLatest { - albumSongs = it.filter { it.mediaMetadata.albumTitle.toString() == album } - } + albumSongs = musics.value.filter { it.mediaMetadata.albumTitle.toString() == album } } } catch (e: Exception) { Log.e(CUTE_ERROR, e.message, e) @@ -68,9 +83,7 @@ class PostViewModel( fun artistSongs(artistName: String) { try { viewModelScope.launch { - musics.collectLatest { - artistSongs = it.filter { it.mediaMetadata.artist == artistName } - } + artistSongs = musics.value.filter { it.mediaMetadata.artist == artistName } } } catch (e: Exception) { Log.e(CUTE_ERROR, e.message, e) 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 f7ef891..aa66759 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 @@ -93,7 +93,11 @@ fun SharedTransitionScope.CuteSearchbar( minWidth = 45.dp, minHeight = 45.dp ) - .align(Alignment.End), + .align(Alignment.End) + .sharedBounds( + sharedContentState = rememberSharedContentState(key = "fab"), + animatedVisibilityScope = animatedVisibilityScope + ), shape = RoundedCornerShape(14.dp) ) { Icon( @@ -111,12 +115,11 @@ fun SharedTransitionScope.CuteSearchbar( color = MaterialTheme.colorScheme.surfaceContainer, shape = RoundedCornerShape(roundedShape) ) - .thenIf( - isPlayerReady, + .thenIf(isPlayerReady) { Modifier.clickable { onNavigate() } - ) + } ) { AnimatedVisibility( visible = isPlayerReady, @@ -149,15 +152,11 @@ fun SharedTransitionScope.CuteSearchbar( text = currentlyPlaying, modifier = Modifier .padding(start = 5.dp) - .sharedElement( - state = rememberSharedContentState(key = "currentlyPlaying"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(500) - } - ) .basicMarquee() - + .sharedBounds( + sharedContentState = rememberSharedContentState(key = "currentlyPlaying"), + animatedVisibilityScope = animatedVisibilityScope + ) ) } Row { @@ -188,10 +187,7 @@ fun SharedTransitionScope.CuteSearchbar( } .sharedElement( state = rememberSharedContentState(key = "skipPreviousButton"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(500) - } + animatedVisibilityScope = animatedVisibilityScope ) ) } @@ -203,10 +199,7 @@ fun SharedTransitionScope.CuteSearchbar( contentDescription = null, modifier = Modifier.sharedElement( state = rememberSharedContentState(key = "playPauseIcon"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(500) - } + animatedVisibilityScope = animatedVisibilityScope ) ) } @@ -237,10 +230,7 @@ fun SharedTransitionScope.CuteSearchbar( } .sharedElement( state = rememberSharedContentState(key = "skipNextButton"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(500) - } + animatedVisibilityScope = animatedVisibilityScope ) ) } diff --git a/app/src/main/java/com/sosauce/cutemusic/utils/Constants.kt b/app/src/main/java/com/sosauce/cutemusic/utils/Constants.kt new file mode 100644 index 0000000..339a2cb --- /dev/null +++ b/app/src/main/java/com/sosauce/cutemusic/utils/Constants.kt @@ -0,0 +1,4 @@ +package com.sosauce.cutemusic.utils + +const val CUTE_MUSIC_ID = "CUTE_MUSIC_ID" +const val ROOT_ID = "cute_music_root" \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/utils/Extensions.kt b/app/src/main/java/com/sosauce/cutemusic/utils/Extensions.kt index 7f56fa0..0c1c5ff 100644 --- a/app/src/main/java/com/sosauce/cutemusic/utils/Extensions.kt +++ b/app/src/main/java/com/sosauce/cutemusic/utils/Extensions.kt @@ -19,17 +19,17 @@ import com.kyant.taglib.PropertyMap import com.sosauce.cutemusic.data.datastore.rememberIsLandscape import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow +import java.io.File +import java.io.FileOutputStream import java.util.Locale fun Modifier.thenIf( condition: Boolean, - modifier: Modifier + modifier: Modifier.() -> Modifier ): Modifier { - return this.then( - if (condition) { - modifier - } else Modifier - ) + return if (condition) { + this.then(modifier()) + } else this } fun Long.formatBinarySize(): String { @@ -140,6 +140,18 @@ fun Player.applyPlaybackSpeed( } +fun ByteArray.getUriFromByteArray(context: Context): Uri { + val albumArtFile = File(context.cacheDir, "album_art_${this.hashCode()}.jpg") + try { + FileOutputStream(albumArtFile).use { os -> + os.write(this) + } + return Uri.fromFile(albumArtFile) + } catch (e: Exception) { + return Uri.EMPTY + } +} + fun Uri.getBitrate(context: Context): String { val retriever = MediaMetadataRetriever() return try { @@ -221,6 +233,7 @@ fun AudioFileMetadata.toPropertyMap(): PropertyMap { fun ContentResolver.observe(uri: Uri) = callbackFlow { val observer = object : ContentObserver(null) { override fun onChange(selfChange: Boolean) { + trySend(selfChange) } } diff --git a/app/src/main/res/drawable/trash_rounded_filled.xml b/app/src/main/res/drawable/trash_rounded_filled.xml new file mode 100644 index 0000000..c14cfe1 --- /dev/null +++ b/app/src/main/res/drawable/trash_rounded_filled.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e71948b..9e74932 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -75,4 +75,10 @@ Set sleep timer Go to: No lyrics found ! + Equalizer + This track comes from the S.A.F + Open S.A.F to add tracks + Blacklisted + Not blacklisted + S.A.F manager \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 759f737..b71e133 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,8 +6,8 @@ koinAndroidxStartup = "4.0.0" kotlin = "2.0.21" activityCompose = "1.9.3" coilCompose = "3.0.3" -composeBom = "2024.11.00" -composeAnimation = "1.7.5" +composeBom = "2024.12.01" +composeAnimation = "1.7.6" coreKtx = "1.15.0" coreSplashscreen = "1.0.1" datastorePreferences = "1.1.1" @@ -16,7 +16,7 @@ lifecycleViewmodelCompose = "2.8.7" media3Common = "1.5.0" media3Exoplayer = "1.5.0" media3Session = "1.5.0" -navigationCompose = "2.8.4" +navigationCompose = "2.8.5" squigglyslider = "1.0.0" serialization = "2.0.0" taglib = "1.0.0-alpha25"