diff --git a/app/src/main/java/org/dueattendant149/bookshelf/presentation/tts/TtsModel.kt b/app/src/main/java/org/dueattendant149/bookshelf/presentation/tts/TtsModel.kt index 71870069..631f96cd 100644 --- a/app/src/main/java/org/dueattendant149/bookshelf/presentation/tts/TtsModel.kt +++ b/app/src/main/java/org/dueattendant149/bookshelf/presentation/tts/TtsModel.kt @@ -8,6 +8,8 @@ package org.dueattendant149.bookshelf.presentation.tts import android.app.Application import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.work.Constraints @@ -29,6 +31,7 @@ import org.dueattendant149.bookshelf.domain.use_case.tts.CreateTtsJobUseCase import org.dueattendant149.bookshelf.domain.use_case.tts.FetchTtsEnginesUseCase import org.dueattendant149.bookshelf.domain.use_case.tts.FetchTtsVoicesUseCase import org.dueattendant149.bookshelf.domain.use_case.tts.GetTtsJobUseCase +import org.dueattendant149.bookshelf.R import javax.inject.Inject data class TtsState( @@ -60,6 +63,9 @@ class TtsModel private val workManager = WorkManager.getInstance(application) + private var observedWork: LiveData? = null + private var workObserver: Observer? = null + fun load(bookRemoteId: String) { viewModelScope.launch { _state.update { it.copy(isLoading = true, error = null) } @@ -123,7 +129,7 @@ class TtsModel speed = _state.value.speed, ) val job = result.getOrElse { - _state.update { it.copy(isCreatingJob = false, error = it.error ?: "Failed to create TTS job") } + _state.update { it.copy(isCreatingJob = false, error = it.error ?: application.getString(R.string.tts_create_job_failed)) } return@launch } _state.update { it.copy(isCreatingJob = false, job = job) } @@ -168,32 +174,47 @@ class TtsModel } private fun observeWorker(workId: java.util.UUID) { - workManager.getWorkInfoByIdLiveData(workId) - .observeForever { info: WorkInfo? -> - info ?: return@observeForever - when (info.state) { - WorkInfo.State.SUCCEEDED -> { - val filePath = info.outputData.getString(TtsDownloadWorker.KEY_OUTPUT_FILE_PATH) - _state.update { it.copy(outputFilePath = filePath) } - } + workObserver?.let { observer -> + observedWork?.removeObserver(observer) + } - WorkInfo.State.FAILED -> { - val message = info.outputData.getString(TtsDownloadWorker.KEY_ERROR_MESSAGE) - _state.update { it.copy(error = message ?: "TTS download failed") } - } - - WorkInfo.State.RUNNING -> { - val progress = info.progress.getDouble(TtsDownloadWorker.PROGRESS_PROGRESS, 0.0) - val status = info.progress.getString(TtsDownloadWorker.PROGRESS_STATUS) ?: "" - _state.update { - it.copy( - job = it.job?.copy(status = status, progress = progress) - ) - } - } - - else -> {} + val liveData = workManager.getWorkInfoByIdLiveData(workId) + val observer: Observer = Observer { info -> + info ?: return@Observer + when (info.state) { + WorkInfo.State.SUCCEEDED -> { + val filePath = info.outputData.getString(TtsDownloadWorker.KEY_OUTPUT_FILE_PATH) + _state.update { it.copy(outputFilePath = filePath) } } + + WorkInfo.State.FAILED -> { + val message = info.outputData.getString(TtsDownloadWorker.KEY_ERROR_MESSAGE) + _state.update { it.copy(error = message ?: application.getString(R.string.tts_download_failed)) } + } + + WorkInfo.State.RUNNING -> { + val progress = info.progress.getDouble(TtsDownloadWorker.PROGRESS_PROGRESS, 0.0) + val status = info.progress.getString(TtsDownloadWorker.PROGRESS_STATUS) ?: "" + _state.update { + it.copy( + job = it.job?.copy(status = status, progress = progress) + ) + } + } + + else -> {} } + } + + observedWork = liveData + workObserver = observer + liveData.observeForever(observer) + } + + override fun onCleared() { + workObserver?.let { observer -> + observedWork?.removeObserver(observer) + } + super.onCleared() } } diff --git a/app/src/main/java/org/dueattendant149/bookshelf/ui/player/PlayerContent.kt b/app/src/main/java/org/dueattendant149/bookshelf/ui/player/PlayerContent.kt index 4ed1a8c1..29b38fbb 100644 --- a/app/src/main/java/org/dueattendant149/bookshelf/ui/player/PlayerContent.kt +++ b/app/src/main/java/org/dueattendant149/bookshelf/ui/player/PlayerContent.kt @@ -41,10 +41,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.flow.StateFlow +import org.dueattendant149.bookshelf.R import org.dueattendant149.bookshelf.presentation.player.PlayerState import org.dueattendant149.bookshelf.ui.navigator.LocalNavigator import kotlin.math.roundToInt @@ -73,7 +75,7 @@ fun PlayerContent( IconButton(onClick = { navigator.pop() }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" + contentDescription = stringResource(R.string.go_back_content_desc) ) } } @@ -97,7 +99,7 @@ fun PlayerContent( val currentTrack = playerState.tracks.getOrNull(playerState.currentTrackIndex) Text( - text = currentTrack?.title ?: "No track", + text = currentTrack?.title ?: stringResource(R.string.player_no_track), style = MaterialTheme.typography.titleMedium, maxLines = 2, overflow = TextOverflow.Ellipsis, @@ -145,10 +147,10 @@ fun PlayerContent( verticalAlignment = Alignment.CenterVertically ) { IconButton(onClick = onSeekBackward) { - Text("-10s") + Text(stringResource(R.string.player_seek_backward)) } IconButton(onClick = onSkipPrevious) { - Icon(Icons.Default.SkipPrevious, contentDescription = "Previous") + Icon(Icons.Default.SkipPrevious, contentDescription = stringResource(R.string.player_previous)) } IconButton( onClick = onPlayPause, @@ -156,15 +158,17 @@ fun PlayerContent( ) { Icon( imageVector = if (playerState.isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, - contentDescription = if (playerState.isPlaying) "Pause" else "Play", + contentDescription = stringResource( + if (playerState.isPlaying) R.string.player_pause else R.string.player_play + ), modifier = Modifier.size(48.dp) ) } IconButton(onClick = onSkipNext) { - Icon(Icons.Default.SkipNext, contentDescription = "Next") + Icon(Icons.Default.SkipNext, contentDescription = stringResource(R.string.player_next)) } IconButton(onClick = onSeekForward) { - Text("+30s") + Text(stringResource(R.string.player_seek_forward)) } } diff --git a/app/src/main/java/org/dueattendant149/bookshelf/ui/tts/TtsContent.kt b/app/src/main/java/org/dueattendant149/bookshelf/ui/tts/TtsContent.kt index 954a8bc4..f36b7142 100644 --- a/app/src/main/java/org/dueattendant149/bookshelf/ui/tts/TtsContent.kt +++ b/app/src/main/java/org/dueattendant149/bookshelf/ui/tts/TtsContent.kt @@ -226,7 +226,15 @@ private fun VoiceItem( ) { ListItem( headlineContent = { Text(voice.name) }, - supportingContent = { Text("${voice.language} • ${voice.engine}") }, + supportingContent = { + Text( + stringResource( + R.string.tts_voice_description, + voice.language, + voice.engine + ) + ) + }, trailingContent = { if (selected) { Text(stringResource(R.string.tts_selected_label)) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3342f532..20890bad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,6 +12,13 @@ Audio playback Shows the currently playing audiobook + No track + -10s + +30s + Previous + Next + Play + Pause Library @@ -569,5 +576,8 @@ Speed: %1$.2fx Create TTS job Book: %1$s + %1$s • %2$s + Failed to create TTS job + TTS download failed \ No newline at end of file