From 7720a7182581020ad3f98330e5f5d63fb4c10f98 Mon Sep 17 00:00:00 2001 From: Antoine Jaury Date: Fri, 19 Jun 2026 13:53:10 +0200 Subject: [PATCH] new: allow user to seek a position in the record --- .../gwenedeg/player/AndroidAudioPlayer.kt | 4 ++++ .../fr/ajaury/gwenedeg/player/AudioPlayer.kt | 2 ++ .../ajaury/gwenedeg/player/IosAudioPlayer.kt | 8 +++++++ .../ajaury/gwenedeg/player/JvmAudioPlayer.kt | 4 ++++ .../ajaury/gwenedeg/player/WebAudioPlayer.kt | 4 ++++ .../ajaury/gwenedeg/player/ui/PlayerScreen.kt | 24 ++++++++++++++++--- .../player/ui/viewmodel/PlayerViewModel.kt | 5 ++++ 7 files changed, 48 insertions(+), 3 deletions(-) diff --git a/core/audioplayer/src/androidMain/kotlin/fr/ajaury/gwenedeg/player/AndroidAudioPlayer.kt b/core/audioplayer/src/androidMain/kotlin/fr/ajaury/gwenedeg/player/AndroidAudioPlayer.kt index e98709f..6f1c040 100644 --- a/core/audioplayer/src/androidMain/kotlin/fr/ajaury/gwenedeg/player/AndroidAudioPlayer.kt +++ b/core/audioplayer/src/androidMain/kotlin/fr/ajaury/gwenedeg/player/AndroidAudioPlayer.kt @@ -49,6 +49,10 @@ class AndroidAudioPlayer( ?.milliseconds ?: 0.milliseconds + override fun seekTo(position: Duration) { + player.seekTo(position.inWholeMilliseconds) + } + override fun load(uri: String) { val mediaItem = MediaItem.fromUri(uri) player.setMediaItem(mediaItem) diff --git a/core/audioplayer/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/AudioPlayer.kt b/core/audioplayer/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/AudioPlayer.kt index 8dbcec4..227ad25 100644 --- a/core/audioplayer/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/AudioPlayer.kt +++ b/core/audioplayer/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/AudioPlayer.kt @@ -10,6 +10,8 @@ interface AudioPlayer { val duration: Duration + fun seekTo(position: Duration) + fun play() fun pause() diff --git a/core/audioplayer/src/iosMain/kotlin/fr/ajaury/gwenedeg/player/IosAudioPlayer.kt b/core/audioplayer/src/iosMain/kotlin/fr/ajaury/gwenedeg/player/IosAudioPlayer.kt index 92ce794..dc21583 100644 --- a/core/audioplayer/src/iosMain/kotlin/fr/ajaury/gwenedeg/player/IosAudioPlayer.kt +++ b/core/audioplayer/src/iosMain/kotlin/fr/ajaury/gwenedeg/player/IosAudioPlayer.kt @@ -13,7 +13,9 @@ import platform.AVFoundation.duration import platform.AVFoundation.pause import platform.AVFoundation.play import platform.AVFoundation.replaceCurrentItemWithPlayerItem +import platform.AVFoundation.seekToTime import platform.CoreMedia.CMTimeGetSeconds +import platform.CoreMedia.CMTimeMakeWithSeconds import platform.Foundation.NSNotificationCenter import platform.Foundation.NSOperationQueue import platform.Foundation.NSURL @@ -45,6 +47,12 @@ class IosAudioPlayer : AudioPlayer { return seconds?.seconds ?: 0.milliseconds } + @OptIn(ExperimentalForeignApi::class) + override fun seekTo(position: Duration) { + val seconds = position.inWholeSeconds + player?.seekToTime(CMTimeMakeWithSeconds(seconds.toDouble(), preferredTimescale = 1000)) + } + @OptIn(ExperimentalForeignApi::class) override fun load(uri: String) { release() diff --git a/core/audioplayer/src/jvmMain/kotlin/fr/ajaury/gwenedeg/player/JvmAudioPlayer.kt b/core/audioplayer/src/jvmMain/kotlin/fr/ajaury/gwenedeg/player/JvmAudioPlayer.kt index 061d277..6beb1ef 100644 --- a/core/audioplayer/src/jvmMain/kotlin/fr/ajaury/gwenedeg/player/JvmAudioPlayer.kt +++ b/core/audioplayer/src/jvmMain/kotlin/fr/ajaury/gwenedeg/player/JvmAudioPlayer.kt @@ -33,6 +33,10 @@ class JvmAudioPlayer : AudioPlayer { override val duration: Duration get() = (clip?.microsecondLength ?: 0L).microseconds + override fun seekTo(position: Duration) { + clip?.microsecondPosition = position.inWholeMicroseconds + } + override fun load(uri: String) { release() diff --git a/core/audioplayer/src/webMain/kotlin/fr/ajaury/gwenedeg/player/WebAudioPlayer.kt b/core/audioplayer/src/webMain/kotlin/fr/ajaury/gwenedeg/player/WebAudioPlayer.kt index 4949677..5f1a515 100644 --- a/core/audioplayer/src/webMain/kotlin/fr/ajaury/gwenedeg/player/WebAudioPlayer.kt +++ b/core/audioplayer/src/webMain/kotlin/fr/ajaury/gwenedeg/player/WebAudioPlayer.kt @@ -20,6 +20,10 @@ class WebAudioPlayer : AudioPlayer { override val duration: Duration get() = (audio?.duration?.takeIf { it.isFinite() } ?: 0.0).seconds + override fun seekTo(position: Duration) { + audio?.currentTime = position.inWholeMicroseconds.toDouble() + } + override fun load(uri: String) { release() diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/PlayerScreen.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/PlayerScreen.kt index f1d5190..db21512 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/PlayerScreen.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/PlayerScreen.kt @@ -18,14 +18,17 @@ import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector @@ -58,6 +61,7 @@ fun PlayerScreen( recordTitle = recordTitle, uiState = uiState, onMainPlayActionButtonClicked = viewModel::performMainPlayAction, + onSeek = viewModel::seekTo, onBackClicked = onBackClicked, ) } @@ -68,6 +72,7 @@ fun PlayerScreen( recordTitle: String, uiState: PlayerUiState, onMainPlayActionButtonClicked: () -> Unit, + onSeek: (progress: Float) -> Unit, onBackClicked: () -> Unit, ) { Scaffold( @@ -103,8 +108,18 @@ fun PlayerScreen( Spacer(modifier = Modifier.weight(1f)) - LinearProgressIndicator( - progress = { uiState.progress }, + var seekProgress by remember { mutableStateOf(null) } + Slider( + value = seekProgress ?: uiState.progress, + onValueChange = { seekProgress = it }, + onValueChangeFinished = { + seekProgress?.let { fraction -> + if (uiState.durationMs > 0) { + onSeek(fraction) + } + } + seekProgress = null + }, modifier = Modifier.fillMaxWidth(), ) @@ -145,6 +160,7 @@ private fun PlayerScreenPlayingPreview() { recordTitle = "Demat", uiState = PlayerUiState(playbackState = PlaybackState.PLAYING, positionMs = 1234, durationMs = 5000), onMainPlayActionButtonClicked = {}, + onSeek = {}, onBackClicked = {}, ) } @@ -158,6 +174,7 @@ private fun PlayerScreenPausedPreview() { recordTitle = "Demat", uiState = PlayerUiState(playbackState = PlaybackState.PAUSED, positionMs = 1234, durationMs = 5000), onMainPlayActionButtonClicked = {}, + onSeek = {}, onBackClicked = {}, ) } @@ -171,6 +188,7 @@ private fun PlayerScreenEndedPreview() { recordTitle = "Demat", uiState = PlayerUiState(playbackState = PlaybackState.ENDED, positionMs = 1234, durationMs = 5000), onMainPlayActionButtonClicked = {}, + onSeek = {}, onBackClicked = {}, ) } diff --git a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerViewModel.kt b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerViewModel.kt index 28d318d..29ae095 100644 --- a/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerViewModel.kt +++ b/shared/src/commonMain/kotlin/fr/ajaury/gwenedeg/player/ui/viewmodel/PlayerViewModel.kt @@ -81,6 +81,11 @@ class PlayerViewModel( } } + fun seekTo(progress: Float) { + val position = (progress.toDouble() * uiState.value.durationMs).milliseconds + audioPlayer.seekTo(position) + } + fun stop() { audioPlayer.stop() }