diff --git a/app/src/main/java/org/grakovne/lissen/player/MediaRepository.kt b/app/src/main/java/org/grakovne/lissen/player/MediaRepository.kt index f6fb2f5f..ebdf5f0a 100644 --- a/app/src/main/java/org/grakovne/lissen/player/MediaRepository.kt +++ b/app/src/main/java/org/grakovne/lissen/player/MediaRepository.kt @@ -8,8 +8,6 @@ import android.os.Looper import androidx.annotation.OptIn import androidx.core.content.ContextCompat import androidx.lifecycle.MutableLiveData -import androidx.media3.common.MediaItem -import androidx.media3.common.MediaMetadata import androidx.media3.common.Player import androidx.media3.common.Timeline import androidx.media3.common.util.UnstableApi @@ -21,23 +19,26 @@ import com.google.common.util.concurrent.MoreExecutors import dagger.hilt.android.qualifiers.ApplicationContext import org.grakovne.lissen.domain.DetailedBook import org.grakovne.lissen.player.service.AudioPlayerService -import org.grakovne.lissen.repository.ServerMediaRepository import javax.inject.Inject import javax.inject.Singleton @Singleton +@OptIn(UnstableApi::class) class MediaRepository @Inject constructor( - @ApplicationContext private val context: Context, - private val serverMediaRepository: ServerMediaRepository + @ApplicationContext private val context: Context ) { - private val sessionToken = - SessionToken(context, ComponentName(context, AudioPlayerService::class.java)) private lateinit var mediaController: MediaController + private val token = SessionToken( + context, + ComponentName(context, AudioPlayerService::class.java) + ) + val _isPlaying = MutableLiveData(false) val _currentPosition = MutableLiveData() + val _currentMediaItemIndex = MutableLiveData() private val handler = Handler(Looper.getMainLooper()) @@ -50,7 +51,7 @@ class MediaRepository } init { - val controllerBuilder = MediaController.Builder(context, sessionToken) + val controllerBuilder = MediaController.Builder(context, token) val futureController = controllerBuilder.buildAsync() Futures.addCallback( @@ -62,6 +63,7 @@ class MediaRepository controller.addListener(object : Player.Listener { override fun onPlaybackStateChanged(playbackState: Int) { _isPlaying.postValue(playbackState == Player.STATE_READY && controller.isPlaying) + _currentMediaItemIndex.postValue(mediaController.currentMediaItemIndex) when (controller.isPlaying) { true -> startUpdatingProgress() @@ -82,13 +84,19 @@ class MediaRepository oldPosition: Player.PositionInfo, newPosition: Player.PositionInfo, reason: Int - ) = _currentPosition.postValue(mediaController.currentPosition) + ) { + _currentMediaItemIndex.postValue(mediaController.currentMediaItemIndex) + _currentPosition.postValue(mediaController.currentPosition) + } override fun onTimelineChanged( timeline: Timeline, reason: Int - ) = _currentPosition.postValue(mediaController.currentPosition) + ) { + _currentMediaItemIndex.postValue(mediaController.currentMediaItemIndex) + _currentPosition.postValue(mediaController.currentPosition) + } }) } @@ -101,7 +109,6 @@ class MediaRepository } - @OptIn(UnstableApi::class) fun playAudio(book: DetailedBook) { val intent = Intent(context, AudioPlayerService::class.java).apply { action = AudioPlayerService.ACTION_START_FOREGROUND @@ -121,11 +128,48 @@ class MediaRepository ContextCompat.startForegroundService(context, intent) } - fun startUpdatingProgress() { + fun nextTrack() { + val nextIndex = mediaController.currentMediaItemIndex + 1 + val timeline = mediaController.currentTimeline + + if (nextIndex < timeline.windowCount) { + mediaController.seekTo(nextIndex, 0) + } + } + + fun setTrack(index: Int) { + val timeline = mediaController.currentTimeline + + if (index < timeline.windowCount && index >= 0) { + mediaController.seekTo(index, 0) + } + } + + fun seekTo(position: Float) { + stopUpdatingProgress() + + val duration = mediaController.duration + if (duration > 0) { + val newPosition = (1000 * position).toLong() + mediaController.seekTo(newPosition) + } + + startUpdatingProgress() + } + + fun previousTrack() { + val previousIndex = mediaController.currentMediaItemIndex - 1 + + if (previousIndex > 0) { + mediaController.seekTo(previousIndex, 0) + } + } + + private fun startUpdatingProgress() { handler.post(updateProgressAction) } - fun stopUpdatingProgress() { + private fun stopUpdatingProgress() { handler.removeCallbacks(updateProgressAction) } } \ No newline at end of file diff --git a/app/src/main/java/org/grakovne/lissen/ui/screens/player/composable/TrackControlComposable.kt b/app/src/main/java/org/grakovne/lissen/ui/screens/player/composable/TrackControlComposable.kt index d5f1cb00..2107a94d 100644 --- a/app/src/main/java/org/grakovne/lissen/ui/screens/player/composable/TrackControlComposable.kt +++ b/app/src/main/java/org/grakovne/lissen/ui/screens/player/composable/TrackControlComposable.kt @@ -23,8 +23,13 @@ import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +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.unit.dp @@ -43,8 +48,16 @@ fun TrackControlComposable( val book by viewModel.book.observeAsState() val chapters = book?.chapters ?: emptyList() + var sliderPosition by remember { mutableStateOf(0f) } + var isDragging by remember { mutableStateOf(false) } val duration = book?.chapters?.get(currentTrackIndex)?.duration?.toFloat() ?: 0f + val draggableTrackProgress by remember { + derivedStateOf { + if (!isDragging) currentPosition.toFloat() else sliderPosition + } + } + Column( modifier = modifier .fillMaxWidth() @@ -52,8 +65,15 @@ fun TrackControlComposable( ) { Slider( - value = currentPosition.toFloat(), - onValueChange = { newPosition -> viewModel.seekTo(newPosition) }, + value = draggableTrackProgress, + onValueChange = { newPosition -> + sliderPosition = newPosition + isDragging = true + }, + onValueChangeFinished = { + viewModel.seekTo(sliderPosition) + isDragging = false + }, valueRange = 0f..(duration), modifier = Modifier .height(36.dp) diff --git a/app/src/main/java/org/grakovne/lissen/viewmodel/PlayerViewModel.kt b/app/src/main/java/org/grakovne/lissen/viewmodel/PlayerViewModel.kt index 95b70d57..15e18462 100644 --- a/app/src/main/java/org/grakovne/lissen/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/org/grakovne/lissen/viewmodel/PlayerViewModel.kt @@ -26,10 +26,9 @@ class PlayerViewModel @Inject constructor( val playingQueueExpanded = _playingQueueExpanded val isPlaying = mediaRepository._isPlaying - val currentPosition = mediaRepository._currentPosition - private val _currentTrackIndex = MutableLiveData(0) + private val _currentTrackIndex = mediaRepository._currentMediaItemIndex val currentTrackIndex: LiveData = _currentTrackIndex fun togglePlayingQueue() { @@ -62,19 +61,19 @@ class PlayerViewModel @Inject constructor( } fun seekTo(position: Float) { - //_currentPosition.value = position + mediaRepository.seekTo(position) } fun setChapter(index: Int) { - _currentTrackIndex.value = index + mediaRepository.setTrack(index) } fun nextTrack() { - _currentTrackIndex.value = _currentTrackIndex.value?.plus(1) + mediaRepository.nextTrack() } fun previousTrack() { - _currentTrackIndex.value = _currentTrackIndex.value?.minus(1) + mediaRepository.previousTrack() } fun togglePlayPause() {