Skip to content

Commit

Permalink
Merge pull request #2564 from quran/initial_audiobar_work
Browse files Browse the repository at this point in the history
Add initial AudioBar Composables
  • Loading branch information
ahmedre authored Jan 30, 2024
2 parents 0822531 + 79f1433 commit 3b4c4b3
Show file tree
Hide file tree
Showing 15 changed files with 910 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.quran.data.core.QuranInfo
import com.quran.labs.androidquran.data.Constants
import com.quran.labs.androidquran.ui.fragment.QuranPageFragment
import com.quran.labs.androidquran.ui.fragment.TabletFragment
import com.quran.labs.androidquran.ui.fragment.TranslationFragment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.quran.labs.androidquran.common.ui.core

import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
Expand Down Expand Up @@ -72,6 +73,8 @@ private val DarkColors = darkColorScheme(

private val forceLtr = listOf("huawei", "lenovo", "tecno")

val QuranIcons = Icons.Filled

@Composable
fun QuranTheme(
useDarkTheme: Boolean = isSystemInDarkTheme(),
Expand Down
27 changes: 27 additions & 0 deletions feature/audiobar/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
plugins {
id("quran.android.library.compose")
}

android.namespace = "com.quran.mobile.feature.audiobar"

dependencies {
implementation(project(":common:audio"))
implementation(project(":common:ui:core"))

// compose
implementation(libs.compose.animation)
implementation(libs.compose.foundation)
implementation(libs.compose.material)
implementation(libs.compose.material3)
implementation(libs.compose.material.icons)
implementation(libs.compose.ui)
implementation(libs.compose.ui.tooling.preview)
debugImplementation(libs.compose.ui.tooling)

// circuit
implementation(libs.circuit.foundation)

// coroutines
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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<AudioBarState> {

@Composable
override fun present(): AudioBarState {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.quran.mobile.feature.audiobar.ui

import android.content.res.Configuration
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.requiredWidthIn
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
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

@Composable
fun AudioBar(audioBarState: AudioBarState) {
val modifier = Modifier
.requiredWidthIn(max = 360.dp)
.fillMaxWidth()
.height(56.dp)

when (audioBarState) {
is AudioBarState.Paused -> PausedAudioBar(state = audioBarState, modifier = modifier)
is AudioBarState.Playing -> PlayingAudioBar(state = audioBarState, modifier = modifier)
is AudioBarState.Error -> ErrorAudioBar(state = audioBarState, modifier = modifier)
is AudioBarState.Loading -> LoadingAudioBar(state = audioBarState, modifier = modifier)
is AudioBarState.Prompt -> PromptingAudioBar(state = audioBarState, modifier = modifier)
is AudioBarState.RecitationListening -> RecitationListeningAudioBar(
state = audioBarState,
modifier = modifier
)

is AudioBarState.RecitationPlaying -> RecitationPlayingAudioBar(
state = audioBarState,
modifier = modifier
)

is AudioBarState.RecitationStopped -> RecitationStoppedAudioBar(
state = audioBarState,
modifier = modifier
)

is AudioBarState.Stopped -> StoppedAudioBar(state = audioBarState, modifier = modifier)
}
}

@Preview
@Preview("arabic", locale = "ar")
@Preview("dark theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun AudioBarStoppedPreview() {
QuranTheme {
Surface {
AudioBar(audioBarState = AudioBarState.Stopped(
qariName = "Abdul Basit",
enableRecording = false,
eventSink = {}
))
}
}
}

@Preview
@Preview("arabic", locale = "ar")
@Preview("dark theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun AudioBarPlayingPreview() {
QuranTheme {
Surface {
AudioBar(audioBarState = AudioBarState.Playing(
repeat = 1,
speed = 1.5f,
eventSink = {},
playbackEventSink = {}
))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.quran.mobile.feature.audiobar.ui

import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
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

@Composable
fun ErrorAudioBar(state: AudioBarState.Error, modifier: Modifier = Modifier) {
val sink = state.eventSink
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier.height(IntrinsicSize.Min)
) {
IconButton(onClick = { sink(AudioBarEvent.CancelablePlaybackEvent.Cancel) }) {
Icon(QuranIcons.Close, contentDescription = stringResource(id = android.R.string.cancel))
}

Divider(
modifier = Modifier
.fillMaxHeight()
.width(Dp.Hairline)
)

Text(text = state.message)
}
}

@Preview
@Composable
fun ErrorAudioBarPreview() {
QuranTheme {
ErrorAudioBar(
state = AudioBarState.Error(
message = "Error message",
eventSink = {}
)
)
}
}
Loading

0 comments on commit 3b4c4b3

Please sign in to comment.