diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dfbd650..6501d8c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -125,9 +125,9 @@ dependencies { implementation(libs.bundles.ktor) // //---------------Media3---------------// -// implementation(libs.bundles.media3) -// implementation(libs.androidx.media3.datasource.okhttp) - + implementation(libs.bundles.media3) + implementation(libs.androidx.media3.datasource.okhttp) + implementation(project(":app:mediaplayer")) //---------------Dependency Injection---------------// implementation(libs.bundles.hilt) diff --git a/app/mediaplayer/build.gradle.kts b/app/mediaplayer/build.gradle.kts index 5dc1644..6ef6eaa 100644 --- a/app/mediaplayer/build.gradle.kts +++ b/app/mediaplayer/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.kotlinAndroid) alias(libs.plugins.kotlin.ksp) + alias(libs.plugins.hilt) } android { diff --git a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaService.kt b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaplayerService.kt similarity index 96% rename from app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaService.kt rename to app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaplayerService.kt index c7f7a5a..22d3055 100644 --- a/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaService.kt +++ b/app/mediaplayer/src/main/java/com/bobbyesp/mediaplayer/service/MediaplayerService.kt @@ -10,7 +10,7 @@ import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint -class MediaService : MediaSessionService() { +class MediaplayerService : MediaSessionService() { @Inject lateinit var mediaSession: MediaSession diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f7e097d..f0417a6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,6 +14,9 @@ android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" /> + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/metadator/MainActivity.kt b/app/src/main/java/com/bobbyesp/metadator/MainActivity.kt index cddd943..aef141d 100644 --- a/app/src/main/java/com/bobbyesp/metadator/MainActivity.kt +++ b/app/src/main/java/com/bobbyesp/metadator/MainActivity.kt @@ -1,5 +1,6 @@ package com.bobbyesp.metadator +import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -9,6 +10,7 @@ import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat +import com.bobbyesp.mediaplayer.service.MediaplayerService import com.bobbyesp.metadator.presentation.Navigator import com.bobbyesp.metadator.presentation.common.AppLocalSettingsProvider import com.bobbyesp.metadator.presentation.common.LocalDarkTheme @@ -18,6 +20,8 @@ import setupFirebase @AndroidEntryPoint class MainActivity : ComponentActivity() { + private var isMusicPlayerServiceStarted = false + @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() @@ -43,6 +47,19 @@ class MainActivity : ComponentActivity() { } } + override fun onDestroy() { + super.onDestroy() + stopService(Intent(this, MediaplayerService::class.java)) + isMusicPlayerServiceStarted = false + } + + fun startMediaPlayerService() { + if (!isMusicPlayerServiceStarted) { + isMusicPlayerServiceStarted = true + startService(Intent(this, MediaplayerService::class.java)) + } + } + companion object { private lateinit var activity: MainActivity fun getActivity(): MainActivity { diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/Navigation.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/Navigation.kt index 0fdee91..a0f7a6a 100644 --- a/app/src/main/java/com/bobbyesp/metadator/presentation/Navigation.kt +++ b/app/src/main/java/com/bobbyesp/metadator/presentation/Navigation.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -47,11 +48,13 @@ import com.bobbyesp.metadator.presentation.common.TagEditorParcelableSongParamTy import com.bobbyesp.metadator.presentation.common.routesToNavigate import com.bobbyesp.metadator.presentation.pages.MediaStorePageViewModel import com.bobbyesp.metadator.presentation.pages.home.HomePage +import com.bobbyesp.metadator.presentation.pages.mediaplayer.MediaplayerPage import com.bobbyesp.metadator.presentation.pages.utilities.tageditor.ID3MetadataEditorPage import com.bobbyesp.metadator.presentation.pages.utilities.tageditor.ID3MetadataEditorPageViewModel import com.bobbyesp.ui.motion.animatedComposable import com.bobbyesp.ui.motion.slideInVerticallyComposable import com.bobbyesp.utilities.navigation.getParcelable +import kotlinx.coroutines.launch @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable @@ -73,6 +76,8 @@ fun Navigator() { val snackbarHostState = LocalSnackbarHostState.current + val scope = rememberCoroutineScope() + val showSnackbarMessage: suspend (String) -> Unit = { message -> snackbarHostState.showSnackbar(message) } @@ -108,35 +113,47 @@ fun Navigator() { fontFamily = FontFamily.Monospace ) routesToNavigate.forEachIndexed { _, route -> - NavigationDrawerItem(label = { - Text(text = route.title?.let { stringResource(id = it) } ?: "") - }, selected = currentRootRoute.value == route.route, onClick = { - - }, icon = { - Icon(imageVector = route.icon!!, - contentDescription = route.title?.let { stringResource(id = it) }) - }, badge = { + NavigationDrawerItem( + label = { + Text(text = route.title?.let { stringResource(id = it) } ?: "") + }, selected = currentRootRoute.value == route.route, onClick = { + if (currentRootRoute.value == route.route) { + scope.launch { + drawerState.close() + } + return@NavigationDrawerItem + } else { + navController.navigate(route.route) { + popUpTo(navController.graph.startDestinationId) + launchSingleTop = true + } + scope.launch { + drawerState.close() + } + } + }, icon = { + Icon(imageVector = route.icon!!, + contentDescription = route.title?.let { stringResource(id = it) }) + }, badge = { - }, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) + }, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) ) } } }, ) { - Scaffold( - snackbarHost = { - SnackbarHost( - hostState = snackbarHostState - ) { dataReceived -> - Snackbar( - modifier = Modifier, - snackbarData = dataReceived, - containerColor = MaterialTheme.colorScheme.inverseSurface, - contentColor = MaterialTheme.colorScheme.inverseOnSurface, - ) - } + Scaffold(snackbarHost = { + SnackbarHost( + hostState = snackbarHostState + ) { dataReceived -> + Snackbar( + modifier = Modifier, + snackbarData = dataReceived, + containerColor = MaterialTheme.colorScheme.inverseSurface, + contentColor = MaterialTheme.colorScheme.inverseOnSurface, + ) } - ) { + }) { NavHost( modifier = Modifier .fillMaxWidth() @@ -154,6 +171,15 @@ fun Navigator() { } } + navigation( + startDestination = Route.MediaplayerNavigator.Mediaplayer.route, + route = Route.MediaplayerNavigator.route + ) { + animatedComposable(Route.MediaplayerNavigator.Mediaplayer.route) { + MediaplayerPage() + } + } + navigation( startDestination = Route.UtilitiesNavigator.TagEditor.route, route = Route.UtilitiesNavigator.route diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/common/Route.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/common/Route.kt index 8cf6a95..f1247ae 100644 --- a/app/src/main/java/com/bobbyesp/metadator/presentation/common/Route.kt +++ b/app/src/main/java/com/bobbyesp/metadator/presentation/common/Route.kt @@ -7,6 +7,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Edit import androidx.compose.material.icons.rounded.Handyman import androidx.compose.material.icons.rounded.Home +import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.ui.graphics.vector.ImageVector import com.bobbyesp.metadator.App.Companion.json import com.bobbyesp.metadator.R @@ -35,6 +36,20 @@ sealed class Route( } } + data object MediaplayerNavigator : Route( + "mediaplayer_navigator", + title = R.string.mediaplayer, + icon = Icons.Rounded.PlayArrow + ) { + data object Mediaplayer : + Route( + "mediaplayer", + title = R.string.mediaplayer, + icon = Icons.Rounded.PlayArrow + ) { + } + } + data object UtilitiesNavigator : Route( "utilities", @@ -61,6 +76,7 @@ sealed class Route( val routesToNavigate = listOf( Route.MetadatorNavigator, + Route.MediaplayerNavigator ) fun Route.getTitle(@ApplicationContext context: Context): String? { diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/components/cards/songs/HorizontalSongCard.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/components/cards/songs/HorizontalSongCard.kt index 32fe2d2..22d7ad3 100644 --- a/app/src/main/java/com/bobbyesp/metadator/presentation/components/cards/songs/HorizontalSongCard.kt +++ b/app/src/main/java/com/bobbyesp/metadator/presentation/components/cards/songs/HorizontalSongCard.kt @@ -1,6 +1,7 @@ package com.bobbyesp.metadator.presentation.components.cards.songs import android.content.res.Configuration +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -9,6 +10,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -20,6 +22,7 @@ import com.bobbyesp.metadator.presentation.components.image.ArtworkAsyncImage import com.bobbyesp.metadator.presentation.theme.MetadatorTheme import com.bobbyesp.model.Song import com.bobbyesp.ui.components.text.MarqueeText +import com.bobbyesp.utilities.Time @Composable fun HorizontalSongCard( @@ -43,7 +46,9 @@ fun HorizontalSongCard( artworkPath = song.artworkPath ) Column( - horizontalAlignment = Alignment.Start, modifier = Modifier.padding(8.dp) + horizontalAlignment = Alignment.Start, modifier = Modifier + .padding(vertical = 8.dp, horizontal = 6.dp) + .weight(1f) ) { MarqueeText( text = song.title, @@ -59,6 +64,19 @@ fun HorizontalSongCard( fontSize = 12.sp ) } + + Text( + text = Time.formatDuration(song.duration.toLong()), + style = MaterialTheme.typography.bodySmall, + fontSize = 12.sp, + modifier = Modifier + .padding(8.dp) + .background( + MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.1f), + MaterialTheme.shapes.small + ) + .padding(6.dp) + ) } } } diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/components/others/MediaplayerConstants.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/components/others/MediaplayerConstants.kt new file mode 100644 index 0000000..cbb6694 --- /dev/null +++ b/app/src/main/java/com/bobbyesp/metadator/presentation/components/others/MediaplayerConstants.kt @@ -0,0 +1,10 @@ +package com.bobbyesp.metadator.presentation.components.others + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +val CollapsedPlayerHeight = 64.dp + +val PlayerAnimationSpec = spring(stiffness = Spring.StiffnessMediumLow) \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/components/others/MediaplayerSheet.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/components/others/MediaplayerSheet.kt new file mode 100644 index 0000000..a24c95e --- /dev/null +++ b/app/src/main/java/com/bobbyesp/metadator/presentation/components/others/MediaplayerSheet.kt @@ -0,0 +1,22 @@ +package com.bobbyesp.metadator.presentation.components.others + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bobbyesp.ui.components.bottomsheet.draggable.DraggableBottomSheet +import com.bobbyesp.ui.components.bottomsheet.draggable.DraggableBottomSheetState + +@Composable +fun MediaplayerSheet(modifier: Modifier = Modifier, state: DraggableBottomSheetState) { + DraggableBottomSheet( + state = state, + collapsedContent = { + + }, + backgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation(NavigationBarDefaults.Elevation) + ) { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/MediaStoreViewModel.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/MediaStoreViewModel.kt index 4b5c536..a096ace 100644 --- a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/MediaStoreViewModel.kt +++ b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/MediaStoreViewModel.kt @@ -47,7 +47,7 @@ class MediaStorePageViewModel @Inject constructor( val songs = withContext(viewModelScope.coroutineContext + Dispatchers.IO) { async { - MediaStoreReceiver.getAllSongsFromMediaStore( + MediaStoreReceiver.getSongsBySearchTerm( applicationContext = context, ) }.await() @@ -70,7 +70,7 @@ class MediaStorePageViewModel @Inject constructor( ) { val songs = withContext(viewModelScope.coroutineContext + Dispatchers.IO) { async { - MediaStoreReceiver.getAllSongsFromMediaStore( + MediaStoreReceiver.getSongsBySearchTerm( applicationContext = context, ) }.await() @@ -95,7 +95,7 @@ class MediaStorePageViewModel @Inject constructor( val songs = withContext(viewModelScope.coroutineContext + Dispatchers.IO) { async { - MediaStoreReceiver.getAllSongsFromMediaStore( + MediaStoreReceiver.getSongsBySearchTerm( applicationContext = context, searchTerm = filter, filterType = filterType ) }.await() diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/home/HomePage.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/home/HomePage.kt index 21f658f..8a5bc95 100644 --- a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/home/HomePage.kt +++ b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/home/HomePage.kt @@ -35,7 +35,6 @@ import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -97,8 +96,6 @@ fun HomePage( mutableStateOf(false) } - val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() - val mediaStoreLazyGridState = rememberForeverLazyGridState(key = "lazyGrid") val mediaStoreLazyColumnState = rememberLazyListState() val pullState = rememberPullState() @@ -115,7 +112,8 @@ fun HomePage( val listIsFirstItemVisible by remember { derivedStateOf { mediaStoreLazyColumnState.firstVisibleItemIndex == 0 } } Scaffold( - modifier = modifier.fillMaxSize(), topBar = { + modifier = modifier.fillMaxSize(), + topBar = { CenterAlignedTopAppBar(navigationIcon = { IconButton(onClick = { scope.launch { @@ -175,7 +173,7 @@ fun HomePage( }) } } - }, scrollBehavior = scrollBehavior + } ) }, floatingActionButton = { when (desiredLayout) { diff --git a/app/src/main/java/com/bobbyesp/metadator/presentation/pages/mediaplayer/MediaplayerPage.kt b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/mediaplayer/MediaplayerPage.kt new file mode 100644 index 0000000..54af986 --- /dev/null +++ b/app/src/main/java/com/bobbyesp/metadator/presentation/pages/mediaplayer/MediaplayerPage.kt @@ -0,0 +1,51 @@ +package com.bobbyesp.metadator.presentation.pages.mediaplayer + +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.bobbyesp.metadator.presentation.components.others.CollapsedPlayerHeight +import com.bobbyesp.metadator.presentation.components.others.MediaplayerSheet +import com.bobbyesp.metadator.presentation.components.others.PlayerAnimationSpec +import com.bobbyesp.ui.components.bottomsheet.draggable.rememberDraggableBottomSheetState + +@Composable +fun MediaplayerPage() { + BoxWithConstraints( + modifier = Modifier.fillMaxSize() + ) { + val mediaPlayerSheetState = rememberDraggableBottomSheetState( + dismissedBound = 0.dp, + collapsedBound = CollapsedPlayerHeight, + expandedBound = maxHeight, + animationSpec = PlayerAnimationSpec, + ) + + Scaffold( + modifier = Modifier.fillMaxSize() + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(it) + ) { + val scope = rememberCoroutineScope() + + Button(onClick = { mediaPlayerSheetState.expandSoft() }) { + Text(text = "Expand soft") + } + } + } + + MediaplayerSheet( + state = mediaPlayerSheetState + ) + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a3016f8..b481c7e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,5 +44,6 @@ Warning Unsaved changes There are some metadata fields that has been changed and not saved. Do you really want to discard the changes? + Mediaplayer \ No newline at end of file diff --git a/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheet.kt b/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheet.kt index 6b37b34..4035d10 100644 --- a/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheet.kt +++ b/app/ui/src/main/java/com/bobbyesp/ui/components/bottomsheet/draggable/DraggableBottomSheet.kt @@ -51,7 +51,7 @@ import kotlinx.coroutines.launch /** * Bottom Sheet - * Modified from [ViMusic](https://github.com/vfsfitvnm/ViMusic) + * Improved version from [ViMusic](https://github.com/vfsfitvnm/ViMusic) */ @Composable fun DraggableBottomSheet( diff --git a/app/utilities/src/main/java/com/bobbyesp/utilities/Time.kt b/app/utilities/src/main/java/com/bobbyesp/utilities/Time.kt index a0e815d..b7ec641 100644 --- a/app/utilities/src/main/java/com/bobbyesp/utilities/Time.kt +++ b/app/utilities/src/main/java/com/bobbyesp/utilities/Time.kt @@ -70,7 +70,7 @@ object Time { fun formatDuration(duration: Long): String { val minutes: Long = duration / 60000 val seconds: Long = (duration % 60000) / 1000 - return String.format("%02d:%02d", minutes, seconds) + return String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds) } fun parseDateStringToLocalTime(dateString: String): String? { diff --git a/app/utilities/src/main/java/com/bobbyesp/utilities/mediastore/MediaStoreReceiver.kt b/app/utilities/src/main/java/com/bobbyesp/utilities/mediastore/MediaStoreReceiver.kt index 73b25b8..e067ab9 100644 --- a/app/utilities/src/main/java/com/bobbyesp/utilities/mediastore/MediaStoreReceiver.kt +++ b/app/utilities/src/main/java/com/bobbyesp/utilities/mediastore/MediaStoreReceiver.kt @@ -22,7 +22,7 @@ object MediaStoreReceiver { * @param applicationContext The application context. * @return A list of all the songs in the device. */ - fun getAllSongsFromMediaStore(applicationContext: Context): List { + fun getSongsBySearchTerm(applicationContext: Context): List { val contentResolver: ContentResolver = applicationContext.contentResolver val uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI @@ -72,7 +72,7 @@ object MediaStoreReceiver { return songs } - fun getAllSongsFromMediaStore( + fun getSongsBySearchTerm( applicationContext: Context, searchTerm: String?, filterType: MediaStoreFilterType? ): List { val contentResolver: ContentResolver = applicationContext.contentResolver