Skip to content

Commit

Permalink
Simplify the sleep timer UI by providing predefined options. (#2091)
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulWoitaschek committed Aug 22, 2023
1 parent 0152f10 commit 26929f5
Show file tree
Hide file tree
Showing 21 changed files with 415 additions and 633 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class TriggerWidgetOnChange
}

private fun playStateChanged(): Flow<PlayStateManager.PlayState> {
return playStateManager.flow.distinctUntilChanged()
return playStateManager.flow
}

private fun currentBookChanged(): Flow<Book> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package voice.playback.playstate

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -12,7 +12,7 @@ constructor() {

private val _playState = MutableStateFlow(PlayState.Paused)

val flow: Flow<PlayState>
val flow: StateFlow<PlayState>
get() = _playState

var playState: PlayState
Expand Down
3 changes: 3 additions & 0 deletions playbackScreen/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,7 @@ dependencies {
implementation(libs.material)

implementation(libs.dagger.core)

testImplementation(libs.prefs.inMemory)
testImplementation(libs.turbine)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
Expand All @@ -24,7 +23,7 @@ import voice.data.getBookId
import voice.data.putBookId
import voice.logging.core.Logger
import voice.playbackScreen.view.BookPlayView
import voice.sleepTimer.SleepTimerDialogController
import voice.sleepTimer.SleepTimerDialog
import voice.strings.R as StringsR

private const val NI_BOOK_ID = "niBookId"
Expand All @@ -42,8 +41,8 @@ class BookPlayController(bundle: Bundle) : ComposeController(bundle) {
override fun Content() {
val snackbarHostState = remember { SnackbarHostState() }
val dialogState = viewModel.dialogState.value
val viewState = remember(viewModel) { viewModel.viewState() }
.collectAsState(initial = null).value ?: return
val viewState = viewModel.viewState()
?: return
val context = LocalContext.current
LaunchedEffect(viewModel) {
viewModel.viewEffects.collect { viewEffect ->
Expand All @@ -62,10 +61,6 @@ class BookPlayController(bundle: Bundle) : ComposeController(bundle) {
toBatteryOptimizations()
}
}

BookPlayViewEffect.ShowSleepTimeDialog -> {
openSleepTimeDialog()
}
}
}
}
Expand Down Expand Up @@ -101,6 +96,15 @@ class BookPlayController(bundle: Bundle) : ComposeController(bundle) {
is BookPlayDialogViewState.SelectChapterDialog -> {
SelectChapterDialog(dialogState, viewModel)
}
is BookPlayDialogViewState.SleepTimer -> {
SleepTimerDialog(
viewState = dialogState.viewState,
onDismiss = viewModel::dismissDialog,
onIncrementSleepTime = viewModel::incrementSleepTime,
onDecrementSleepTime = viewModel::decrementSleepTime,
onAcceptSleepTime = viewModel::onAcceptSleepTime,
)
}
}
}
}
Expand All @@ -119,11 +123,6 @@ class BookPlayController(bundle: Bundle) : ComposeController(bundle) {
}
}

private fun openSleepTimeDialog() {
SleepTimerDialogController(bookId)
.showDialog(router)
}

@ContributesTo(AppScope::class)
interface Component {
val bookPlayViewModelFactory: BookPlayViewModel.Factory
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package voice.playbackScreen

internal sealed interface BookPlayViewEffect {
object BookmarkAdded : BookPlayViewEffect
object ShowSleepTimeDialog : BookPlayViewEffect
object RequestIgnoreBatteryOptimization : BookPlayViewEffect
data object BookmarkAdded : BookPlayViewEffect
data object RequestIgnoreBatteryOptimization : BookPlayViewEffect
}
144 changes: 109 additions & 35 deletions playbackScreen/src/main/kotlin/voice/playbackScreen/BookPlayViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,90 +1,162 @@
package voice.playbackScreen

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.datastore.core.DataStore
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.MainScope
import de.paulwoitaschek.flowpref.Pref
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
import voice.common.BookId
import voice.common.DispatcherProvider
import voice.common.compose.ImmutableFile
import voice.common.navigation.Destination
import voice.common.navigation.Navigator
import voice.common.pref.CurrentBook
import voice.common.pref.PrefKeys
import voice.data.durationMs
import voice.data.markForPosition
import voice.data.repo.BookRepository
import voice.data.repo.BookmarkRepo
import voice.logging.core.Logger
import voice.playback.PlayerController
import voice.playback.misc.Decibel
import voice.playback.misc.VolumeGain
import voice.playback.playstate.PlayStateManager
import voice.playbackScreen.batteryOptimization.BatteryOptimization
import voice.sleepTimer.SleepTimer
import voice.sleepTimer.SleepTimerViewState
import javax.inject.Named
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes

class BookPlayViewModel
@AssistedInject constructor(
private val repo: BookRepository,
private val bookRepository: BookRepository,
private val player: PlayerController,
private val sleepTimer: SleepTimer,
private val playStateManager: PlayStateManager,
@CurrentBook
private val currentBookId: DataStore<BookId?>,
private val navigator: Navigator,
private val bookmarkRepo: BookmarkRepo,
private val bookmarkRepository: BookmarkRepo,
private val volumeGainFormatter: VolumeGainFormatter,
private val batteryOptimization: BatteryOptimization,
dispatcherProvider: DispatcherProvider,
@Named(PrefKeys.SLEEP_TIME)
private val sleepTimePref: Pref<Int>,
@Assisted
private val bookId: BookId,
) {

private val scope = MainScope()
private val scope = CoroutineScope(SupervisorJob() + dispatcherProvider.main)

private val _viewEffects = MutableSharedFlow<BookPlayViewEffect>(extraBufferCapacity = 1)
internal val viewEffects: Flow<BookPlayViewEffect> get() = _viewEffects

private val _dialogState = mutableStateOf<BookPlayDialogViewState?>(null)
internal val dialogState: State<BookPlayDialogViewState?> get() = _dialogState

fun viewState(): Flow<BookPlayViewState> {
@Composable
fun viewState(): BookPlayViewState? {
player.pauseIfCurrentBookDifferentFrom(bookId)
scope.launch {
LaunchedEffect(Unit) {
currentBookId.updateData { bookId }
}

return combine(
repo.flow(bookId).filterNotNull(),
playStateManager.flow,
sleepTimer.leftSleepTimeFlow,
) { book, playState, sleepTime ->
val currentMark = book.currentChapter.markForPosition(book.content.positionInChapter)
val hasMoreThanOneChapter = book.chapters.sumOf { it.chapterMarks.count() } > 1
BookPlayViewState(
sleepTime = sleepTime,
playing = playState == PlayStateManager.PlayState.Playing,
title = book.content.name,
showPreviousNextButtons = hasMoreThanOneChapter,
chapterName = currentMark.name.takeIf { hasMoreThanOneChapter },
duration = currentMark.durationMs.milliseconds,
playedTime = (book.content.positionInChapter - currentMark.startMs).milliseconds,
cover = book.content.cover?.let(::ImmutableFile),
skipSilence = book.content.skipSilence,
)
}
val book = remember { bookRepository.flow(bookId).filterNotNull() }.collectAsState(initial = null).value
?: return null

val playState by remember {
playStateManager.flow
}.collectAsState()

val sleepTime by remember { sleepTimer.leftSleepTimeFlow }.collectAsState()

val currentMark = book.currentChapter.markForPosition(book.content.positionInChapter)
val hasMoreThanOneChapter = book.chapters.sumOf { it.chapterMarks.count() } > 1
return BookPlayViewState(
sleepTime = sleepTime,
playing = playState == PlayStateManager.PlayState.Playing,
title = book.content.name,
showPreviousNextButtons = hasMoreThanOneChapter,
chapterName = currentMark.name.takeIf { hasMoreThanOneChapter },
duration = currentMark.durationMs.milliseconds,
playedTime = (book.content.positionInChapter - currentMark.startMs).milliseconds,
cover = book.content.cover?.let(::ImmutableFile),
skipSilence = book.content.skipSilence,
)
}

fun dismissDialog() {
Logger.d("dismissDialog")
_dialogState.value = null
}

fun incrementSleepTime() {
updateSleepTimeViewState {
val customTime = it.customSleepTime
val newTime = when {
customTime < 5 -> customTime + 1
else -> customTime + 5
}
sleepTimePref.value = newTime
SleepTimerViewState(newTime)
}
}

fun decrementSleepTime() {
updateSleepTimeViewState {
val customTime = it.customSleepTime
val newTime = when {
customTime <= 1 -> 1
customTime <= 5 -> customTime - 1
else -> (customTime - 5).coerceAtLeast(5)
}
sleepTimePref.value = newTime
SleepTimerViewState(newTime)
}
}

fun onAcceptSleepTime(time: Int) {
updateSleepTimeViewState {
scope.launch {
val book = bookRepository.get(bookId) ?: return@launch
bookmarkRepository.addBookmarkAtBookPosition(
book = book,
setBySleepTimer = true,
title = null,
)
}
sleepTimer.setActive(time.minutes)
null
}
}

private fun updateSleepTimeViewState(
update: (SleepTimerViewState) -> SleepTimerViewState?,
) {
val current = dialogState.value
val updated: SleepTimerViewState? = if (current is BookPlayDialogViewState.SleepTimer) {
update(current.viewState)
} else {
update(SleepTimerViewState(sleepTimePref.value))
}
_dialogState.value = updated?.let(BookPlayDialogViewState::SleepTimer)
}

fun onPlaybackSpeedChanged(speed: Float) {
_dialogState.value = BookPlayDialogViewState.SpeedDialog(speed)
player.setSpeed(speed)
Expand Down Expand Up @@ -125,7 +197,7 @@ class BookPlayViewModel

fun onCurrentChapterClicked() {
scope.launch {
val book = repo.get(bookId) ?: return@launch
val book = bookRepository.get(bookId) ?: return@launch
val chapterMarks = book.chapters.flatMap {
it.chapterMarks
}
Expand All @@ -139,7 +211,7 @@ class BookPlayViewModel

fun onChapterClicked(index: Int) {
scope.launch {
val book = repo.get(bookId) ?: return@launch
val book = bookRepository.get(bookId) ?: return@launch
var currentIndex = -1
book.chapters.forEach { chapter ->
chapter.chapterMarks.forEach { mark ->
Expand All @@ -156,7 +228,7 @@ class BookPlayViewModel

fun onPlaybackSpeedIconClicked() {
scope.launch {
val playbackSpeed = repo.get(bookId)?.content?.playbackSpeed
val playbackSpeed = bookRepository.get(bookId)?.content?.playbackSpeed
if (playbackSpeed != null) {
_dialogState.value = BookPlayDialogViewState.SpeedDialog(playbackSpeed)
}
Expand All @@ -165,7 +237,7 @@ class BookPlayViewModel

fun onVolumeGainIconClicked() {
scope.launch {
val content = repo.get(bookId)?.content
val content = bookRepository.get(bookId)?.content
if (content != null) {
_dialogState.value = volumeGainDialogViewState(Decibel(content.gain))
}
Expand All @@ -186,8 +258,8 @@ class BookPlayViewModel

fun onBookmarkLongClicked() {
scope.launch {
val book = repo.get(bookId) ?: return@launch
bookmarkRepo.addBookmarkAtBookPosition(
val book = bookRepository.get(bookId) ?: return@launch
bookmarkRepository.addBookmarkAtBookPosition(
book = book,
title = null,
setBySleepTimer = false,
Expand All @@ -198,24 +270,26 @@ class BookPlayViewModel

fun seekTo(position: Duration) {
scope.launch {
val book = repo.get(bookId) ?: return@launch
val book = bookRepository.get(bookId) ?: return@launch
val currentChapter = book.currentChapter
val currentMark = currentChapter.markForPosition(book.content.positionInChapter)
player.setPosition(currentMark.startMs + position.inWholeMilliseconds, currentChapter.id)
}
}

fun toggleSleepTimer() {
Logger.d("toggleSleepTimer while active=${sleepTimer.sleepTimerActive()}")
if (sleepTimer.sleepTimerActive()) {
sleepTimer.setActive(false)
_dialogState.value = null
} else {
_viewEffects.tryEmit(BookPlayViewEffect.ShowSleepTimeDialog)
_dialogState.value = BookPlayDialogViewState.SleepTimer(SleepTimerViewState(sleepTimePref.value))
}
}

fun toggleSkipSilence() {
scope.launch {
val book = repo.get(bookId) ?: return@launch
val book = bookRepository.get(bookId) ?: return@launch
val skipSilence = book.content.skipSilence
player.skipSilence(!skipSilence)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
import voice.common.compose.ImmutableFile
import voice.data.ChapterMark
import voice.playback.misc.Decibel
import voice.sleepTimer.SleepTimerViewState
import kotlin.time.Duration

@Immutable
Expand Down Expand Up @@ -44,4 +45,9 @@ internal sealed interface BookPlayDialogViewState {
val chapters: List<ChapterMark>,
val selectedIndex: Int?,
) : BookPlayDialogViewState

@JvmInline
value class SleepTimer(
val viewState: SleepTimerViewState,
) : BookPlayDialogViewState
}

0 comments on commit 26929f5

Please sign in to comment.