diff --git a/feature/audiobar/build.gradle.kts b/feature/audiobar/build.gradle.kts index a3593a4ca7..20ab1b6ee4 100644 --- a/feature/audiobar/build.gradle.kts +++ b/feature/audiobar/build.gradle.kts @@ -5,8 +5,10 @@ plugins { android.namespace = "com.quran.mobile.feature.audiobar" dependencies { + implementation(project(":common:data")) implementation(project(":common:audio")) implementation(project(":common:download")) + implementation(project(":common:recitation")) implementation(project(":common:ui:core")) // compose diff --git a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/AudioBarPresenter.kt b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/AudioBarPresenter.kt deleted file mode 100644 index 4e91c72b50..0000000000 --- a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/AudioBarPresenter.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.quran.mobile.feature.audiobar - -import androidx.compose.runtime.Composable -import com.quran.labs.androidquran.common.audio.repository.AudioStatusRepository -import com.slack.circuit.runtime.presenter.Presenter - -class AudioBarPresenter( - private val audioStatusRepository: AudioStatusRepository -) : Presenter { - - @Composable - override fun present(): AudioBarState { - TODO("Not yet implemented") - } -} diff --git a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/AudioBarState.kt b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/AudioBarState.kt deleted file mode 100644 index de6bd3f898..0000000000 --- a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/AudioBarState.kt +++ /dev/null @@ -1,120 +0,0 @@ -package com.quran.mobile.feature.audiobar - -import com.slack.circuit.runtime.CircuitUiEvent -import com.slack.circuit.runtime.CircuitUiState - -sealed class AudioBarState : CircuitUiState { - sealed class ActivePlayback : AudioBarState() { - abstract val repeat: Int - abstract val speed: Float - abstract val eventSink: (AudioBarEvent.CommonPlaybackEvent) -> Unit - } - - data class Playing( - override val repeat: Int, - override val speed: Float, - override val eventSink: (AudioBarEvent.CommonPlaybackEvent) -> Unit, - val playbackEventSink: (AudioBarEvent.PlayingPlaybackEvent) -> Unit - ) : ActivePlayback() - - data class Paused( - override val repeat: Int, - override val speed: Float, - override val eventSink: (AudioBarEvent.CommonPlaybackEvent) -> Unit, - val pausedEventSink: (AudioBarEvent.PausedPlaybackEvent) -> Unit - ) : ActivePlayback() - - data class Stopped( - val qariName: String, - val enableRecording: Boolean, - val eventSink: (AudioBarEvent.StoppedPlaybackEvent) -> Unit - ) : AudioBarState() - - data class Loading( - val progress: Int, - val message: String, - val eventSink: (AudioBarEvent.CancelablePlaybackEvent) -> Unit - ) : AudioBarState() - - data class Error( - val message: String, - val eventSink: (AudioBarEvent.CancelablePlaybackEvent) -> Unit - ) : AudioBarState() - - data class Prompt( - val message: String, - val eventSink: (AudioBarEvent.PromptEvent) -> Unit - ) : AudioBarState() - - sealed class RecitationState(val isRecitationActive: Boolean) : AudioBarState() { - abstract val eventSink: (AudioBarEvent.CommonRecordingEvent) -> Unit - } - - data class RecitationListening( - override val eventSink: (AudioBarEvent.CommonRecordingEvent) -> Unit, - val listeningEventSink: (AudioBarEvent.RecitationListeningEvent) -> Unit - ) : RecitationState(true) - - data class RecitationPlaying( - override val eventSink: (AudioBarEvent.CommonRecordingEvent) -> Unit, - val playingEventSink: (AudioBarEvent.RecitationPlayingEvent) -> Unit - ) : RecitationState(false) - - data class RecitationStopped( - override val eventSink: (AudioBarEvent.CommonRecordingEvent) -> Unit, - val stoppedEventSink: (AudioBarEvent.RecitationStoppedEvent) -> Unit - ) : RecitationState(false) -} - -sealed class AudioBarEvent : CircuitUiEvent { - sealed class CommonPlaybackEvent : AudioBarEvent() { - data object Stop : CommonPlaybackEvent() - data object Rewind : CommonPlaybackEvent() - data object FastForward : CommonPlaybackEvent() - data class SetSpeed(val speed: Float) : CommonPlaybackEvent() - data class SetRepeat(val repeat: Int) : CommonPlaybackEvent() - } - - sealed class PlayingPlaybackEvent : AudioBarEvent() { - data object Pause : PlayingPlaybackEvent() - } - - sealed class PausedPlaybackEvent : AudioBarEvent() { - data object Play : PausedPlaybackEvent() - } - - sealed class CancelablePlaybackEvent : AudioBarEvent() { - data object Cancel : CancelablePlaybackEvent() - } - - sealed class StoppedPlaybackEvent : AudioBarEvent() { - data object ChangeQari : StoppedPlaybackEvent() - data object Play : StoppedPlaybackEvent() - data object Record : StoppedPlaybackEvent() - } - - sealed class PromptEvent : AudioBarEvent() { - data object Cancel : PromptEvent() - data object Acknowledge : PromptEvent() - } - - sealed class CommonRecordingEvent : AudioBarEvent() { - data object Recitation : CommonRecordingEvent() - data object RecitationLongPress : CommonRecordingEvent() - data object Transcript : CommonRecordingEvent() - } - - sealed class RecitationListeningEvent : AudioBarEvent() { - data object HideVerses : RecitationListeningEvent() - } - - sealed class RecitationPlayingEvent : AudioBarEvent() { - data object EndSession : RecitationPlayingEvent() - data object PauseRecitation : RecitationPlayingEvent() - } - - sealed class RecitationStoppedEvent : AudioBarEvent() { - data object EndSession : RecitationStoppedEvent() - data object PlayRecitation : RecitationStoppedEvent() - } -} diff --git a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/presenter/AudioBarEventRepository.kt b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/presenter/AudioBarEventRepository.kt new file mode 100644 index 0000000000..52cd1a6348 --- /dev/null +++ b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/presenter/AudioBarEventRepository.kt @@ -0,0 +1,19 @@ +package com.quran.mobile.feature.audiobar.presenter + +import com.quran.mobile.feature.audiobar.state.AudioBarEvent +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow + +class AudioBarEventRepository { + private val internalAudioBarEventFlow = MutableSharedFlow( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + val audioBarEventFlow: Flow = internalAudioBarEventFlow + + internal fun onAudioBarEvent(event: AudioBarEvent) { + internalAudioBarEventFlow.tryEmit(event) + } +} diff --git a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/presenter/AudioBarPresenter.kt b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/presenter/AudioBarPresenter.kt new file mode 100644 index 0000000000..14545a2469 --- /dev/null +++ b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/presenter/AudioBarPresenter.kt @@ -0,0 +1,188 @@ +package com.quran.mobile.feature.audiobar.presenter + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import com.quran.data.model.audio.Qari +import com.quran.labs.androidquran.common.audio.model.playback.AudioStatus +import com.quran.labs.androidquran.common.audio.model.playback.PlaybackStatus +import com.quran.labs.androidquran.common.audio.repository.AudioStatusRepository +import com.quran.labs.androidquran.common.audio.repository.CurrentQariManager +import com.quran.mobile.common.download.DownloadInfo +import com.quran.mobile.common.download.DownloadInfoStreams +import com.quran.mobile.common.ui.core.R +import com.quran.mobile.feature.audiobar.state.AudioBarEvent +import com.quran.mobile.feature.audiobar.state.AudioBarState +import com.quran.mobile.feature.audiobar.state.AudioBarUiEvent +import com.quran.recitation.common.RecitationSession +import com.quran.recitation.events.RecitationEventPresenter +import com.quran.recitation.events.RecitationPlaybackEventPresenter +import com.quran.recitation.presenter.RecitationPresenter +import com.slack.circuit.runtime.presenter.Presenter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach + +class AudioBarPresenter( + downloadInfoStreams: DownloadInfoStreams, + audioStatusRepository: AudioStatusRepository, + currentQariManager: CurrentQariManager, + recitationPresenter: RecitationPresenter, + recitationEventPresenter: RecitationEventPresenter, + recitationPlaybackEventPresenter: RecitationPlaybackEventPresenter, + private val audioBarEventRepository: AudioBarEventRepository +) : Presenter { + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val internalAudioBarFlow = MutableStateFlow( + AudioBarState.Loading(-1, R.string.loading) { /* noop */ } + ) + + init { + downloadInfoStreams.downloadInfoStream() + .map { downloadInfo -> downloadInfo.toAudioBarState() } + .onEach { internalAudioBarFlow.value = it } + .launchIn(scope) + + combine( + audioStatusRepository.audioPlaybackFlow, + currentQariManager.flow() + ) { audioStatus, qari -> + transformAudioBarState(audioStatus, recitationPresenter.isRecitationEnabled(), qari) + } + .onEach { internalAudioBarFlow.value = it } + .launchIn(scope) + + combine( + recitationPresenter.isRecitationEnabledFlow(), + recitationEventPresenter.recitationSessionFlow, + recitationEventPresenter.listeningStateFlow, + recitationPlaybackEventPresenter.playingStateFlow, + currentQariManager.flow() + ) { isRecitationEnabled, recitationSession, isListening, isPlaying, qari -> + if (isRecitationEnabled) { + transformAudioBarRecitationState( + recitationSession, isListening, isPlaying, qari + ) + } else { + null + } + } + .filterNotNull() + .onEach { internalAudioBarFlow.value = it } + .launchIn(scope) + } + + @Composable + override fun present(): AudioBarState { + val state = internalAudioBarFlow.collectAsState() + return state.value + } + + private fun DownloadInfo.toAudioBarState(): AudioBarState { + return when (this) { + is DownloadInfo.DownloadBatchError -> AudioBarState.Error(errorId, cancelableEventSink) + is DownloadInfo.FileDownloadProgress -> AudioBarState.Loading( + progress, + com.quran.mobile.common.download.R.string.downloading, + cancelableEventSink + ) + + is DownloadInfo.FileDownloaded, is DownloadInfo.DownloadBatchSuccess -> AudioBarState.Loading( + -1, + R.string.loading, + cancelableEventSink + ) + } + } + + private fun transformAudioBarState( + audioStatus: AudioStatus, + enableRecitation: Boolean, + qari: Qari + ): AudioBarState { + return when (audioStatus) { + is AudioStatus.Playback -> { + when (audioStatus.playbackStatus) { + PlaybackStatus.PREPARING -> + AudioBarState.Loading( + -1, + R.string.loading, + cancelableEventSink + ) + + PlaybackStatus.PLAYING -> AudioBarState.Playing( + audioStatus.audioRequest.repeatInfo, + audioStatus.audioRequest.playbackSpeed, + commonPlaybackEventSink, + playingPlaybackEventSink + ) + + PlaybackStatus.PAUSED -> AudioBarState.Paused( + audioStatus.audioRequest.repeatInfo, + audioStatus.audioRequest.playbackSpeed, + commonPlaybackEventSink, + pausedPlaybackEvent + ) + } + } + + AudioStatus.Stopped -> AudioBarState.Stopped( + qari.nameResource, + enableRecitation, + stoppedEventSink + ) + } + } + + private fun transformAudioBarRecitationState( + recitationSession: RecitationSession?, + isListening: Boolean, + isPlaying: Boolean, + qari: Qari + ): AudioBarState { + return if (recitationSession == null) { + AudioBarState.Stopped( + qari.nameResource, + false, + stoppedEventSink + ) + } else if (isListening) { + AudioBarState.RecitationListening(commonRecordingEventSink, recitationListeningEventSink) + } else if (isPlaying) { + AudioBarState.RecitationPlaying(commonRecordingEventSink, recitationPlayingEventSink) + } else { + AudioBarState.RecitationStopped(commonRecordingEventSink, recitationStoppedEventSink) + } + } + + private fun emit(event: AudioBarEvent) { + audioBarEventRepository.onAudioBarEvent(event) + } + + // event sinks - these only emit the underlying mapped event type to the stream today + // we could easily specialize the handling of events per sink type in the future if necessary + private val cancelableEventSink = + { event: AudioBarUiEvent.CancelablePlaybackEvent -> emit(event.audioBarEvent) } + private val commonPlaybackEventSink = + { event: AudioBarUiEvent.CommonPlaybackEvent -> emit(event.audioBarEvent) } + private val playingPlaybackEventSink = + { event: AudioBarUiEvent.PlayingPlaybackEvent -> emit(event.audioBarEvent) } + private val pausedPlaybackEvent = + { event: AudioBarUiEvent.PausedPlaybackEvent -> emit(event.audioBarEvent) } + private val stoppedEventSink = + { event: AudioBarUiEvent.StoppedPlaybackEvent -> emit(event.audioBarEvent) } + private val commonRecordingEventSink = + { event: AudioBarUiEvent.CommonRecordingEvent -> emit(event.audioBarEvent) } + private val recitationListeningEventSink = + { event: AudioBarUiEvent.RecitationListeningEvent -> emit(event.audioBarEvent) } + private val recitationPlayingEventSink = + { event: AudioBarUiEvent.RecitationPlayingEvent -> emit(event.audioBarEvent) } + private val recitationStoppedEventSink = + { event: AudioBarUiEvent.RecitationStoppedEvent -> emit(event.audioBarEvent) } +} diff --git a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/state/AudioBarEvent.kt b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/state/AudioBarEvent.kt new file mode 100644 index 0000000000..849440809d --- /dev/null +++ b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/state/AudioBarEvent.kt @@ -0,0 +1,27 @@ +package com.quran.mobile.feature.audiobar.state + +sealed class AudioBarEvent { + data object ChangeQari : AudioBarEvent() + data object Cancel : AudioBarEvent() + data object Acknowledge : AudioBarEvent() + + // playback + data object Play : AudioBarEvent() + data object Pause : AudioBarEvent() + data object Stop : AudioBarEvent() + data object FastForward : AudioBarEvent() + data object Rewind : AudioBarEvent() + data object ShowSettings : AudioBarEvent() + data class SetRepeat(val repeat: Int) : AudioBarEvent() + data class SetSpeed(val speed: Float) : AudioBarEvent() + + // recitation + data object Record : AudioBarEvent() + data object Recitation : AudioBarEvent() + data object RecitationLongPress : AudioBarEvent() + data object Transcript : AudioBarEvent() + data object HideVerses : AudioBarEvent() + data object EndSession : AudioBarEvent() + data object PlayRecitation : AudioBarEvent() + data object PauseRecitation : AudioBarEvent() +} diff --git a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/state/AudioBarState.kt b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/state/AudioBarState.kt new file mode 100644 index 0000000000..5347e82dd5 --- /dev/null +++ b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/state/AudioBarState.kt @@ -0,0 +1,124 @@ +package com.quran.mobile.feature.audiobar.state + +import androidx.annotation.RestrictTo +import com.slack.circuit.runtime.CircuitUiEvent +import com.slack.circuit.runtime.CircuitUiState + +@RestrictTo(RestrictTo.Scope.LIBRARY) +sealed class AudioBarState : CircuitUiState { + sealed class ActivePlayback : AudioBarState() { + abstract val repeat: Int + abstract val speed: Float + abstract val eventSink: (AudioBarUiEvent.CommonPlaybackEvent) -> Unit + } + + data class Playing( + override val repeat: Int, + override val speed: Float, + override val eventSink: (AudioBarUiEvent.CommonPlaybackEvent) -> Unit, + val playbackEventSink: (AudioBarUiEvent.PlayingPlaybackEvent) -> Unit + ) : ActivePlayback() + + data class Paused( + override val repeat: Int, + override val speed: Float, + override val eventSink: (AudioBarUiEvent.CommonPlaybackEvent) -> Unit, + val pausedEventSink: (AudioBarUiEvent.PausedPlaybackEvent) -> Unit + ) : ActivePlayback() + + data class Stopped( + val qariNameResource: Int, + val enableRecording: Boolean, + val eventSink: (AudioBarUiEvent.StoppedPlaybackEvent) -> Unit + ) : AudioBarState() + + data class Loading( + val progress: Int, + val messageResource: Int, + val eventSink: (AudioBarUiEvent.CancelablePlaybackEvent) -> Unit + ) : AudioBarState() + + data class Error( + val messageResource: Int, + val eventSink: (AudioBarUiEvent.CancelablePlaybackEvent) -> Unit + ) : AudioBarState() + + data class Prompt( + val messageResource: Int, + val eventSink: (AudioBarUiEvent.PromptEvent) -> Unit + ) : AudioBarState() + + sealed class RecitationState(val isRecitationActive: Boolean) : AudioBarState() { + abstract val eventSink: (AudioBarUiEvent.CommonRecordingEvent) -> Unit + } + + data class RecitationListening( + override val eventSink: (AudioBarUiEvent.CommonRecordingEvent) -> Unit, + val listeningEventSink: (AudioBarUiEvent.RecitationListeningEvent) -> Unit + ) : RecitationState(true) + + data class RecitationPlaying( + override val eventSink: (AudioBarUiEvent.CommonRecordingEvent) -> Unit, + val playingEventSink: (AudioBarUiEvent.RecitationPlayingEvent) -> Unit + ) : RecitationState(false) + + data class RecitationStopped( + override val eventSink: (AudioBarUiEvent.CommonRecordingEvent) -> Unit, + val stoppedEventSink: (AudioBarUiEvent.RecitationStoppedEvent) -> Unit + ) : RecitationState(false) +} + +@RestrictTo(RestrictTo.Scope.LIBRARY) +sealed class AudioBarUiEvent(val audioBarEvent: AudioBarEvent) : CircuitUiEvent { + sealed class CommonPlaybackEvent(audioBarEvent: AudioBarEvent) : AudioBarUiEvent(audioBarEvent) { + data object Stop : CommonPlaybackEvent(AudioBarEvent.Stop) + data object Rewind : CommonPlaybackEvent(AudioBarEvent.Rewind) + data object FastForward : CommonPlaybackEvent(AudioBarEvent.FastForward) + data class SetSpeed(val speed: Float) : CommonPlaybackEvent(AudioBarEvent.SetSpeed(speed)) + data class SetRepeat(val repeat: Int) : CommonPlaybackEvent(AudioBarEvent.SetRepeat(repeat)) + data object ShowSettings : CommonPlaybackEvent(AudioBarEvent.ShowSettings) + } + + sealed class PlayingPlaybackEvent(audioBarEvent: AudioBarEvent) : AudioBarUiEvent(audioBarEvent) { + data object Pause : PlayingPlaybackEvent(AudioBarEvent.Pause) + } + + sealed class PausedPlaybackEvent(audioBarEvent: AudioBarEvent) : AudioBarUiEvent(audioBarEvent) { + data object Play : PausedPlaybackEvent(AudioBarEvent.Play) + } + + sealed class CancelablePlaybackEvent(event: AudioBarEvent) : AudioBarUiEvent(event) { + data object Cancel : CancelablePlaybackEvent(AudioBarEvent.Cancel) + } + + sealed class StoppedPlaybackEvent(audioBarEvent: AudioBarEvent) : AudioBarUiEvent(audioBarEvent) { + data object ChangeQari : StoppedPlaybackEvent(AudioBarEvent.ChangeQari) + data object Play : StoppedPlaybackEvent(AudioBarEvent.Play) + data object Record : StoppedPlaybackEvent(AudioBarEvent.Record) + } + + sealed class PromptEvent(audioBarEvent: AudioBarEvent) : AudioBarUiEvent(audioBarEvent) { + data object Cancel : PromptEvent(AudioBarEvent.Cancel) + data object Acknowledge : PromptEvent(AudioBarEvent.Acknowledge) + } + + sealed class CommonRecordingEvent(audioBarEvent: AudioBarEvent) : AudioBarUiEvent(audioBarEvent) { + data object Recitation : CommonRecordingEvent(AudioBarEvent.Recitation) + data object RecitationLongPress : CommonRecordingEvent(AudioBarEvent.RecitationLongPress) + data object Transcript : CommonRecordingEvent(AudioBarEvent.Transcript) + } + + sealed class RecitationListeningEvent(event: AudioBarEvent) : AudioBarUiEvent(event) { + data object HideVerses : RecitationListeningEvent(AudioBarEvent.HideVerses) + } + + sealed class RecitationPlayingEvent(event: AudioBarEvent) : AudioBarUiEvent(event) { + data object EndSession : RecitationPlayingEvent(AudioBarEvent.EndSession) + data object PauseRecitation : RecitationPlayingEvent(AudioBarEvent.PauseRecitation) + } + + sealed class RecitationStoppedEvent(event: AudioBarEvent) : AudioBarUiEvent(event) { + data object EndSession : RecitationStoppedEvent(AudioBarEvent.EndSession) + data object PlayRecitation : RecitationStoppedEvent(AudioBarEvent.PlayRecitation) + } +} diff --git a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/AudioBar.kt b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/AudioBar.kt index 58a887867d..14e0705b6d 100644 --- a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/AudioBar.kt +++ b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/AudioBar.kt @@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.quran.labs.androidquran.common.ui.core.QuranTheme -import com.quran.mobile.feature.audiobar.AudioBarState +import com.quran.mobile.feature.audiobar.state.AudioBarState @Composable fun AudioBar(audioBarState: AudioBarState) { @@ -52,7 +52,7 @@ fun AudioBarStoppedPreview() { QuranTheme { Surface { AudioBar(audioBarState = AudioBarState.Stopped( - qariName = "Abdul Basit", + qariNameResource = com.quran.labs.androidquran.common.audio.R.string.qari_abdulbaset, enableRecording = false, eventSink = {} )) diff --git a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/ErrorAudioBar.kt b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/ErrorAudioBar.kt index b8fb7e6404..4b3d79883c 100644 --- a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/ErrorAudioBar.kt +++ b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/ErrorAudioBar.kt @@ -18,8 +18,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import com.quran.labs.androidquran.common.ui.core.QuranIcons import com.quran.labs.androidquran.common.ui.core.QuranTheme -import com.quran.mobile.feature.audiobar.AudioBarEvent -import com.quran.mobile.feature.audiobar.AudioBarState +import com.quran.mobile.feature.audiobar.state.AudioBarUiEvent +import com.quran.mobile.feature.audiobar.state.AudioBarState @Composable fun ErrorAudioBar(state: AudioBarState.Error, modifier: Modifier = Modifier) { @@ -28,7 +28,7 @@ fun ErrorAudioBar(state: AudioBarState.Error, modifier: Modifier = Modifier) { verticalAlignment = Alignment.CenterVertically, modifier = modifier.height(IntrinsicSize.Min) ) { - IconButton(onClick = { sink(AudioBarEvent.CancelablePlaybackEvent.Cancel) }) { + IconButton(onClick = { sink(AudioBarUiEvent.CancelablePlaybackEvent.Cancel) }) { Icon(QuranIcons.Close, contentDescription = stringResource(id = android.R.string.cancel)) } @@ -38,7 +38,7 @@ fun ErrorAudioBar(state: AudioBarState.Error, modifier: Modifier = Modifier) { .width(Dp.Hairline) ) - Text(text = state.message) + Text(text = stringResource(id = state.messageResource)) } } @@ -48,7 +48,7 @@ fun ErrorAudioBarPreview() { QuranTheme { ErrorAudioBar( state = AudioBarState.Error( - message = "Error message", + messageResource = android.R.string.httpErrorUnsupportedScheme, eventSink = {} ) ) diff --git a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/LoadingAudioBar.kt b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/LoadingAudioBar.kt index 8bd8d2b9af..201d6c9215 100644 --- a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/LoadingAudioBar.kt +++ b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/LoadingAudioBar.kt @@ -20,17 +20,17 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import com.quran.labs.androidquran.common.ui.core.QuranIcons import com.quran.labs.androidquran.common.ui.core.QuranTheme -import com.quran.mobile.feature.audiobar.AudioBarEvent -import com.quran.mobile.feature.audiobar.AudioBarState +import com.quran.mobile.feature.audiobar.state.AudioBarUiEvent +import com.quran.mobile.feature.audiobar.state.AudioBarState @Composable -fun LoadingAudioBar(state: AudioBarState.Loading, modifier: Modifier = Modifier) { +internal fun LoadingAudioBar(state: AudioBarState.Loading, modifier: Modifier = Modifier) { val sink = state.eventSink Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier.height(IntrinsicSize.Min) ) { - IconButton(onClick = { sink(AudioBarEvent.CancelablePlaybackEvent.Cancel) }) { + IconButton(onClick = { sink(AudioBarUiEvent.CancelablePlaybackEvent.Cancel) }) { Icon(QuranIcons.Close, contentDescription = stringResource(id = android.R.string.cancel)) } @@ -45,7 +45,7 @@ fun LoadingAudioBar(state: AudioBarState.Loading, modifier: Modifier = Modifier) LinearProgressIndicator(progress = state.progress.toFloat() / 100f) } - Text(text = state.message) + Text(text = stringResource(id = state.messageResource)) } } } @@ -57,7 +57,7 @@ fun LoadingAudioBarPreview() { LoadingAudioBar( state = AudioBarState.Loading( progress = 50, - message = "Downloading...", + messageResource = com.quran.mobile.common.download.R.string.downloading, eventSink = {} ) ) @@ -71,7 +71,7 @@ fun LoadingAudioBarIndeterminatePreview() { LoadingAudioBar( state = AudioBarState.Loading( progress = -1, - message = "Loading...", + messageResource = com.quran.mobile.common.ui.core.R.string.loading, eventSink = {} ) ) diff --git a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/PlaybackAudioBar.kt b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/PlaybackAudioBar.kt index 35472f0706..a9aec1eb90 100644 --- a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/PlaybackAudioBar.kt +++ b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/PlaybackAudioBar.kt @@ -7,6 +7,7 @@ import androidx.compose.material.icons.filled.FastRewind import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Repeat +import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Speed import androidx.compose.material.icons.filled.Stop import androidx.compose.material3.Icon @@ -17,15 +18,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.quran.labs.androidquran.common.ui.core.QuranIcons import com.quran.labs.androidquran.common.ui.core.QuranTheme -import com.quran.mobile.feature.audiobar.AudioBarEvent -import com.quran.mobile.feature.audiobar.AudioBarState +import com.quran.mobile.feature.audiobar.state.AudioBarUiEvent +import com.quran.mobile.feature.audiobar.state.AudioBarState @Composable fun PlayingAudioBar(state: AudioBarState.Playing, modifier: Modifier = Modifier) { val sink = state.playbackEventSink AudioBar(state, modifier) { - IconButton(onClick = { sink(AudioBarEvent.PlayingPlaybackEvent.Pause) }) { + IconButton(onClick = { sink(AudioBarUiEvent.PlayingPlaybackEvent.Pause) }) { Icon(QuranIcons.Pause, contentDescription = "") } } @@ -36,7 +37,7 @@ fun PausedAudioBar(state: AudioBarState.Paused, modifier: Modifier = Modifier) { val sink = state.pausedEventSink AudioBar(state = state, modifier = modifier) { - IconButton(onClick = { sink(AudioBarEvent.PausedPlaybackEvent.Play) }) { + IconButton(onClick = { sink(AudioBarUiEvent.PausedPlaybackEvent.Play) }) { Icon(QuranIcons.PlayArrow, contentDescription = "") } } @@ -54,17 +55,17 @@ fun AudioBar( verticalAlignment = Alignment.CenterVertically, modifier = modifier ) { - IconButton(onClick = { sink(AudioBarEvent.CommonPlaybackEvent.Stop) }) { + IconButton(onClick = { sink(AudioBarUiEvent.CommonPlaybackEvent.Stop) }) { Icon(QuranIcons.Stop, contentDescription = "") } - IconButton(onClick = { sink(AudioBarEvent.CommonPlaybackEvent.Rewind) }) { + IconButton(onClick = { sink(AudioBarUiEvent.CommonPlaybackEvent.Rewind) }) { Icon(QuranIcons.FastRewind, contentDescription = "") } actionButton() - IconButton(onClick = { sink(AudioBarEvent.CommonPlaybackEvent.FastForward) }) { + IconButton(onClick = { sink(AudioBarUiEvent.CommonPlaybackEvent.FastForward) }) { Icon(QuranIcons.FastForward, contentDescription = "") } @@ -76,7 +77,7 @@ fun AudioBar( defaultValue = 0, format = { it.toString() } ) { - sink(AudioBarEvent.CommonPlaybackEvent.SetRepeat(it)) + sink(AudioBarUiEvent.CommonPlaybackEvent.SetRepeat(it)) } RepeatableButton( @@ -87,7 +88,11 @@ fun AudioBar( defaultValue = 1.0f, format = { it.toString() } ) { - sink(AudioBarEvent.CommonPlaybackEvent.SetSpeed(it)) + sink(AudioBarUiEvent.CommonPlaybackEvent.SetSpeed(it)) + } + + IconButton(onClick = { sink(AudioBarUiEvent.CommonPlaybackEvent.ShowSettings) }) { + Icon(QuranIcons.Settings, contentDescription = "") } } } diff --git a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/PromptingAudioBar.kt b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/PromptingAudioBar.kt index 0879dbc42a..2a9324cbdf 100644 --- a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/PromptingAudioBar.kt +++ b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/PromptingAudioBar.kt @@ -19,8 +19,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import com.quran.labs.androidquran.common.ui.core.QuranIcons import com.quran.labs.androidquran.common.ui.core.QuranTheme -import com.quran.mobile.feature.audiobar.AudioBarEvent -import com.quran.mobile.feature.audiobar.AudioBarState +import com.quran.mobile.feature.audiobar.state.AudioBarUiEvent +import com.quran.mobile.feature.audiobar.state.AudioBarState @Composable fun PromptingAudioBar(state: AudioBarState.Prompt, modifier: Modifier = Modifier) { @@ -29,7 +29,7 @@ fun PromptingAudioBar(state: AudioBarState.Prompt, modifier: Modifier = Modifier verticalAlignment = Alignment.CenterVertically, modifier = modifier.height(IntrinsicSize.Min) ) { - IconButton(onClick = { sink(AudioBarEvent.PromptEvent.Acknowledge) }) { + IconButton(onClick = { sink(AudioBarUiEvent.PromptEvent.Acknowledge) }) { Icon(QuranIcons.Close, contentDescription = stringResource(id = android.R.string.ok)) } @@ -39,10 +39,10 @@ fun PromptingAudioBar(state: AudioBarState.Prompt, modifier: Modifier = Modifier .width(Dp.Hairline) ) - Text(text = state.message) + Text(text = stringResource(state.messageResource)) Spacer(modifier = Modifier.weight(1f)) - IconButton(onClick = { sink(AudioBarEvent.PromptEvent.Cancel) }) { + IconButton(onClick = { sink(AudioBarUiEvent.PromptEvent.Cancel) }) { Icon(QuranIcons.Close, contentDescription = stringResource(id = android.R.string.cancel)) } } @@ -54,7 +54,7 @@ fun PromptingAudioBarPreview() { QuranTheme { PromptingAudioBar( state = AudioBarState.Prompt( - message = "Error message", + messageResource = android.R.string.httpErrorUnsupportedScheme, eventSink = {} ) ) diff --git a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/RecitationAudioBar.kt b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/RecitationAudioBar.kt index da8a1601b9..f8362eaf20 100644 --- a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/RecitationAudioBar.kt +++ b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/RecitationAudioBar.kt @@ -34,8 +34,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.quran.labs.androidquran.common.ui.core.QuranIcons import com.quran.labs.androidquran.common.ui.core.QuranTheme -import com.quran.mobile.feature.audiobar.AudioBarEvent -import com.quran.mobile.feature.audiobar.AudioBarState +import com.quran.mobile.feature.audiobar.state.AudioBarUiEvent +import com.quran.mobile.feature.audiobar.state.AudioBarState @Composable fun RecitationListeningAudioBar( @@ -44,7 +44,7 @@ fun RecitationListeningAudioBar( ) { val sink = state.listeningEventSink RecitationAudioBar(state = state, modifier = modifier) { - IconButton(onClick = { sink(AudioBarEvent.RecitationListeningEvent.HideVerses) }) { + IconButton(onClick = { sink(AudioBarUiEvent.RecitationListeningEvent.HideVerses) }) { Icon(QuranIcons.MenuBook, contentDescription = "") } } @@ -57,11 +57,11 @@ fun RecitationPlayingAudioBar( ) { val sink = state.playingEventSink RecitationAudioBar(state = state, modifier = modifier) { - IconButton(onClick = { sink(AudioBarEvent.RecitationPlayingEvent.EndSession) }) { + IconButton(onClick = { sink(AudioBarUiEvent.RecitationPlayingEvent.EndSession) }) { Icon(QuranIcons.Close, contentDescription = "") } - IconButton(onClick = { sink(AudioBarEvent.RecitationPlayingEvent.PauseRecitation) }) { + IconButton(onClick = { sink(AudioBarUiEvent.RecitationPlayingEvent.PauseRecitation) }) { Icon(QuranIcons.Pause, contentDescription = "") } } @@ -74,11 +74,11 @@ fun RecitationStoppedAudioBar( ) { val sink = state.stoppedEventSink RecitationAudioBar(state = state, modifier = modifier) { - IconButton(onClick = { sink(AudioBarEvent.RecitationStoppedEvent.EndSession) }) { + IconButton(onClick = { sink(AudioBarUiEvent.RecitationStoppedEvent.EndSession) }) { Icon(QuranIcons.Close, contentDescription = "") } - IconButton(onClick = { sink(AudioBarEvent.RecitationStoppedEvent.PlayRecitation) }) { + IconButton(onClick = { sink(AudioBarUiEvent.RecitationStoppedEvent.PlayRecitation) }) { Icon(QuranIcons.PlayArrow, contentDescription = "") } } @@ -111,7 +111,7 @@ fun RecitationAudioBar( .width(Dp.Hairline) ) - IconButton(onClick = { sink(AudioBarEvent.CommonRecordingEvent.Transcript) }) { + IconButton(onClick = { sink(AudioBarUiEvent.CommonRecordingEvent.Transcript) }) { Icon(QuranIcons.Chat, contentDescription = "") } @@ -129,8 +129,8 @@ fun RecitationAudioBar( .background(color = Color.Transparent) .combinedClickable( role = Role.Button, - onClick = { sink(AudioBarEvent.CommonRecordingEvent.Recitation) }, - onLongClick = { sink(AudioBarEvent.CommonRecordingEvent.RecitationLongPress) }, + onClick = { sink(AudioBarUiEvent.CommonRecordingEvent.Recitation) }, + onLongClick = { sink(AudioBarUiEvent.CommonRecordingEvent.RecitationLongPress) }, ), contentAlignment = Alignment.Center ) { diff --git a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/StoppedAudioBar.kt b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/StoppedAudioBar.kt index ed9a4de290..d0c9d10994 100644 --- a/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/StoppedAudioBar.kt +++ b/feature/audiobar/src/main/kotlin/com/quran/mobile/feature/audiobar/ui/StoppedAudioBar.kt @@ -17,12 +17,13 @@ import androidx.compose.material3.TextButton 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.tooling.preview.Preview import androidx.compose.ui.unit.Dp import com.quran.labs.androidquran.common.ui.core.QuranIcons import com.quran.labs.androidquran.common.ui.core.QuranTheme -import com.quran.mobile.feature.audiobar.AudioBarEvent -import com.quran.mobile.feature.audiobar.AudioBarState +import com.quran.mobile.feature.audiobar.state.AudioBarUiEvent +import com.quran.mobile.feature.audiobar.state.AudioBarState @Composable fun StoppedAudioBar(state: AudioBarState.Stopped, modifier: Modifier = Modifier) { @@ -31,7 +32,7 @@ fun StoppedAudioBar(state: AudioBarState.Stopped, modifier: Modifier = Modifier) verticalAlignment = Alignment.CenterVertically, modifier = modifier.height(IntrinsicSize.Min) ) { - IconButton(onClick = { sink(AudioBarEvent.StoppedPlaybackEvent.Play) }) { + IconButton(onClick = { sink(AudioBarUiEvent.StoppedPlaybackEvent.Play) }) { Icon(QuranIcons.PlayArrow, contentDescription = "") } @@ -43,9 +44,9 @@ fun StoppedAudioBar(state: AudioBarState.Stopped, modifier: Modifier = Modifier) TextButton( modifier = Modifier.weight(1f), - onClick = { sink(AudioBarEvent.StoppedPlaybackEvent.ChangeQari) } + onClick = { sink(AudioBarUiEvent.StoppedPlaybackEvent.ChangeQari) } ) { - Text(text = state.qariName) + Text(text = stringResource(state.qariNameResource)) Spacer(modifier = Modifier.weight(1f)) Icon(QuranIcons.ExpandMore, contentDescription = "") } @@ -57,7 +58,7 @@ fun StoppedAudioBar(state: AudioBarState.Stopped, modifier: Modifier = Modifier) .width(Dp.Hairline) ) - IconButton(onClick = { sink(AudioBarEvent.StoppedPlaybackEvent.Record) }) { + IconButton(onClick = { sink(AudioBarUiEvent.StoppedPlaybackEvent.Record) }) { Icon(QuranIcons.Mic, contentDescription = "") } } @@ -70,7 +71,7 @@ fun StoppedAudioBarPreview() { QuranTheme { StoppedAudioBar( state = AudioBarState.Stopped( - qariName = "Qari Name", + qariNameResource = com.quran.labs.androidquran.common.audio.R.string.qari_dussary, enableRecording = false, eventSink = {} ) @@ -84,7 +85,7 @@ fun StoppedAudioBarWithRecordingPreview() { QuranTheme { StoppedAudioBar( state = AudioBarState.Stopped( - qariName = "Qari Name", + qariNameResource = com.quran.labs.androidquran.common.audio.R.string.qari_dussary, enableRecording = true, eventSink = {} )