Store voice player playback index in a datastore.
This commit is contained in:
parent
325d7d5fde
commit
152b351bf3
6 changed files with 88 additions and 6 deletions
|
|
@ -26,10 +26,12 @@ dependencies {
|
||||||
implementation(projects.libraries.di)
|
implementation(projects.libraries.di)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.mediaplayer.api)
|
implementation(projects.libraries.mediaplayer.api)
|
||||||
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.libraries.uiUtils)
|
implementation(projects.libraries.uiUtils)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
|
|
||||||
implementation(libs.androidx.annotationjvm)
|
implementation(libs.androidx.annotationjvm)
|
||||||
|
implementation(libs.androidx.datastore.preferences)
|
||||||
implementation(libs.coroutines.core)
|
implementation(libs.coroutines.core)
|
||||||
|
|
||||||
testCommonDependencies(libs)
|
testCommonDependencies(libs)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ class DefaultVoiceMessagePresenterFactory(
|
||||||
@SessionCoroutineScope
|
@SessionCoroutineScope
|
||||||
private val sessionCoroutineScope: CoroutineScope,
|
private val sessionCoroutineScope: CoroutineScope,
|
||||||
private val voiceMessagePlayerFactory: VoiceMessagePlayer.Factory,
|
private val voiceMessagePlayerFactory: VoiceMessagePlayer.Factory,
|
||||||
|
private val voicePlayerStore: VoicePlayerStore,
|
||||||
) : VoiceMessagePresenterFactory {
|
) : VoiceMessagePresenterFactory {
|
||||||
override fun createVoiceMessagePresenter(
|
override fun createVoiceMessagePresenter(
|
||||||
eventId: EventId?,
|
eventId: EventId?,
|
||||||
|
|
@ -44,6 +45,7 @@ class DefaultVoiceMessagePresenterFactory(
|
||||||
return VoiceMessagePresenter(
|
return VoiceMessagePresenter(
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
sessionCoroutineScope = sessionCoroutineScope,
|
sessionCoroutineScope = sessionCoroutineScope,
|
||||||
|
voicePlayerStore = voicePlayerStore,
|
||||||
player = player,
|
player = player,
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
duration = duration,
|
duration = duration,
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,13 @@
|
||||||
package io.element.android.libraries.voiceplayer.impl
|
package io.element.android.libraries.voiceplayer.impl
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runUpdatingState
|
import io.element.android.libraries.architecture.runUpdatingState
|
||||||
|
|
@ -34,15 +35,16 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||||
class VoiceMessagePresenter(
|
class VoiceMessagePresenter(
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val sessionCoroutineScope: CoroutineScope,
|
private val sessionCoroutineScope: CoroutineScope,
|
||||||
|
private val voicePlayerStore: VoicePlayerStore,
|
||||||
private val player: VoiceMessagePlayer,
|
private val player: VoiceMessagePlayer,
|
||||||
private val eventId: EventId?,
|
private val eventId: EventId?,
|
||||||
private val duration: Duration,
|
private val duration: Duration,
|
||||||
) : Presenter<VoiceMessageState> {
|
) : Presenter<VoiceMessageState> {
|
||||||
private val play = mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
|
private val play = mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
|
||||||
private val playbackSpeedIndex = mutableIntStateOf(0)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): VoiceMessageState {
|
override fun present(): VoiceMessageState {
|
||||||
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val playerState by player.state.collectAsState(
|
val playerState by player.state.collectAsState(
|
||||||
VoiceMessagePlayer.State(
|
VoiceMessagePlayer.State(
|
||||||
isReady = false,
|
isReady = false,
|
||||||
|
|
@ -53,6 +55,12 @@ class VoiceMessagePresenter(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val playbackSpeedIndex by voicePlayerStore.playBackSpeedIndex().collectAsState(0)
|
||||||
|
|
||||||
|
LaunchedEffect(playbackSpeedIndex) {
|
||||||
|
player.setPlaybackSpeed(VoicePlayerConfig.availablePlaybackSpeeds[playbackSpeedIndex])
|
||||||
|
}
|
||||||
|
|
||||||
val buttonType by remember {
|
val buttonType by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
when {
|
when {
|
||||||
|
|
@ -114,9 +122,10 @@ class VoiceMessagePresenter(
|
||||||
is VoiceMessageEvent.Seek -> {
|
is VoiceMessageEvent.Seek -> {
|
||||||
player.seekTo((event.percentage * duration).toLong())
|
player.seekTo((event.percentage * duration).toLong())
|
||||||
}
|
}
|
||||||
is VoiceMessageEvent.ChangePlaybackSpeed -> {
|
is VoiceMessageEvent.ChangePlaybackSpeed -> localCoroutineScope.launch {
|
||||||
playbackSpeedIndex.intValue = (playbackSpeedIndex.intValue + 1) % VoicePlayerConfig.availablePlaybackSpeeds.size
|
voicePlayerStore.setPlayBackSpeedIndex(
|
||||||
player.setPlaybackSpeed(VoicePlayerConfig.availablePlaybackSpeeds[playbackSpeedIndex.intValue])
|
(playbackSpeedIndex + 1) % VoicePlayerConfig.availablePlaybackSpeeds.size
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -126,7 +135,7 @@ class VoiceMessagePresenter(
|
||||||
progress = progress,
|
progress = progress,
|
||||||
time = time,
|
time = time,
|
||||||
showCursor = showCursor,
|
showCursor = showCursor,
|
||||||
playbackSpeed = VoicePlayerConfig.availablePlaybackSpeeds[playbackSpeedIndex.intValue],
|
playbackSpeed = VoicePlayerConfig.availablePlaybackSpeeds[playbackSpeedIndex],
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.voiceplayer.impl
|
||||||
|
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
interface VoicePlayerStore {
|
||||||
|
suspend fun setPlayBackSpeedIndex(index: Int)
|
||||||
|
fun playBackSpeedIndex(): Flow<Int>
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class PreferencesVoicePlayerStore(
|
||||||
|
preferenceDataStoreFactory: PreferenceDataStoreFactory,
|
||||||
|
) : VoicePlayerStore {
|
||||||
|
private val store = preferenceDataStoreFactory.create("elementx_voice_player")
|
||||||
|
private val playbackSpeedIndex = intPreferencesKey("playback_speed_index")
|
||||||
|
|
||||||
|
override fun playBackSpeedIndex(): Flow<Int> {
|
||||||
|
return store.data.map { prefs ->
|
||||||
|
prefs[playbackSpeedIndex] ?: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setPlayBackSpeedIndex(index: Int) {
|
||||||
|
store.edit { prefs ->
|
||||||
|
prefs[playbackSpeedIndex] = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.voiceplayer.impl
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
internal class InMemoryVoicePlayerStore(
|
||||||
|
defaultPlaybackSpeedIndex: Int = 0,
|
||||||
|
) : VoicePlayerStore {
|
||||||
|
private val playBackSpeedIndex = MutableStateFlow(defaultPlaybackSpeedIndex)
|
||||||
|
|
||||||
|
override fun playBackSpeedIndex(): Flow<Int> {
|
||||||
|
return playBackSpeedIndex.asStateFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setPlayBackSpeedIndex(index: Int) {
|
||||||
|
playBackSpeedIndex.emit(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -240,6 +240,7 @@ fun TestScope.createVoiceMessagePresenter(
|
||||||
mediaPlayer: FakeMediaPlayer = FakeMediaPlayer(),
|
mediaPlayer: FakeMediaPlayer = FakeMediaPlayer(),
|
||||||
voiceMessageMediaRepo: VoiceMessageMediaRepo = FakeVoiceMessageMediaRepo(),
|
voiceMessageMediaRepo: VoiceMessageMediaRepo = FakeVoiceMessageMediaRepo(),
|
||||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||||
|
voicePlayerStore: VoicePlayerStore = InMemoryVoicePlayerStore(),
|
||||||
eventId: EventId? = EventId("\$anEventId"),
|
eventId: EventId? = EventId("\$anEventId"),
|
||||||
filename: String = "filename doesn't really matter for a voice message",
|
filename: String = "filename doesn't really matter for a voice message",
|
||||||
duration: Duration = 61_000.milliseconds,
|
duration: Duration = 61_000.milliseconds,
|
||||||
|
|
@ -257,6 +258,7 @@ fun TestScope.createVoiceMessagePresenter(
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
filename = filename
|
filename = filename
|
||||||
),
|
),
|
||||||
|
voicePlayerStore = voicePlayerStore,
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
duration = duration,
|
duration = duration,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue