From 5a10bcd07e3f069d5a33418a2b102f710f7357a1 Mon Sep 17 00:00:00 2001 From: sosauce2 <98750531+sosauce@users.noreply.github.com> Date: Sun, 3 Nov 2024 20:15:13 +0100 Subject: [PATCH] v2.2.4 --- .DS_Store | Bin 8196 -> 8196 bytes ...otlin-compiler-16555594222562041686.salive | 0 app/.DS_Store | Bin 8196 -> 8196 bytes app/build.gradle.kts | 8 +- app/release/baselineProfiles/0/app-release.dm | Bin 7250 -> 7333 bytes app/release/baselineProfiles/1/app-release.dm | Bin 7224 -> 7307 bytes app/release/output-metadata.json | 4 +- app/src/.DS_Store | Bin 6148 -> 6148 bytes app/src/main/.DS_Store | Bin 6148 -> 6148 bytes app/src/main/AndroidManifest.xml | 34 +- .../com/sosauce/cutemusic/data/MusicState.kt | 5 +- .../cutemusic/data/actions/MetadataActions.kt | 4 +- .../cutemusic/data/actions/PlayerActions.kt | 12 + .../cutemusic/data/datastore/DataStore.kt | 6 + .../domain/repository/MediaStoreHelperImpl.kt | 6 + .../main/quickplay/QuickPlayActivity.kt | 45 ++- .../cutemusic/ui/navigation/Navigation.kt | 12 +- .../ui/screens/album/AlbumDetailsLandscape.kt | 3 +- .../ui/screens/album/AlbumDetailsScreen.kt | 34 +- .../cutemusic/ui/screens/album/AlbumScreen.kt | 8 +- .../ui/screens/artist/ArtistDetails.kt | 43 +-- .../screens/artist/ArtistDetailsLandscape.kt | 6 +- .../ui/screens/artist/ArtistsScreen.kt | 7 +- .../screens/blacklisted/BlacklistedScreen.kt | 53 ++- .../components/AllFolderBottomSheet.kt | 72 ++-- .../cutemusic/ui/screens/lyrics/LyricsView.kt | 2 +- .../cutemusic/ui/screens/main/MainScreen.kt | 106 +++--- .../main/components/ShareOptionsContent.kt | 97 ++++++ .../ui/screens/metadata/MetadataEditor.kt | 11 +- .../ui/screens/metadata/MetadataState.kt | 1 + .../ui/screens/metadata/MetadataViewModel.kt | 32 +- .../ui/screens/playing/NowPlayingLandscape.kt | 5 +- .../ui/screens/playing/NowPlayingScreen.kt | 14 +- .../ui/screens/playing/components/Buttons.kt | 259 +++++++++++++- .../playing/components/QuickActionsRow.kt | 29 +- .../components/RateAdjustmentDialog.kt | 89 +++++ .../ui/screens/playing/components/Slider.kt | 69 ++-- .../screens/playing/components/SpeedCard.kt | 123 +++++-- .../ui/screens/settings/SettingsScreen.kt | 18 + .../settings/compenents/SettingsCards.kt | 47 +++ .../screens/settings/compenents/Switches.kt | 42 ++- .../shared_components/MusicDetailsDialog.kt | 11 +- .../ui/shared_components/MusicViewModel.kt | 90 ++--- .../ui/shared_components/Searchbar.kt | 319 ++++++++++-------- .../com/sosauce/cutemusic/ui/theme/Theme.kt | 2 +- .../com/sosauce/cutemusic/utils/Extensions.kt | 8 + .../com/sosauce/cutemusic/utils/ImageUtils.kt | 6 +- app/src/main/res/.DS_Store | Bin 8196 -> 8196 bytes app/src/main/res/drawable/ribbon.xml | 9 + app/src/main/res/values/strings.xml | 11 + gradle/libs.versions.toml | 16 +- 51 files changed, 1279 insertions(+), 499 deletions(-) create mode 100644 .kotlin/sessions/kotlin-compiler-16555594222562041686.salive create mode 100644 app/src/main/java/com/sosauce/cutemusic/ui/screens/main/components/ShareOptionsContent.kt create mode 100644 app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/RateAdjustmentDialog.kt create mode 100644 app/src/main/res/drawable/ribbon.xml diff --git a/.DS_Store b/.DS_Store index fb9ffd8479591b2c6412312e3e82df94fde078c7..e735c14a1559f092b90958ed1c53d358e83359f0 100644 GIT binary patch delta 41 ucmZp1XmQwJF2csi$-&9XIoVz~SrEjG7m%o~HZn8NQ7|U4PNeK@C delta 16 XcmZp1XmQwJE;89bbob^@B0}5%H9rOI diff --git a/.kotlin/sessions/kotlin-compiler-16555594222562041686.salive b/.kotlin/sessions/kotlin-compiler-16555594222562041686.salive new file mode 100644 index 0000000..e69de29 diff --git a/app/.DS_Store b/app/.DS_Store index 503656357ac9fb4478e3fbc0165cd44a6adeb2fa..222e6412527f48ab0de7d72e9134fae938b6b652 100644 GIT binary patch delta 82 zcmZp1XmQwZS&WU7lY^6&bF!mAvLJ{VFCbA}ZDeMkqhM-eG4fq{;KiJ3{QjzYDi5r}PKJo%u2>E=FhS9ZKACijV#0RSTF B6!-uD diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 76943c1..d9b74f1 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 = 14 - versionName = "2.2.3" + versionCode = 15 + versionName = "2.2.4" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true @@ -79,9 +79,9 @@ android { implementation(libs.kotlinx.serialization.json) implementation(libs.koin.android) implementation(libs.koin.androidx.compose) - debugImplementation(libs.androidx.ui.tooling) - //implementation("com.materialkolor:material-kolor:1.7.1") + //implementation("com.materialkolor:material-kolor:2.0.0") implementation(libs.koin.androidx.startup) implementation(libs.jaudiotagger) + debugImplementation(libs.androidx.ui.tooling) } } diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm index 135c0325c5a30611fc0ad61ad55c62774b2e39cd..84dc65e02cd9586fc16ba65f00541989b4910407 100644 GIT binary patch literal 7333 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 VsIG*A`=9cY$K&xBY3TlQ`!BS_kGuc? literal 7250 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 diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index 5b69d58..c7a5ead 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -11,8 +11,8 @@ "type": "SINGLE", "filters": [], "attributes": [], - "versionCode": 13, - "versionName": "2.2.2", + "versionCode": 15, + "versionName": "2.2.4", "outputFile": "app-release.apk" } ], diff --git a/app/src/.DS_Store b/app/src/.DS_Store index 95ed873fb3735b6508de8be81badcb232a954c2a..2752949254ad86430b71993ab77a82171f3e9e54 100644 GIT binary patch delta 45 ycmZoMXffE}&C15f$-&9XIXRFuSrEjG7m%o~HZn8NQ7| + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/data/MusicState.kt b/app/src/main/java/com/sosauce/cutemusic/data/MusicState.kt index cff4672..a7ea19a 100644 --- a/app/src/main/java/com/sosauce/cutemusic/data/MusicState.kt +++ b/app/src/main/java/com/sosauce/cutemusic/data/MusicState.kt @@ -1,6 +1,8 @@ package com.sosauce.cutemusic.data import android.net.Uri +import androidx.media3.common.PlaybackParameters +import androidx.media3.common.Player import com.sosauce.cutemusic.domain.model.Lyrics import java.io.File @@ -20,5 +22,6 @@ data class MusicState( val currentAlbum: String = "", val currentAlbumId: Long = 0, val currentSize: Long = 0, - val currentLrcFile: File? = null + val currentLrcFile: File? = null, + val playbackParameters: PlaybackParameters = PlaybackParameters.DEFAULT, ) diff --git a/app/src/main/java/com/sosauce/cutemusic/data/actions/MetadataActions.kt b/app/src/main/java/com/sosauce/cutemusic/data/actions/MetadataActions.kt index f2499ab..f32cf05 100644 --- a/app/src/main/java/com/sosauce/cutemusic/data/actions/MetadataActions.kt +++ b/app/src/main/java/com/sosauce/cutemusic/data/actions/MetadataActions.kt @@ -5,9 +5,7 @@ sealed interface MetadataActions { val path: String ) : MetadataActions - data class SaveChanges( - val path: String - ) : MetadataActions + data object SaveChanges : MetadataActions data object ClearState : MetadataActions } \ No newline at end of file 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 c8265ee..35a0d89 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 @@ -1,5 +1,7 @@ package com.sosauce.cutemusic.data.actions +import android.net.Uri + sealed interface PlayerActions { data object PlayOrPause : PlayerActions data object SeekToNextMusic : PlayerActions @@ -8,6 +10,7 @@ sealed interface PlayerActions { data object PlayRandom : PlayerActions data object ApplyLoop : PlayerActions data object ApplyShuffle : PlayerActions + data object StopPlayback : PlayerActions data class SeekTo(val position: Long) : PlayerActions data class SeekToSlider(val position: Long) : PlayerActions data class RewindTo(val position: Long) : PlayerActions @@ -26,4 +29,13 @@ sealed interface PlayerActions { val speed: Float, val pitch: Float ) : PlayerActions + + data class UpdateCurrentPosition( + val position: Long + ) : PlayerActions + + + data class QuickPlay( + val uri: Uri + ) : 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 bbaa39b..3f0a608 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 @@ -11,6 +11,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.SHOW_X_BUTTON 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 @@ -40,6 +41,7 @@ private data object PreferencesKeys { val USE_ART_THEME = booleanPreferencesKey("use_art_theme") val APPLY_LOOP = booleanPreferencesKey("apply_loop") val USE_CLASSIC_SLIDER = booleanPreferencesKey("use_classic_slider") + val SHOW_X_BUTTON = booleanPreferencesKey("show_x_button") } @Composable @@ -95,3 +97,7 @@ fun rememberShouldApplyLoop() = @Composable fun rememberUseClassicSlider() = rememberPreference(key = USE_CLASSIC_SLIDER, defaultValue = false) + +@Composable +fun rememberShowXButton() = + rememberPreference(key = SHOW_X_BUTTON, defaultValue = true) 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 710ff8f..4af0bdd 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 @@ -12,6 +12,7 @@ import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.IntentSenderRequest import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata +import androidx.media3.common.util.UnstableApi import com.sosauce.cutemusic.domain.model.Album import com.sosauce.cutemusic.domain.model.Artist import com.sosauce.cutemusic.domain.model.Folder @@ -20,6 +21,7 @@ class MediaStoreHelperImpl( private val context: Context ) : MediaStoreHelper { + @UnstableApi override fun fetchMusics(): List { val musics = mutableListOf() @@ -32,6 +34,7 @@ class MediaStoreHelperImpl( MediaStore.Audio.Media.ALBUM_ID, MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.SIZE, + MediaStore.Audio.Media.DURATION, //MediaStore.Audio.Media.IS_FAVORITE, ) @@ -52,6 +55,7 @@ class MediaStoreHelperImpl( val albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID) 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 isFavColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.IS_FAVORITE) while (cursor.moveToNext()) { @@ -64,6 +68,7 @@ class MediaStoreHelperImpl( val filePath = cursor.getString(folderColumn) val folder = filePath.substring(0, filePath.lastIndexOf('/')) val size = cursor.getLong(sizeColumn) + val duration = cursor.getLong(durationColumn) //val isFavorite = cursor.getInt(isFavColumn) // 1 = is favorite, 0 = no val uri = ContentUris.withAppendedId( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, @@ -87,6 +92,7 @@ class MediaStoreHelperImpl( .setArtist(artist) .setAlbumTitle(album) .setArtworkUri(artUri) + .setDurationMs(duration) .setExtras( Bundle() .apply { 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 333ace8..8900b64 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 @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalSharedTransitionApi::class) + package com.sosauce.cutemusic.main.quickplay import android.content.Intent @@ -7,23 +9,18 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.foundation.background 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.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Pause -import androidx.compose.material.icons.rounded.PlayArrow -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -35,6 +32,7 @@ import androidx.compose.ui.unit.sp import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sosauce.cutemusic.data.actions.PlayerActions +import com.sosauce.cutemusic.ui.screens.playing.components.ActionsButtonsRowQuickPlay import com.sosauce.cutemusic.ui.screens.playing.components.MusicSlider import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.MusicViewModel @@ -60,6 +58,7 @@ class QuickPlayActivity : ComponentActivity() { val viewModel = koinViewModel() val musicState by viewModel.musicState.collectAsStateWithLifecycle() + when { intent?.action == Intent.ACTION_SEND -> { if (intent?.type?.startsWith("audio/") == true) { @@ -76,6 +75,13 @@ class QuickPlayActivity : ComponentActivity() { } } } + + LaunchedEffect(Unit) { + viewModel.handlePlayerActions( + PlayerActions.QuickPlay(uri!!) + ) + } + Column( modifier = Modifier .fillMaxSize() @@ -104,25 +110,14 @@ class QuickPlayActivity : ComponentActivity() { musicState = musicState ) Spacer(modifier = Modifier.height(7.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - FloatingActionButton( - onClick = { - if (!viewModel.isPlayerReady()) viewModel.quickPlay(uri) else viewModel.handlePlayerActions( - PlayerActions.PlayOrPause - ) - } - ) { - Icon( - imageVector = if (musicState.isCurrentlyPlaying) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, - contentDescription = "pause/play button" - ) - } - } + ActionsButtonsRowQuickPlay( + onClickLoop = { viewModel.handlePlayerActions(PlayerActions.ApplyLoop) }, + onClickShuffle = { viewModel.handlePlayerActions(PlayerActions.ApplyShuffle) }, + onEvent = { viewModel.handlePlayerActions(it) }, + musicState = musicState + ) } + } } } 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 4a87145..affb724 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 @@ -46,10 +46,14 @@ fun Nav() { SharedTransitionLayout { + + this NavHost( navController = navController, startDestination = Screen.Main ) { + + this@SharedTransitionLayout composable { MainScreen( musics = musics, @@ -99,6 +103,7 @@ fun Nav() { postViewModel.artistSongs(it) postViewModel.artistAlbums(it) }, + musicState = musicState ) } @@ -130,8 +135,8 @@ fun Nav() { launchSingleTop = true } }, - selectedIndex = viewModel.selectedItem - + selectedIndex = viewModel.selectedItem, + musicState = musicState ) } composable { @@ -165,7 +170,8 @@ fun Nav() { listToHandle = ListToHandle.ARTISTS, query = query ) - } + }, + musicState = musicState ) } 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 71c1ccd..a41164e 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 @@ -110,7 +110,8 @@ fun AlbumDetailsLandscape( it ) ) - } + }, + isPlayerReady = viewModel.isPlayerReady() ) } } 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 0eb3f5c..8ce2b63 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 @@ -22,9 +22,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack -import androidx.compose.material.icons.rounded.Shuffle 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 @@ -119,25 +117,6 @@ private fun SharedTransitionScope.AlbumDetailsContent( } } ) - }, - floatingActionButton = { - FloatingActionButton( - onClick = { - viewModel.handlePlayerActions( - PlayerActions.StartAlbumPlayback( - albumName = album.name, - mediaId = null - ) - ) - }, - modifier = Modifier - .padding(bottom = 55.dp) - ) { - Icon( - imageVector = Icons.Rounded.Shuffle, - contentDescription = null - ) - } } ) { values -> Box( @@ -204,7 +183,8 @@ private fun SharedTransitionScope.AlbumDetailsContent( ) ) }, - currentMusicUri = musicState.currentMusicUri + currentMusicUri = musicState.currentMusicUri, + isPlayerReady = viewModel.isPlayerReady() ) } } @@ -223,7 +203,15 @@ private fun SharedTransitionScope.AlbumDetailsContent( .padding(end = rememberSearchbarRightPadding()) .align(rememberSearchbarAlignment()), showSearchField = false, - onNavigate = { onNavigate(Screen.NowPlaying) } + onNavigate = { onNavigate(Screen.NowPlaying) }, + onClickFAB = { + viewModel.handlePlayerActions( + PlayerActions.StartAlbumPlayback( + albumName = album.name, + mediaId = null + ) + ) + } ) } 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 9f9f868..13d3c40 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 @@ -48,6 +48,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.sosauce.cutemusic.R +import com.sosauce.cutemusic.data.MusicState import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.data.datastore.rememberIsLandscape import com.sosauce.cutemusic.domain.model.Album @@ -76,7 +77,8 @@ fun SharedTransitionScope.AlbumsScreen( isPlaying: Boolean, onHandlePlayerActions: (PlayerActions) -> Unit, isPlayerReady: Boolean, - onNavigationItemClicked: (Int, NavigationItem) -> Unit + onNavigationItemClicked: (Int, NavigationItem) -> Unit, + musicState: MusicState ) { val isLandscape = rememberIsLandscape() var query by remember { mutableStateOf("") } @@ -91,7 +93,6 @@ fun SharedTransitionScope.AlbumsScreen( } Box { - if (albums.isEmpty()) { Column( modifier = Modifier @@ -215,7 +216,8 @@ fun SharedTransitionScope.AlbumsScreen( isPlaying = isPlaying, animatedVisibilityScope = animatedVisibilityScope, isPlayerReady = isPlayerReady, - onNavigate = { onNavigate(Screen.NowPlaying) } + onNavigate = { onNavigate(Screen.NowPlaying) }, + onClickFAB = { onHandlePlayerActions(PlayerActions.PlayRandom) } ) } } 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 b1d2783..99991ac 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,9 +21,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.filled.ArrowBack -import androidx.compose.material.icons.rounded.Shuffle import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -77,7 +75,8 @@ fun SharedTransitionScope.ArtistDetails( onNavigate = { navController.navigate(it) }, chargePVMAlbumSongs = { postViewModel.albumSongs(it) }, artist = artist, - currentMusicUri = musicState.currentMusicUri + currentMusicUri = musicState.currentMusicUri, + isPlayerReady = viewModel.isPlayerReady() ) } else { Scaffold( @@ -109,25 +108,6 @@ fun SharedTransitionScope.ArtistDetails( } } ) - }, - floatingActionButton = { - FloatingActionButton( - onClick = { - viewModel.handlePlayerActions( - PlayerActions.StartArtistPlayback( - artistName = artist.name, - mediaId = null - ) - ) - }, - modifier = Modifier - .padding(bottom = 55.dp) - ) { - Icon( - imageVector = Icons.Rounded.Shuffle, - contentDescription = null - ) - } } ) { values -> Box( @@ -154,8 +134,10 @@ fun SharedTransitionScope.ArtistDetails( ) } } - Spacer(modifier = Modifier.height(10.dp)) - HorizontalDivider() + if (artistAlbums.isNotEmpty()) { + Spacer(modifier = Modifier.height(10.dp)) + HorizontalDivider() + } Spacer(modifier = Modifier.height(10.dp)) LazyColumn { items(artistSongs) { music -> @@ -169,7 +151,8 @@ fun SharedTransitionScope.ArtistDetails( ) ) }, - currentMusicUri = musicState.currentMusicUri + currentMusicUri = musicState.currentMusicUri, + isPlayerReady = viewModel.isPlayerReady() ) } } @@ -186,7 +169,15 @@ fun SharedTransitionScope.ArtistDetails( .padding(end = rememberSearchbarRightPadding()) .align(rememberSearchbarAlignment()), showSearchField = false, - onNavigate = { onNavigate(Screen.NowPlaying) } + onNavigate = { onNavigate(Screen.NowPlaying) }, + onClickFAB = { + viewModel.handlePlayerActions( + PlayerActions.StartArtistPlayback( + artistName = artist.name, + mediaId = null + ) + ) + } ) } } 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 e84e35a..2e83c40 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 @@ -40,7 +40,8 @@ fun ArtistDetailsLandscape( onNavigate: (Screen) -> Unit, chargePVMAlbumSongs: (String) -> Unit, artist: Artist, - currentMusicUri: String + currentMusicUri: String, + isPlayerReady: Boolean ) { Column( modifier = Modifier @@ -96,7 +97,8 @@ fun ArtistDetailsLandscape( MusicListItem( music = music, currentMusicUri = currentMusicUri, - onShortClick = { onClickPlay(it) } + onShortClick = { onClickPlay(it) }, + isPlayerReady = isPlayerReady ) } } 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 e4dfdc1..283776b 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 @@ -45,6 +45,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.sosauce.cutemusic.R +import com.sosauce.cutemusic.data.MusicState import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.domain.model.Artist import com.sosauce.cutemusic.ui.navigation.Screen @@ -71,7 +72,8 @@ fun SharedTransitionScope.ArtistsScreen( isPlaying: Boolean, onHandlePlayerActions: (PlayerActions) -> Unit, isPlayerReady: Boolean, - onNavigationItemClicked: (Int, NavigationItem) -> Unit + onNavigationItemClicked: (Int, NavigationItem) -> Unit, + musicState: MusicState ) { var query by remember { mutableStateOf("") } @@ -204,7 +206,8 @@ fun SharedTransitionScope.ArtistsScreen( isPlaying = isPlaying, animatedVisibilityScope = animatedVisibilityScope, isPlayerReady = isPlayerReady, - onNavigate = { onNavigate(Screen.NowPlaying) } + onNavigate = { onNavigate(Screen.NowPlaying) }, + onClickFAB = { onHandlePlayerActions(PlayerActions.PlayRandom) } ) } } 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 633c4a8..9db3731 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 @@ -3,17 +3,20 @@ 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 import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +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 @@ -35,10 +38,10 @@ import androidx.compose.runtime.remember 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.ColorFilter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController @@ -120,16 +123,36 @@ private fun BlacklistedScreenContent( } ) { values -> LazyColumn( - modifier = Modifier.padding(values) + modifier = Modifier + .fillMaxSize() + .padding(values) ) { - items(items = blacklistedFolders.toList()) { folder -> + itemsIndexed( + items = blacklistedFolders.toList(), + key = { _, folder -> folder } + ) { index, folder -> + + 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) + ) + BlackFolderItem( folder = folder, onClick = { blacklistedFolders = blacklistedFolders.toMutableSet().apply { remove(folder) } - } + }, + topDp = topDp, + bottomDp = bottomDp, + modifier = Modifier.animateItem() ) } } @@ -139,20 +162,28 @@ private fun BlacklistedScreenContent( @Composable private fun BlackFolderItem( + modifier: Modifier = Modifier, folder: String, - onClick: () -> Unit + onClick: () -> Unit, + topDp: Dp, + bottomDp: Dp, ) { Card( - modifier = Modifier + modifier = modifier .padding( start = 13.dp, end = 13.dp, bottom = 8.dp - ) - .clip(RoundedCornerShape(24.dp)), + ), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceContainer - ) + ), + shape = RoundedCornerShape( + topStart = topDp, + topEnd = topDp, + bottomStart = bottomDp, + bottomEnd = bottomDp + ), ) { Row( modifier = Modifier @@ -185,7 +216,7 @@ private fun BlackFolderItem( ) } IconButton( - onClick = { onClick() } + onClick = onClick ) { Icon( imageVector = Icons.Default.Delete, 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 index ee8b92c..1d5c0b5 100644 --- 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 @@ -1,19 +1,25 @@ package com.sosauce.cutemusic.ui.screens.blacklisted.components -import androidx.compose.foundation.clickable +import androidx.compose.foundation.Image 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.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FolderOpen import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults 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.graphics.ColorFilter +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 @@ -25,15 +31,17 @@ fun AllFoldersBottomSheet( ) { LazyColumn { - items( + itemsIndexed( items = folders, - key = { it.path } - ) { + key = { _, folder -> folder.path } + ) { index, folder -> FoldersLayout( - folder = it, + 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 ) } } @@ -43,7 +51,9 @@ fun AllFoldersBottomSheet( @Composable private fun FoldersLayout( folder: Folder, - onClick: (path: String) -> Unit + onClick: (path: String) -> Unit, + topDp: Dp, + bottomDp: Dp, ) { Card( colors = CardDefaults.cardColors( @@ -51,24 +61,42 @@ private fun FoldersLayout( alpha = 0.5f ) ), - modifier = Modifier - .padding(5.dp) - .clip(RoundedCornerShape(24.dp)) - .clickable { onClick(folder.path) } + modifier = Modifier.padding(5.dp), + shape = RoundedCornerShape( + topStart = topDp, + topEnd = topDp, + bottomStart = bottomDp, + bottomEnd = bottomDp + ), + onClick = { onClick(folder.path) } ) { - Column( - verticalArrangement = Arrangement.Center, + Row( modifier = Modifier .fillMaxWidth() - .padding(15.dp) + .padding(15.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween ) { - CuteText( - text = folder.name - ) - CuteText( - text = folder.path, - color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f) + Image( + imageVector = Icons.Default.FolderOpen, + contentDescription = null, + modifier = Modifier.size(33.dp), + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground) ) + 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/lyrics/LyricsView.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/lyrics/LyricsView.kt index 74c9df7..7c8f13e 100644 --- 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 @@ -94,7 +94,7 @@ fun LyricsView( if (musicState.currentLyrics.isEmpty()) { item { CuteText( - text = viewModel.loadEmbeddedLyrics(musicState.currentPath), + text = viewModel.loadEmbeddedLyrics(musicState.currentPath).toString(), ) } } else { 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 123cbf6..cc6578d 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 @@ -6,13 +6,13 @@ package com.sosauce.cutemusic.ui.screens.main import android.app.Activity +import android.content.Intent import android.net.Uri import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.IntentSenderRequest import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.Crossfade import androidx.compose.animation.ExperimentalSharedTransitionApi @@ -24,7 +24,6 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween -import androidx.compose.animation.scaleOut import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee @@ -54,17 +53,18 @@ import androidx.compose.material.icons.rounded.MoreVert import androidx.compose.material.icons.rounded.MusicNote import androidx.compose.material.icons.rounded.Person import androidx.compose.material.icons.rounded.Settings -import androidx.compose.material.icons.rounded.Shuffle +import androidx.compose.material.icons.rounded.Share +import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor 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 import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -73,7 +73,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate -import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -82,9 +81,11 @@ import androidx.compose.ui.unit.dp import androidx.media3.common.MediaItem import coil3.compose.AsyncImage import com.sosauce.cutemusic.R +import com.sosauce.cutemusic.data.MusicState import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.data.datastore.rememberHasSeenTip import com.sosauce.cutemusic.ui.navigation.Screen +import com.sosauce.cutemusic.ui.screens.main.components.ShareOptionsContent import com.sosauce.cutemusic.ui.shared_components.CuteSearchbar import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.MusicDetailsDialog @@ -115,7 +116,8 @@ fun SharedTransitionScope.MainScreen( onHandleSorting: (SortingType) -> Unit, onHandleSearching: (String) -> Unit, onChargeAlbumSongs: (String) -> Unit, - onChargeArtistLists: (String) -> Unit + onChargeArtistLists: (String) -> Unit, + musicState: MusicState ) { var query by remember { mutableStateOf("") } val state = rememberLazyListState() @@ -125,31 +127,18 @@ fun SharedTransitionScope.MainScreen( targetValue = if (isSortedByASC) 45f else 135f, label = "Arrow Icon Animation" ) - - - Scaffold( - floatingActionButton = { - AnimatedVisibility( - visible = !isPlayerReady, - exit = scaleOut( - // 2 times faster than the searchbar so it doesn't look too weird - animationSpec = tween(250), - transformOrigin = TransformOrigin(0.5f, 0.25f) - ) - ) { - FloatingActionButton( - onClick = { onHandlePlayerAction(PlayerActions.PlayRandom) }, - modifier = Modifier - .padding(bottom = 70.dp) - ) { - Icon( - imageVector = Icons.Rounded.Shuffle, - contentDescription = null - ) - } + val showCuteSearchbar by remember { + derivedStateOf { + if (musics.isEmpty()) { + true + } else { + state.layoutInfo.visibleItemsInfo.lastOrNull()?.index != musics.size - 1 } } - ) { _ -> + } + + + Scaffold { _ -> Box(Modifier.fillMaxSize()) { LazyColumn( state = state @@ -159,6 +148,7 @@ fun SharedTransitionScope.MainScreen( CuteText( text = stringResource(id = R.string.no_musics_found), modifier = Modifier + .statusBarsPadding() .padding(16.dp) .fillMaxWidth(), textAlign = TextAlign.Center @@ -184,13 +174,15 @@ fun SharedTransitionScope.MainScreen( currentMusicUri = currentMusicUri, onLoadMetadata = onLoadMetadata, showBottomSheet = true, - modifier = Modifier.thenIf( - index == 0, - Modifier.statusBarsPadding() - ), onDeleteMusic = onDeleteMusic, onChargeAlbumSongs = onChargeAlbumSongs, - onChargeArtistLists = onChargeArtistLists + onChargeArtistLists = onChargeArtistLists, + modifier = Modifier + .thenIf( + index == 0, + Modifier.statusBarsPadding() + ), + isPlayerReady = isPlayerReady ) } } @@ -199,8 +191,7 @@ fun SharedTransitionScope.MainScreen( // TODO : How do you make it NOT scroll to the first item when sorting changes !!!!! Crossfade( - targetState = true, - //targetState = state.canScrollForward || musics.size <= 15, + targetState = showCuteSearchbar, label = "", modifier = Modifier.align(rememberSearchbarAlignment()) ) { visible -> @@ -243,7 +234,6 @@ fun SharedTransitionScope.MainScreen( IconButton( onClick = { screenSelectionExpanded = true - // Let's prevent writing to datastore everytime the user clicks ;) if (!hasSeenTip) { hasSeenTip = true } @@ -308,7 +298,8 @@ fun SharedTransitionScope.MainScreen( isPlaying = isCurrentlyPlaying, animatedVisibilityScope = animatedVisibilityScope, isPlayerReady = isPlayerReady, - onNavigate = { onNavigateTo(Screen.NowPlaying) } + onNavigate = { onNavigateTo(Screen.NowPlaying) }, + onClickFAB = { onHandlePlayerAction(PlayerActions.PlayRandom) } ) } } @@ -328,16 +319,18 @@ fun MusicListItem( onDeleteMusic: (List, ActivityResultLauncher) -> Unit = { _, _ -> }, onChargeAlbumSongs: (String) -> Unit = {}, onChargeArtistLists: (String) -> Unit = {}, + isPlayerReady: Boolean ) { val context = LocalContext.current var isDropDownExpanded by remember { mutableStateOf(false) } var showDetailsDialog by remember { mutableStateOf(false) } + var showShareOptions by remember { mutableStateOf(false) } val uri = remember { Uri.parse(music.mediaMetadata.extras?.getString("uri")) } val path = remember { music.mediaMetadata.extras?.getString("path") } val isPlaying = currentMusicUri == music.mediaMetadata.extras?.getString("uri") val bgColor by animateColorAsState( - targetValue = if (isPlaying) { + targetValue = if (isPlaying && isPlayerReady) { MaterialTheme.colorScheme.surfaceContainer } else { MaterialTheme.colorScheme.background @@ -363,6 +356,14 @@ fun MusicListItem( } } + val shareIntent = Intent() + .apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + type = "audio/*" + } + if (showDetailsDialog) { MusicDetailsDialog( music = music, @@ -370,6 +371,12 @@ fun MusicListItem( ) } + if (showShareOptions) { + BasicAlertDialog( + onDismissRequest = { showShareOptions = false } + ) { ShareOptionsContent() } + } + Row( @@ -502,6 +509,27 @@ fun MusicListItem( ) } ) + DropdownMenuItem( + onClick = { + context.startActivity( + Intent.createChooser( + shareIntent, + null + ) + ) + }, + text = { + CuteText( + text = stringResource(R.string.share) + ) + }, + leadingIcon = { + Icon( + imageVector = Icons.Rounded.Share, + contentDescription = null + ) + } + ) DropdownMenuItem( onClick = { onDeleteMusic(listOf(uri), deleteSongLauncher) }, text = { diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/components/ShareOptionsContent.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/components/ShareOptionsContent.kt new file mode 100644 index 0000000..4e81f31 --- /dev/null +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/components/ShareOptionsContent.kt @@ -0,0 +1,97 @@ +package com.sosauce.cutemusic.ui.screens.main.components + +import androidx.compose.foundation.layout.Arrangement +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Android +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.res.painterResource +import androidx.compose.ui.unit.dp +import com.sosauce.cutemusic.R +import com.sosauce.cutemusic.ui.shared_components.CuteText + + +@Composable +fun ShareOptionsContent() { + Column { + Card( + onClick = {}, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer + ), + shape = RoundedCornerShape( + topStart = 24.dp, + topEnd = 24.dp, + bottomStart = 4.dp, + bottomEnd = 4.dp + ), + modifier = Modifier + .fillMaxWidth() + ) { + Row( + modifier = Modifier.padding( + start = 10.dp, + top = 25.dp, + bottom = 25.dp + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Rounded.Android, + contentDescription = null + ) + Spacer(Modifier.width(10.dp)) + CuteText("Android Share Menu") + } + } + Spacer(Modifier.height(4.dp)) + Card( + onClick = {}, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer + ), + shape = RoundedCornerShape( + topStart = 4.dp, + topEnd = 4.dp, + bottomStart = 24.dp, + bottomEnd = 24.dp + ), + modifier = Modifier.fillMaxWidth(), + + ) { + Row( + modifier = Modifier.padding( + start = 10.dp, + top = 25.dp, + bottom = 25.dp + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.ribbon), + contentDescription = null, + modifier = Modifier + .size(24.dp) + ) + Spacer(Modifier.width(10.dp)) + CuteText("CuteShare") + } + } + } +} \ No newline at end of file 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 5b1f719..2597bb6 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 @@ -27,6 +27,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold 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 @@ -36,6 +37,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.media3.common.MediaItem import coil3.compose.AsyncImage import com.sosauce.cutemusic.R @@ -54,11 +56,13 @@ fun MetadataEditor( onEditMusic: (List, ActivityResultLauncher) -> Unit ) { + val metadataState by metadataViewModel.metadataState.collectAsStateWithLifecycle() + MetadataEditorContent( music = music, onPopBackStack = onPopBackStack, onNavigate = onNavigate, - metadataState = metadataViewModel.metadataState, + metadataState = metadataState, onMetadataAction = { metadataViewModel.onHandleMetadataActions(it) }, //vm = metadataViewModel, onEditMusic = onEditMusic @@ -77,7 +81,6 @@ fun MetadataEditorContent( ) { val context = LocalContext.current val uri = Uri.parse(music.mediaMetadata.extras?.getString("uri")) - val path = music.mediaMetadata.extras?.getString("path") // var selectedImageUri by remember { mutableStateOf(null) } // // val photoPickerLauncher = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { @@ -90,13 +93,13 @@ fun MetadataEditorContent( contract = ActivityResultContracts.StartIntentSenderForResult() ) { if (it.resultCode == Activity.RESULT_OK) { - onMetadataAction(MetadataActions.SaveChanges(path!!)) + onMetadataAction(MetadataActions.SaveChanges) Toast.makeText( context, context.getString(R.string.success), Toast.LENGTH_SHORT ).show() - onPopBackStack() + //onPopBackStack() } else { Toast.makeText( context, diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataState.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataState.kt index 27857d7..1780942 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataState.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataState.kt @@ -5,5 +5,6 @@ import androidx.compose.runtime.snapshots.SnapshotStateList data class MetadataState( val mutablePropertiesMap: SnapshotStateList = mutableStateListOf(), + val songPath: String = "" //var art: Artwork? = null ) \ No newline at end of file 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 17f2168..0f7c9e6 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 @@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.jaudiotagger.audio.AudioFileIO import org.jaudiotagger.tag.FieldKey -import org.jaudiotagger.tag.TagOptionSingleton import java.io.File class MetadataViewModel( @@ -19,11 +18,8 @@ class MetadataViewModel( ) : AndroidViewModel(application) { private val _metadata = MutableStateFlow(MetadataState()) - val metadataState = _metadata.asStateFlow().value + val metadataState = _metadata.asStateFlow() - init { - TagOptionSingleton.getInstance().isAndroid = true - } override fun onCleared() { super.onCleared() @@ -32,7 +28,8 @@ class MetadataViewModel( private fun loadMetadataJAudio(path: String) { - val audioFile = AudioFileIO.read(File(path)) + val audioFile = AudioFileIO + .read(File(path)) audioFile.tag.apply { val tagList = listOf( @@ -59,8 +56,9 @@ class MetadataViewModel( try { val file = File(path) val audioFile = AudioFileIO.read(file) + audioFile.tag.apply { - val tagList = mapOf( + mapOf( FieldKey.TITLE to 0, FieldKey.ARTIST to 1, FieldKey.ALBUM to 2, @@ -70,12 +68,14 @@ class MetadataViewModel( FieldKey.DISC_NO to 6, FieldKey.LYRICS to 7 ) - tagList.forEach { - setField(it.key, _metadata.value.mutablePropertiesMap[it.value]) - } - //setField(ArtworkFactory.) - AudioFileIO.write(audioFile) + .forEach { + Log.d("Test", _metadata.value.mutablePropertiesMap[it.value]) + setField(it.key, _metadata.value.mutablePropertiesMap[it.value]) + } } + + AudioFileIO.write(audioFile) + MediaScannerConnection.scanFile( application.applicationContext, arrayOf(file.toString()), @@ -99,13 +99,16 @@ class MetadataViewModel( when (action) { is MetadataActions.SaveChanges -> { viewModelScope.launch { - saveAllChanges(action.path) + saveAllChanges(metadataState.value.songPath) } } is MetadataActions.LoadSong -> { viewModelScope.launch { - loadMetadataJAudio(action.path) + _metadata.value = _metadata.value.copy( + songPath = action.path + ) + loadMetadataJAudio(metadataState.value.songPath) } } @@ -114,7 +117,6 @@ class MetadataViewModel( } } } - } 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 e6983af..e9516a4 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 @@ -70,10 +70,11 @@ fun SharedTransitionScope.NowPlayingLandscape( if (showSpeedCard) { SpeedCard( - viewModel = viewModel, onDismiss = { showSpeedCard = false }, shouldSnap = snap, - onChangeSnap = { snap = !snap } + onChangeSnap = { snap = !snap }, + musicState = musicState, + onHandlePlayerAction = 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 519497b..eec35a2 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 @@ -1,4 +1,4 @@ -@file:kotlin.OptIn(ExperimentalSharedTransitionApi::class) +@file:OptIn(ExperimentalSharedTransitionApi::class) package com.sosauce.cutemusic.ui.screens.playing @@ -131,10 +131,11 @@ private fun SharedTransitionScope.NowPlayingContent( if (showSpeedCard) { SpeedCard( - viewModel = viewModel, onDismiss = { showSpeedCard = false }, shouldSnap = snap, - onChangeSnap = { snap = !snap } + onChangeSnap = { snap = !snap }, + musicState = musicState, + onHandlePlayerAction = onEvent ) } Column( @@ -158,13 +159,6 @@ private fun SharedTransitionScope.NowPlayingContent( imageVector = Icons.Rounded.KeyboardArrowDown, contentDescription = null, modifier = Modifier - .sharedElement( - state = rememberSharedContentState(key = "arrow"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } - ) .size(28.dp) ) } 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 bed1b14..015fb3f 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 @@ -66,7 +66,7 @@ fun LoopButton( onClick = { shouldApplyLoop = !isLooping onClick() - scope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.Main) { rotation.animateTo( targetValue = -360f, animationSpec = tween(1000) @@ -116,8 +116,6 @@ fun SharedTransitionScope.ActionsButtonsRow( animatedVisibilityScope: AnimatedVisibilityScope, musicState: MusicState ) { - - val leftIconOffsetX = remember { Animatable(0f) } val rightIconOffsetX = remember { Animatable(0f) } val scope = rememberCoroutineScope() @@ -192,7 +190,7 @@ fun SharedTransitionScope.ActionsButtonsRow( } else { onEvent(PlayerActions.SeekToPreviousMusic) } - scope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.Main) { leftIconOffsetX.animateTo( targetValue = -20f, animationSpec = tween(250) @@ -327,7 +325,7 @@ fun SharedTransitionScope.ActionsButtonsRow( IconButton( onClick = { onEvent(PlayerActions.SeekToNextMusic) - scope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.Main) { rightIconOffsetX.animateTo( targetValue = 20f, animationSpec = tween(250) @@ -361,6 +359,257 @@ fun SharedTransitionScope.ActionsButtonsRow( } } + Crossfade( + targetState = showLongPressMenuPlus, + label = "" + ) { + if (it) { + IconButton( + onClick = { + onEvent(PlayerActions.SeekTo(10000)) + showLongPressMenuPlus = false + } + ) { CuteText("+10") } + } else { + LoopButton( + onClick = onClickLoop, + isLooping = musicState.isLooping + ) + } + } + } + } +} + +@Composable +fun ActionsButtonsRowQuickPlay( + onClickLoop: () -> Unit, + onClickShuffle: () -> Unit, + onEvent: (PlayerActions) -> Unit, + musicState: MusicState +) { + val leftIconOffsetX = remember { Animatable(0f) } + val rightIconOffsetX = remember { Animatable(0f) } + val scope = rememberCoroutineScope() + var showLongPressMenuPlus by remember { mutableStateOf(false) } + var showLongPressMenuMinus by remember { mutableStateOf(false) } + val colorMinus by animateColorAsState( + targetValue = if (showLongPressMenuMinus) MaterialTheme.colorScheme.surfaceContainer else MaterialTheme.colorScheme.background, + label = "", + animationSpec = tween(300) + ) + val colorPlus by animateColorAsState( + targetValue = if (showLongPressMenuPlus) MaterialTheme.colorScheme.surfaceContainer else MaterialTheme.colorScheme.background, + label = "", + animationSpec = tween(300) + ) + + val roundedFAB by animateIntAsState( + targetValue = if (musicState.isCurrentlyPlaying) 30 else 50, + label = "FAB Shape" + ) + + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + Row( + modifier = Modifier + .padding(end = 5.dp) + .background( + color = colorMinus, + shape = RoundedCornerShape(24.dp) + ) + ) { + Crossfade( + targetState = showLongPressMenuMinus, + label = "" + ) { + if (it) { + IconButton( + onClick = { + onEvent(PlayerActions.RewindTo(3000)) + showLongPressMenuMinus = false + } + ) { CuteText("-3") } + } else { + ShuffleButton( + onClick = onClickShuffle, + isShuffling = musicState.isShuffling + ) + } + } + + Crossfade( + targetState = showLongPressMenuMinus, + label = "" + ) { + if (it) { + IconButton( + onClick = { + onEvent(PlayerActions.RewindTo(5000)) + showLongPressMenuMinus = false + } + ) { CuteText("-5") } + } else { + IconButton( + onClick = { + if (musicState.currentPosition >= 10000) { + onEvent(PlayerActions.RestartSong) + } else { + onEvent(PlayerActions.SeekToPreviousMusic) + } + scope.launch(Dispatchers.Main) { + leftIconOffsetX.animateTo( + targetValue = -20f, + animationSpec = tween(250) + ) + leftIconOffsetX.animateTo( + targetValue = 0f, + animationSpec = tween(250) + ) + } + } + ) { + Crossfade( + targetState = musicState.currentPosition >= 10000, + label = "" + ) { + if (!it) { + Icon( + imageVector = Icons.Rounded.SkipPrevious, + contentDescription = null, + modifier = Modifier + .offset { + IntOffset( + x = leftIconOffsetX.value.toInt(), + y = 0 + ) + } + ) + } else { + Icon( + imageVector = Icons.Rounded.RestartAlt, + contentDescription = null + ) + } + } + } + } + } + + Crossfade( + targetState = showLongPressMenuMinus, + label = "" + ) { + if (it) { + IconButton( + onClick = { + onEvent(PlayerActions.RewindTo(10000)) + showLongPressMenuMinus = false + } + ) { CuteText("-10") } + } else { + CuteIconButton( + onClick = { onEvent(PlayerActions.RewindTo(5000)) }, + onLongClick = { showLongPressMenuMinus = true } + ) { + Icon( + imageVector = Icons.Rounded.FastRewind, + contentDescription = null + ) + } + } + } + } + FloatingActionButton( + onClick = { onEvent(PlayerActions.PlayOrPause) }, + shape = RoundedCornerShape(roundedFAB) + ) { + Icon( + imageVector = if (musicState.isCurrentlyPlaying) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, + contentDescription = "pause/play button", + ) + } + Row( + modifier = Modifier + .padding(start = 5.dp) + .background( + color = colorPlus, + shape = RoundedCornerShape(24.dp) + + ) + ) { + + Crossfade( + targetState = showLongPressMenuPlus, + label = "" + ) { + if (it) { + IconButton( + onClick = { + onEvent(PlayerActions.SeekTo(3000)) + showLongPressMenuPlus = false + } + ) { CuteText("+3") } + } else { + CuteIconButton( + onClick = { onEvent(PlayerActions.SeekTo(5000)) }, + onLongClick = { showLongPressMenuPlus = true } + ) { + Icon( + imageVector = Icons.Rounded.FastForward, + contentDescription = null + ) + } + } + } + + Crossfade( + targetState = showLongPressMenuPlus, + label = "" + ) { + if (it) { + IconButton( + onClick = { + onEvent(PlayerActions.SeekTo(5000)) + showLongPressMenuPlus = false + } + ) { CuteText("+5") } + } else { + IconButton( + onClick = { + onEvent(PlayerActions.SeekToNextMusic) + scope.launch(Dispatchers.Main) { + rightIconOffsetX.animateTo( + targetValue = 20f, + animationSpec = tween(250) + ) + rightIconOffsetX.animateTo( + targetValue = 0f, + animationSpec = tween(250) + ) + } + } + ) { + Icon( + imageVector = Icons.Rounded.SkipNext, + contentDescription = null, + modifier = Modifier + .offset { + IntOffset( + x = rightIconOffsetX.value.toInt(), + y = 0 + ) + } + ) + } + } + } + Crossfade( targetState = showLongPressMenuPlus, label = "" 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 8a79021..7181b7c 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,5 +1,7 @@ package com.sosauce.cutemusic.ui.screens.playing.components +import android.content.Intent +import android.net.Uri import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -11,6 +13,7 @@ import androidx.compose.material.icons.rounded.Album import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.MoreVert import androidx.compose.material.icons.rounded.Person +import androidx.compose.material.icons.rounded.Share import androidx.compose.material.icons.rounded.Speed import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem @@ -23,6 +26,7 @@ import androidx.compose.runtime.remember 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 com.sosauce.cutemusic.R @@ -40,9 +44,10 @@ fun QuickActionsRow( onNavigate: (Screen) -> Unit, onChargeArtistLists: (String) -> Unit ) { - + val context = LocalContext.current var isDropDownExpanded by remember { mutableStateOf(false) } var showDetailsDialog by remember { mutableStateOf(false) } + val uri = remember { Uri.parse(musicState.currentMusicUri) } if (showDetailsDialog) { MusicStateDetailsDialog( @@ -50,6 +55,13 @@ fun QuickActionsRow( onDismissRequest = { showDetailsDialog = false } ) } + val shareIntent = Intent() + .apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + type = "audio/*" + } Row( @@ -66,6 +78,7 @@ fun QuickActionsRow( contentDescription = "show lyrics" ) } + IconButton(onClick = onShowSpeedCard) { Icon( imageVector = Icons.Rounded.Speed, @@ -130,6 +143,20 @@ fun QuickActionsRow( ) } ) + DropdownMenuItem( + onClick = { context.startActivity(Intent.createChooser(shareIntent, null)) }, + text = { + CuteText( + text = stringResource(R.string.share) + ) + }, + leadingIcon = { + Icon( + imageVector = Icons.Rounded.Share, + contentDescription = null + ) + } + ) } } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/RateAdjustmentDialog.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/RateAdjustmentDialog.kt new file mode 100644 index 0000000..1d498b2 --- /dev/null +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/RateAdjustmentDialog.kt @@ -0,0 +1,89 @@ +package com.sosauce.cutemusic.ui.screens.playing.components + +import androidx.compose.foundation.layout.Arrangement +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.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.sosauce.cutemusic.R +import com.sosauce.cutemusic.ui.shared_components.CuteText + +@Composable +fun RateAdjustmentDialog( + rate: Float, + onSetNewRate: (Float) -> Unit, + titleText: String, + onDismissRequest: () -> Unit +) { + + var newRate by remember { mutableStateOf("%.2f".format(rate)) } + val focusRequest = remember { FocusRequester() } + + LaunchedEffect(Unit) { focusRequest.requestFocus() } + + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton(onClick = onDismissRequest) { + CuteText(stringResource(id = R.string.cancel)) + } + }, + title = { CuteText(titleText) }, + text = { + Column { + CuteText(stringResource(id = R.string.new_rate)) + Spacer(Modifier.height(10.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + OutlinedTextField( + value = newRate.toString(), + onValueChange = { newRate = it }, + singleLine = true, + keyboardActions = KeyboardActions( + onDone = { + if (newRate.toFloat() > 2.0f) { + onSetNewRate(2.0f) + } else if (newRate.toFloat() < 0.5f) { + onSetNewRate(0.5f) + } else { + onSetNewRate(newRate.toFloat()) + } + } + ), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number + ), + modifier = Modifier + .fillMaxWidth(0.5f) + .focusRequester(focusRequest), + textStyle = TextStyle( + textAlign = TextAlign.Center + ), + ) + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/Slider.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/Slider.kt index a02cba1..ee11465 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/Slider.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/Slider.kt @@ -18,8 +18,9 @@ import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.DpSize @@ -29,8 +30,8 @@ import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.data.datastore.rememberUseClassicSlider import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.MusicViewModel +import com.sosauce.cutemusic.utils.formatToReadableTime import me.saket.squiggles.SquigglySlider -import java.util.Locale @Composable fun MusicSlider( @@ -38,9 +39,10 @@ fun MusicSlider( musicState: MusicState ) { - val sliderPosition = rememberUpdatedState(musicState.currentPosition) val useClassicSlider by rememberUseClassicSlider() val interactionSource = remember { MutableInteractionSource() } + var tempSliderValue by remember { mutableStateOf(null) } + Column { Row( @@ -56,9 +58,7 @@ fun MusicSlider( ) .padding(horizontal = 8.dp, vertical = 4.dp) ) { - CuteText( - text = totalDuration(musicState.currentMusicDuration), - ) + CuteText(musicState.currentPosition.formatToReadableTime()) } Box( modifier = Modifier @@ -68,22 +68,22 @@ fun MusicSlider( ) .padding(horizontal = 8.dp, vertical = 4.dp) ) { - CuteText( - text = timeLeft(musicState.currentPosition) - ) + CuteText(musicState.currentMusicDuration.formatToReadableTime()) } } if (useClassicSlider) { Slider( - value = sliderPosition.value.toFloat(), - onValueChange = { - musicState.currentPosition = it.toLong() - viewModel.handlePlayerActions(PlayerActions.SeekToSlider(musicState.currentPosition)) - }, - valueRange = 0f..musicState.currentMusicDuration.toFloat(), + value = tempSliderValue ?: musicState.currentPosition.toFloat(), + onValueChange = { tempSliderValue = it }, onValueChangeFinished = { - viewModel.handlePlayerActions(PlayerActions.SeekToSlider(musicState.currentPosition)) + tempSliderValue?.let { + viewModel.handlePlayerActions(PlayerActions.UpdateCurrentPosition(it.toLong())) + viewModel.handlePlayerActions(PlayerActions.SeekToSlider(it.toLong())) + } + + tempSliderValue = null }, + valueRange = 0f..musicState.currentMusicDuration.toFloat(), modifier = Modifier.fillMaxWidth(), track = { sliderState -> SliderDefaults.Track( @@ -102,36 +102,19 @@ fun MusicSlider( ) } else { SquigglySlider( - value = sliderPosition.value.toFloat(), - onValueChange = { - musicState.currentPosition = it.toLong() - viewModel.handlePlayerActions(PlayerActions.SeekToSlider(musicState.currentPosition)) - }, - valueRange = 0f..musicState.currentMusicDuration.toFloat(), + value = tempSliderValue ?: musicState.currentPosition.toFloat(), + onValueChange = { tempSliderValue = it }, onValueChangeFinished = { - viewModel.handlePlayerActions(PlayerActions.SeekToSlider(musicState.currentPosition)) + tempSliderValue?.let { + viewModel.handlePlayerActions(PlayerActions.UpdateCurrentPosition(it.toLong())) + viewModel.handlePlayerActions(PlayerActions.SeekToSlider(it.toLong())) + } + + tempSliderValue = null }, - modifier = Modifier.fillMaxWidth() + valueRange = 0f..musicState.currentMusicDuration.toFloat(), + modifier = Modifier.fillMaxWidth(), ) } } } - - -fun totalDuration( - currentMusicDuration: Long -): String { - val totalSeconds = currentMusicDuration / 1000 - val minutes = totalSeconds / 60 - val seconds = totalSeconds % 60 - return String.format(Locale.getDefault(), "%d:%02d", minutes, seconds) -} - -fun timeLeft( - currentPosition: Long -): String { - val totalSeconds = currentPosition / 1000 - val minutes = totalSeconds / 60 - val seconds = totalSeconds % 60 - return String.format(Locale.getDefault(), "%d:%02d", minutes, seconds) -} 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 fc5dea3..c191e2e 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 @@ -3,6 +3,7 @@ package com.sosauce.cutemusic.ui.screens.playing.components import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -23,48 +24,101 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf +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.draw.clip 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.MusicState import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.ui.shared_components.CuteText -import com.sosauce.cutemusic.ui.shared_components.MusicViewModel @Composable fun SpeedCard( onDismiss: () -> Unit, - viewModel: MusicViewModel, shouldSnap: Boolean, - onChangeSnap: () -> Unit + onChangeSnap: () -> Unit, + musicState: MusicState, + onHandlePlayerAction: (PlayerActions) -> Unit ) { - var speed by remember { mutableFloatStateOf(viewModel.getPlaybackSpeed().speed) } - var pitch by remember { mutableFloatStateOf(viewModel.getPlaybackSpeed().pitch) } + var speed by remember { mutableFloatStateOf(musicState.playbackParameters.speed) } + var pitch by remember { mutableFloatStateOf(musicState.playbackParameters.pitch) } + var showSpeedAndPitchChangerDialog by remember { mutableStateOf(false) } + var showSpeedChangerDialog by remember { mutableStateOf(false) } + var showPitchChangerDialog by remember { mutableStateOf(false) } val interactionSource = remember { MutableInteractionSource() } + if (showSpeedAndPitchChangerDialog) { + RateAdjustmentDialog( + rate = speed, + onSetNewRate = { + speed = it + pitch = it + onHandlePlayerAction( + PlayerActions.ApplyPlaybackSpeed( + speed = speed, + pitch = pitch + ) + ) + showSpeedAndPitchChangerDialog = false + }, + titleText = stringResource(id = R.string.set_sap), + onDismissRequest = { showSpeedAndPitchChangerDialog = false } + ) + } + + if (showSpeedChangerDialog) { + RateAdjustmentDialog( + rate = speed, + onSetNewRate = { + speed = it + onHandlePlayerAction( + PlayerActions.ApplyPlaybackSpeed( + speed = speed, + pitch = pitch + ) + ) + showSpeedChangerDialog = false + }, + titleText = stringResource(id = R.string.set_speed), + onDismissRequest = { showSpeedChangerDialog = false }) + } + + if (showPitchChangerDialog) { + RateAdjustmentDialog( + rate = pitch, + onSetNewRate = { + speed = it + onHandlePlayerAction( + PlayerActions.ApplyPlaybackSpeed( + speed = speed, + pitch = pitch + ) + ) + showPitchChangerDialog = false + }, + titleText = stringResource(id = R.string.set_pitch), + onDismissRequest = { showPitchChangerDialog = false } + ) + } + + AlertDialog( onDismissRequest = { onDismiss() }, confirmButton = { TextButton(onClick = { onDismiss() }) { - CuteText( - text = stringResource(id = R.string.okay), - - ) + CuteText(text = stringResource(id = R.string.okay)) } }, - title = { - CuteText( - text = stringResource(id = R.string.playback_speed), - - ) - }, + title = { CuteText(stringResource(id = R.string.playback_speed)) }, text = { Box { Column { @@ -72,14 +126,15 @@ fun SpeedCard( if (!shouldSnap) { Box( modifier = Modifier - .background( - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), - shape = RoundedCornerShape(11.dp) - ) + .clip(RoundedCornerShape(11.dp)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)) + .clickable { showSpeedChangerDialog = true } .padding(horizontal = 8.dp, vertical = 4.dp) ) { CuteText( - text = "Speed: " + "%.2f".format(speed), + text = "${stringResource(id = R.string.speed)}: " + "%.2f".format( + speed + ), color = MaterialTheme.colorScheme.onBackground, fontSize = 15.sp ) @@ -88,7 +143,7 @@ fun SpeedCard( value = speed, onValueChange = { speed = it - viewModel.handlePlayerActions( + onHandlePlayerAction( PlayerActions.ApplyPlaybackSpeed( speed = speed, pitch = pitch @@ -116,14 +171,15 @@ fun SpeedCard( // Box( modifier = Modifier - .background( - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), - shape = RoundedCornerShape(11.dp) - ) + .clip(RoundedCornerShape(11.dp)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)) + .clickable { showPitchChangerDialog = true } .padding(horizontal = 8.dp, vertical = 4.dp) ) { CuteText( - text = "Pitch: " + "%.2f".format(pitch), + text = "${stringResource(id = R.string.pitch)}: " + "%.2f".format( + pitch + ), color = MaterialTheme.colorScheme.onBackground ) } @@ -131,7 +187,7 @@ fun SpeedCard( value = pitch, onValueChange = { pitch = it - viewModel.handlePlayerActions( + onHandlePlayerAction( PlayerActions.ApplyPlaybackSpeed( speed = speed, pitch = pitch @@ -164,7 +220,7 @@ fun SpeedCard( onValueChange = { speed = it pitch = it - viewModel.handlePlayerActions( + onHandlePlayerAction( PlayerActions.ApplyPlaybackSpeed( speed = speed, pitch = pitch @@ -190,15 +246,14 @@ fun SpeedCard( ) Box( modifier = Modifier - .background( - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), - shape = RoundedCornerShape(11.dp) - ) + .clip(RoundedCornerShape(11.dp)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)) + .clickable { showSpeedAndPitchChangerDialog = true } .padding(horizontal = 8.dp, vertical = 4.dp), ) { CuteText( text = "%.2f".format(speed), - color = MaterialTheme.colorScheme.onBackground + color = MaterialTheme.colorScheme.onBackground, ) } } @@ -213,7 +268,7 @@ fun SpeedCard( onCheckedChange = { onChangeSnap() } ) CuteText( - text = "Snap speed and pitch" + text = stringResource(id = R.string.snap) ) } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/SettingsScreen.kt index bcda213..0f9ea8c 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/SettingsScreen.kt @@ -2,7 +2,9 @@ package com.sosauce.cutemusic.ui.screens.settings import android.annotation.SuppressLint import androidx.compose.foundation.layout.Column +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.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -11,6 +13,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import com.sosauce.cutemusic.R import com.sosauce.cutemusic.ui.navigation.Screen import com.sosauce.cutemusic.ui.screens.settings.compenents.AboutCard @@ -49,9 +52,24 @@ fun SettingsScreen( horizontalAlignment = Alignment.CenterHorizontally ) { AboutCard() + Spacer(Modifier.height(10.dp)) ThemeManagement() UISettings() Misc(onNavigateTo = onNavigate) +// SettingCategoryCards( +// text = "UI & Theme", +// onClick = {}, +// topDp = 24.dp, +// bottomDp = 4.dp, +// icon = Icons.Rounded.ColorLens +// ) +// SettingCategoryCards( +// text = "Misc", +// onClick = {}, +// topDp = 4.dp, +// bottomDp = 24.dp, +// icon = Icons.Rounded.MiscellaneousServices +// ) } } } 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 9328df8..e871579 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 @@ -20,6 +20,7 @@ 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.graphics.vector.ImageVector import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -133,4 +134,50 @@ fun TextSettingsCards( } } } +} + +@Composable +fun SettingCategoryCards( + modifier: Modifier = Modifier, + text: String, + onClick: () -> Unit, + topDp: Dp, + bottomDp: Dp, + icon: ImageVector, +) { + Card( + shape = RoundedCornerShape( + topStart = topDp, + topEnd = topDp, + bottomStart = bottomDp, + bottomEnd = bottomDp + ), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 2.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer + ), + onClick = onClick + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = modifier + .padding( + top = 24.dp, + bottom = 24.dp, + start = 10.dp + ) + ) { + Icon( + imageVector = icon, + contentDescription = null + ) + CuteText( + text = text, + modifier = Modifier.padding(start = 5.dp) + ) + } + } } \ No newline at end of file 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 494f998..84c9683 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 @@ -19,6 +19,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.sosauce.cutemusic.R import com.sosauce.cutemusic.data.datastore.rememberFollowSys +import com.sosauce.cutemusic.data.datastore.rememberShowXButton import com.sosauce.cutemusic.data.datastore.rememberUseAmoledMode import com.sosauce.cutemusic.data.datastore.rememberUseArtTheme import com.sosauce.cutemusic.data.datastore.rememberUseClassicSlider @@ -33,7 +34,6 @@ fun Misc( onNavigateTo: (Screen) -> Unit ) { val context = LocalContext.current - var useSystemFont by rememberUseSystemFont() //var killService by remember { rememberKillService(context) } Column { @@ -62,13 +62,6 @@ fun Misc( // bottomDp = 4.dp, // text = "Kill Service" // ) - SettingsCards( - checked = useSystemFont, - onCheckedChange = { useSystemFont = !useSystemFont }, - topDp = 4.dp, - bottomDp = 4.dp, - text = stringResource(id = R.string.use_sys_font) - ) TextSettingsCards( text = stringResource(id = R.string.restart_app), tipText = stringResource(id = R.string.restart_app_why), @@ -130,8 +123,9 @@ fun ThemeManagement() { @Composable fun UISettings() { - var useArtTheme by rememberUseArtTheme() var useClassicSlider by rememberUseClassicSlider() + var useSystemFont by rememberUseSystemFont() + var showXButton by rememberShowXButton() Column { CuteText( @@ -139,27 +133,41 @@ fun UISettings() { color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(horizontal = 34.dp, vertical = 8.dp) ) + SettingsCards( + checked = useClassicSlider, + onCheckedChange = { useClassicSlider = !useClassicSlider }, + topDp = 24.dp, + bottomDp = 4.dp, + text = stringResource(id = R.string.classic_slider), + ) // SettingsCards( // checked = useArtTheme, // onCheckedChange = { useArtTheme = !useArtTheme }, -// topDp = 24.dp, -// bottomDp = 24.dp, +// topDp = 4.dp, +// bottomDp = 4.dp, // text = stringResource(id = R.string.use_art), // optionalDescription = { // CuteText( -// text = "App's theme will follow the currently playing music's art", +// text = "CuteSearchbar will have the current artwork as it's background.", // color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.85f), -// fontSize = 13.sp +// fontSize = 12.sp // // ) // } // ) SettingsCards( - checked = useClassicSlider, - onCheckedChange = { useClassicSlider = !useClassicSlider }, - topDp = 24.dp, + checked = showXButton, + onCheckedChange = { showXButton = !showXButton }, + topDp = 4.dp, + bottomDp = 4.dp, + text = stringResource(id = R.string.show_close_button) + ) + SettingsCards( + checked = useSystemFont, + onCheckedChange = { useSystemFont = !useSystemFont }, + topDp = 4.dp, bottomDp = 24.dp, - text = stringResource(id = R.string.classic_slider), + text = stringResource(id = R.string.use_sys_font) ) } } 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 62c4ff4..678c6d3 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 @@ -25,13 +25,16 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.media3.common.MediaItem +import androidx.media3.common.util.UnstableApi import coil3.compose.AsyncImage import com.sosauce.cutemusic.R import com.sosauce.cutemusic.data.MusicState import com.sosauce.cutemusic.utils.ImageUtils import com.sosauce.cutemusic.utils.formatBinarySize +import com.sosauce.cutemusic.utils.formatToReadableTime import com.sosauce.cutemusic.utils.getBitrate +@UnstableApi @Composable fun MusicDetailsDialog( music: MediaItem, @@ -104,12 +107,14 @@ fun MusicDetailsDialog( ) CuteText( text = "${stringResource(id = R.string.bitrate)}: $fileBitrate", - modifier = Modifier.padding(bottom = 5.dp) ) CuteText( text = "${stringResource(id = R.string.type)}: $fileType", - + modifier = Modifier.padding(bottom = 5.dp) + ) + CuteText( + text = "${stringResource(id = R.string.duration)}: ${music.mediaMetadata.durationMs?.formatToReadableTime() ?: 0}", modifier = Modifier.padding(bottom = 5.dp) ) } @@ -187,12 +192,10 @@ fun MusicStateDetailsDialog( ) CuteText( text = "${stringResource(id = R.string.bitrate)}: $fileBitrate", - modifier = Modifier.padding(bottom = 5.dp) ) CuteText( text = "${stringResource(id = R.string.type)}: $fileType", - modifier = Modifier.padding(bottom = 5.dp) ) } 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 adc6179..d2397bb 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 @@ -2,7 +2,6 @@ package com.sosauce.cutemusic.ui.shared_components import android.app.Application import android.content.ComponentName -import android.net.Uri import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -11,7 +10,9 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata +import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi import androidx.media3.session.MediaController import androidx.media3.session.SessionToken import com.google.common.util.concurrent.MoreExecutors @@ -48,6 +49,7 @@ class MusicViewModel( private val playerListener = object : Player.Listener { + @UnstableApi override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) { super.onMediaMetadataChanged(mediaMetadata) _musicState.value = _musicState.value.copy( @@ -57,15 +59,23 @@ class MusicViewModel( currentArt = mediaMetadata.artworkUri, currentPath = mediaMetadata.extras?.getString("path") ?: "No Path Found!", currentMusicUri = mediaMetadata.extras?.getString("uri") ?: "No Uri Found!", - currentLrcFile = loadLrcFile(musicState.value.currentPath), - currentLyrics = parseLrcFile(musicState.value.currentLrcFile), + currentLrcFile = getLrcFile(), currentAlbum = mediaMetadata.albumTitle.toString(), currentAlbumId = mediaMetadata.extras?.getLong("album_id") ?: 0, - currentSize = mediaMetadata.extras?.getLong("size") ?: 0 + currentSize = mediaMetadata.extras?.getLong("size") ?: 0, + currentMusicDuration = mediaMetadata.durationMs ?: 0, + currentLyrics = parseLyrics() ) } + override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { + super.onPlaybackParametersChanged(playbackParameters) + _musicState.value = _musicState.value.copy( + playbackParameters = playbackParameters + ) + } + override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) _musicState.value = _musicState.value.copy( @@ -88,7 +98,6 @@ class MusicViewModel( ) } - else -> { _musicState.value = _musicState.value.copy( isLooping = false @@ -119,7 +128,6 @@ class MusicViewModel( } } - init { MediaController .Builder( @@ -139,45 +147,39 @@ class MusicViewModel( }, MoreExecutors.directExecutor() ) + } } - private fun loadLrcFile(path: String): File? { - val lrcFilePath = path.replaceAfterLast('.', "lrc") + private fun getLrcFile(): File? { + val lrcFilePath = musicState.value.currentPath.replaceAfterLast('.', "lrc") val lrcFile = File(lrcFilePath) return if (lrcFile.exists()) lrcFile else null } - private fun parseLrcFile(file: File?): List { - val lyrics = mutableListOf() + fun parseLyrics(): List { + val file = musicState.value.currentLrcFile val regex = Regex("""\[(\d{2}):(\d{2})\.(\d{2})]""") - if (file == null) { - return emptyList() - } + if (file == null) return emptyList() - viewModelScope.launch { + return try { file.bufferedReader().useLines { lines -> - lines.forEach { line -> - val matchResult = regex.find(line) - if (matchResult != null) { + lines.asSequence().mapNotNull { line -> + regex.find(line)?.let { matchResult -> val (minutes, seconds, hundredths) = matchResult.destructured - val timeInMillis = + val millis = minutes.toLong() * 60_000 + seconds.toLong() * 1000 + hundredths.toLong() * 10 - val lyricText = line.substring(matchResult.range.last + 1).trim() - lyrics.add( - Lyrics( - timeInMillis, - lyricText - ) - ) + val lyric = line.substring(matchResult.range.last + 1).trim() + Lyrics(millis, lyric) } - } + }.toList() } + } catch (e: Exception) { + e.stackTrace + emptyList() } - - return lyrics } fun loadEmbeddedLyrics( @@ -202,8 +204,6 @@ class MusicViewModel( mediaController!!.release() } - fun getPlaybackSpeed() = mediaController!!.playbackParameters - fun isPlayerReady(): Boolean { return if (mediaController == null) false else @@ -214,18 +214,6 @@ class MusicViewModel( } } - fun quickPlay(uri: Uri?) { - mediaController!!.clearMediaItems() - uri?.let { MediaItem.fromUri(it) }?.let { - mediaController!!.setMediaItem( - it - ) - } - mediaController!!.prepare() - mediaController!!.play() - } - - fun handlePlayerActions(action: PlayerActions) { when (action) { is PlayerActions.RestartSong -> mediaController!!.seekTo(0) @@ -238,6 +226,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.StopPlayback -> mediaController!!.stop() is PlayerActions.ApplyPlaybackSpeed -> mediaController!!.applyPlaybackSpeed( action.speed, action.pitch @@ -264,6 +253,23 @@ class MusicViewModel( mediaController!!.playAtIndex(action.mediaId) } } + + is PlayerActions.UpdateCurrentPosition -> { + _musicState.value = _musicState.value.copy( + currentPosition = action.position + ) + } + + is PlayerActions.QuickPlay -> { + if (mediaController != null) { + mediaController!!.clearMediaItems() + mediaController!!.setMediaItem( + MediaItem.fromUri(action.uri) + ) + mediaController!!.prepare() + mediaController!!.play() + } + } } } } 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 efb175d..be74f92 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 @@ -16,25 +16,34 @@ 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.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.KeyboardArrowUp +import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.Pause import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material.icons.rounded.Shuffle import androidx.compose.material.icons.rounded.SkipNext import androidx.compose.material.icons.rounded.SkipPrevious +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton 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.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -43,10 +52,12 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.sosauce.cutemusic.data.actions.PlayerActions +import com.sosauce.cutemusic.data.datastore.rememberShowXButton import com.sosauce.cutemusic.utils.thenIf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch + @Composable fun SharedTransitionScope.CuteSearchbar( modifier: Modifier = Modifier, @@ -62,184 +73,209 @@ fun SharedTransitionScope.CuteSearchbar( isPlayerReady: Boolean, onNavigate: () -> Unit = {}, showSearchField: Boolean = true, + onClickFAB: () -> Unit ) { + val focusManager = LocalFocusManager.current - val roundedShape = 24.dp + val roundedShape = remember { 24.dp } val leftIconOffsetX = remember { Animatable(0f) } val rightIconOffsetX = remember { Animatable(0f) } val scope = rememberCoroutineScope() + var showXButton by rememberShowXButton() Column( modifier = modifier - .clip(RoundedCornerShape(roundedShape)) - .background(MaterialTheme.colorScheme.surface) - .border( - width = 1.dp, - color = MaterialTheme.colorScheme.surfaceContainer, - shape = RoundedCornerShape(roundedShape) - ) - .thenIf( - isPlayerReady, - Modifier.clickable { - onNavigate() - } - ) ) { - AnimatedVisibility( - visible = isPlayerReady, - enter = fadeIn() + slideInVertically( - animationSpec = tween(500), - initialOffsetY = { it } + FloatingActionButton( + onClick = onClickFAB, + modifier = Modifier + .defaultMinSize( + minWidth = 45.dp, + minHeight = 45.dp + ) + .align(Alignment.End) + ) { + Icon( + imageVector = Icons.Rounded.Shuffle, + contentDescription = null ) + } + Spacer(Modifier.height(5.dp)) + Column( + modifier = Modifier + .clip(RoundedCornerShape(roundedShape)) + .background(MaterialTheme.colorScheme.surface) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.surfaceContainer, + shape = RoundedCornerShape(roundedShape) + ) + .thenIf( + isPlayerReady, + Modifier.clickable { + onNavigate() + } + ) ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(start = 15.dp) - .fillMaxWidth(), + AnimatedVisibility( + visible = isPlayerReady, + enter = fadeIn() + slideInVertically( + animationSpec = tween(500), + initialOffsetY = { it } + ) ) { Row( - modifier = Modifier.weight(1f) + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(start = 15.dp) + .fillMaxWidth(), ) { - Icon( - imageVector = Icons.Rounded.KeyboardArrowUp, - contentDescription = null, - modifier = Modifier - .sharedElement( - state = rememberSharedContentState(key = "arrow"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(500) - } - ) - ) - CuteText( - text = currentlyPlaying, - modifier = Modifier - .padding(start = 10.dp) - .sharedElement( - state = rememberSharedContentState(key = "currentlyPlaying"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(500) - } - ) - .basicMarquee() - - ) - } - Row { - IconButton( - onClick = { - onHandlePlayerActions(PlayerActions.SeekToPreviousMusic) - scope.launch(Dispatchers.IO) { - leftIconOffsetX.animateTo( - targetValue = -20f, - animationSpec = tween(250) - ) - leftIconOffsetX.animateTo( - targetValue = 0f, - animationSpec = tween(250) + Row( + modifier = Modifier.weight(1f) + ) { + if (showXButton) { + IconButton( + onClick = { onHandlePlayerActions(PlayerActions.StopPlayback) }, + modifier = Modifier.size(22.dp) + ) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = null, ) } } - ) { - Icon( - imageVector = Icons.Rounded.SkipPrevious, - contentDescription = null, + CuteText( + text = currentlyPlaying, modifier = Modifier - .offset { - IntOffset( - x = leftIconOffsetX.value.toInt(), - y = 0 - ) - } + .padding(start = 5.dp) .sharedElement( - state = rememberSharedContentState(key = "skipPreviousButton"), + state = rememberSharedContentState(key = "currentlyPlaying"), animatedVisibilityScope = animatedVisibilityScope, boundsTransform = { _, _ -> tween(500) } ) + .basicMarquee() + ) } - IconButton( - onClick = { onHandlePlayerActions(PlayerActions.PlayOrPause) } - ) { - Icon( - imageVector = if (isPlaying) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, - contentDescription = null, - modifier = Modifier.sharedElement( - state = rememberSharedContentState(key = "playPauseIcon"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(500) + Row { + IconButton( + onClick = { + onHandlePlayerActions(PlayerActions.SeekToPreviousMusic) + scope.launch(Dispatchers.Main) { + leftIconOffsetX.animateTo( + targetValue = -20f, + animationSpec = tween(250) + ) + leftIconOffsetX.animateTo( + targetValue = 0f, + animationSpec = tween(250) + ) } - ) - ) - } - IconButton( - onClick = { - onHandlePlayerActions(PlayerActions.SeekToNextMusic) - scope.launch(Dispatchers.IO) { - rightIconOffsetX.animateTo( - targetValue = 20f, - animationSpec = tween(250) - ) - rightIconOffsetX.animateTo( - targetValue = 0f, - animationSpec = tween(250) - ) } - } - ) { - Icon( - imageVector = Icons.Rounded.SkipNext, - contentDescription = null, - modifier = Modifier - .offset { - IntOffset( - x = rightIconOffsetX.value.toInt(), - y = 0 + ) { + Icon( + imageVector = Icons.Rounded.SkipPrevious, + contentDescription = null, + modifier = Modifier + .offset { + IntOffset( + x = leftIconOffsetX.value.toInt(), + y = 0 + ) + } + .sharedElement( + state = rememberSharedContentState(key = "skipPreviousButton"), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = { _, _ -> + tween(500) + } ) - } - .sharedElement( - state = rememberSharedContentState(key = "skipNextButton"), + ) + } + IconButton( + onClick = { onHandlePlayerActions(PlayerActions.PlayOrPause) } + ) { + Icon( + imageVector = if (isPlaying) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, + contentDescription = null, + modifier = Modifier.sharedElement( + state = rememberSharedContentState(key = "playPauseIcon"), animatedVisibilityScope = animatedVisibilityScope, boundsTransform = { _, _ -> tween(500) } ) - ) + ) + } + IconButton( + onClick = { + onHandlePlayerActions(PlayerActions.SeekToNextMusic) + scope.launch(Dispatchers.Main) { + rightIconOffsetX.animateTo( + targetValue = 20f, + animationSpec = tween(250) + ) + rightIconOffsetX.animateTo( + targetValue = 0f, + animationSpec = tween(250) + ) + } + } + ) { + Icon( + imageVector = Icons.Rounded.SkipNext, + contentDescription = null, + modifier = Modifier + .offset { + IntOffset( + x = rightIconOffsetX.value.toInt(), + y = 0 + ) + } + .sharedElement( + state = rememberSharedContentState(key = "skipNextButton"), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = { _, _ -> + tween(500) + } + ) + ) + } } } } - } - if (showSearchField) { - TextField( - value = query, - onValueChange = onQueryChange, - colors = TextFieldDefaults.colors( - unfocusedContainerColor = MaterialTheme.colorScheme.secondaryContainer.copy(0.5f), - focusedContainerColor = MaterialTheme.colorScheme.secondaryContainer.copy(0.5f), - disabledIndicatorColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - errorIndicatorColor = Color.Transparent - ), - shape = RoundedCornerShape(50.dp), - leadingIcon = leadingIcon, - trailingIcon = trailingIcon, - placeholder = placeholder, - singleLine = true, - modifier = Modifier - .fillMaxWidth() - .padding(6.dp), - keyboardActions = KeyboardActions( - onDone = { focusManager.clearFocus() } - ) + if (showSearchField) { + TextField( + value = query, + onValueChange = onQueryChange, + colors = TextFieldDefaults.colors( + unfocusedContainerColor = MaterialTheme.colorScheme.secondaryContainer.copy( + 0.5f + ), + focusedContainerColor = MaterialTheme.colorScheme.secondaryContainer.copy( + 0.5f + ), + disabledIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + errorIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(50.dp), + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + placeholder = placeholder, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(6.dp), + keyboardActions = KeyboardActions( + onDone = { focusManager.clearFocus() } + ) - ) + ) + } } } } @@ -248,3 +284,4 @@ fun SharedTransitionScope.CuteSearchbar( + diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/theme/Theme.kt b/app/src/main/java/com/sosauce/cutemusic/ui/theme/Theme.kt index 757ea1a..64319f2 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/theme/Theme.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/theme/Theme.kt @@ -215,4 +215,4 @@ val GlobalFont = FontFamily(Font(R.font.nunito)) //private fun calculateSeedColor(bitmap: ImageBitmap): Color { // val suitableColors = bitmap.themeColors(fallback = Color.Black) // return suitableColors.first() -//} \ 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 7c244b7..3de9a1c 100644 --- a/app/src/main/java/com/sosauce/cutemusic/utils/Extensions.kt +++ b/app/src/main/java/com/sosauce/cutemusic/utils/Extensions.kt @@ -144,12 +144,20 @@ fun Uri.getBitrate(context: Context): String { val bitrate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE) bitrate?.toInt()?.div(1000)?.toString()?.plus(" kbps") ?: "Unknown" } catch (e: Exception) { + e.stackTrace "Error parsing bitrate!" } finally { retriever.release() } } +fun Long.formatToReadableTime(): String { + val totalSeconds = this / 1000 + val minutes = totalSeconds / 60 + val seconds = totalSeconds % 60 + return String.format(Locale.getDefault(), "%d:%02d", minutes, seconds) +} + @Composable fun rememberSearchbarAlignment( ): Alignment { diff --git a/app/src/main/java/com/sosauce/cutemusic/utils/ImageUtils.kt b/app/src/main/java/com/sosauce/cutemusic/utils/ImageUtils.kt index aa8f34b..4e3bc77 100644 --- a/app/src/main/java/com/sosauce/cutemusic/utils/ImageUtils.kt +++ b/app/src/main/java/com/sosauce/cutemusic/utils/ImageUtils.kt @@ -3,15 +3,15 @@ package com.sosauce.cutemusic.utils import android.content.ContentUris import android.content.Context import androidx.core.net.toUri -import coil3.request.ImageRequest import coil3.request.crossfade import coil3.request.transformations import coil3.transform.RoundedCornersTransformation import java.io.FileNotFoundException +import coil3.request.ImageRequest as ImageRequest3 object ImageUtils { - fun imageRequester(img: Any?, context: Context): ImageRequest { - val request = ImageRequest.Builder(context) + fun imageRequester(img: Any?, context: Context): ImageRequest3 { + val request = ImageRequest3.Builder(context) .data(img) .crossfade(true) .transformations( diff --git a/app/src/main/res/.DS_Store b/app/src/main/res/.DS_Store index 7c12205e6c295b012401afefe7593f0ec74c04d9..41c7bcda4a836c2fa05d08ecb0c2d05697b77db4 100644 GIT binary patch delta 49 zcmZp1XmQwJC& + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 800e81f..348d7cd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -60,4 +60,15 @@ Editor Details Use Classic Slider + Cancel + Set Speed and Pitch + Set Speed + Set Pitch + Speed + Pitch + Snap speed and pitch + Enter new rate between 0.5 and 2 + Share + Show Close Button on CuteSearchbar + Duration \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0484d7b..10eca25 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,23 +1,23 @@ [versions] -agp = "8.7.1" +agp = "8.7.2" jaudiotagger = "3.0.1" koinAndroid = "4.0.0" koinAndroidxCompose = "4.0.0" koinAndroidxStartup = "4.0.0" kotlin = "2.0.20" -activityCompose = "1.9.2" -coilCompose = "3.0.0-rc01" -composeBom = "2024.09.03" -composeAnimation = "1.7.3" -coreKtx = "1.13.1" +activityCompose = "1.9.3" +coilCompose = "3.0.0-rc02" +composeBom = "2024.10.01" +composeAnimation = "1.7.5" +coreKtx = "1.15.0" coreSplashscreen = "1.0.1" datastorePreferences = "1.1.1" kotlinxSerializationJson = "1.7.1" -lifecycleViewmodelCompose = "2.8.6" +lifecycleViewmodelCompose = "2.8.7" media3Common = "1.4.1" media3Exoplayer = "1.4.1" media3Session = "1.4.1" -navigationCompose = "2.8.2" +navigationCompose = "2.8.3" squigglyslider = "1.0.0" serialization = "2.0.0"