Polish audio/TTS UI: move labels to strings.xml, fix TTS WorkManager observer leak
This commit is contained in:
parent
ccfc677ea7
commit
6ddbb01973
4 changed files with 76 additions and 33 deletions
|
|
@ -8,6 +8,8 @@ package org.dueattendant149.bookshelf.presentation.tts
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.work.Constraints
|
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.FetchTtsEnginesUseCase
|
||||||
import org.dueattendant149.bookshelf.domain.use_case.tts.FetchTtsVoicesUseCase
|
import org.dueattendant149.bookshelf.domain.use_case.tts.FetchTtsVoicesUseCase
|
||||||
import org.dueattendant149.bookshelf.domain.use_case.tts.GetTtsJobUseCase
|
import org.dueattendant149.bookshelf.domain.use_case.tts.GetTtsJobUseCase
|
||||||
|
import org.dueattendant149.bookshelf.R
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class TtsState(
|
data class TtsState(
|
||||||
|
|
@ -60,6 +63,9 @@ class TtsModel
|
||||||
|
|
||||||
private val workManager = WorkManager.getInstance(application)
|
private val workManager = WorkManager.getInstance(application)
|
||||||
|
|
||||||
|
private var observedWork: LiveData<WorkInfo?>? = null
|
||||||
|
private var workObserver: Observer<WorkInfo?>? = null
|
||||||
|
|
||||||
fun load(bookRemoteId: String) {
|
fun load(bookRemoteId: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.update { it.copy(isLoading = true, error = null) }
|
_state.update { it.copy(isLoading = true, error = null) }
|
||||||
|
|
@ -123,7 +129,7 @@ class TtsModel
|
||||||
speed = _state.value.speed,
|
speed = _state.value.speed,
|
||||||
)
|
)
|
||||||
val job = result.getOrElse {
|
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
|
return@launch
|
||||||
}
|
}
|
||||||
_state.update { it.copy(isCreatingJob = false, job = job) }
|
_state.update { it.copy(isCreatingJob = false, job = job) }
|
||||||
|
|
@ -168,9 +174,13 @@ class TtsModel
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeWorker(workId: java.util.UUID) {
|
private fun observeWorker(workId: java.util.UUID) {
|
||||||
workManager.getWorkInfoByIdLiveData(workId)
|
workObserver?.let { observer ->
|
||||||
.observeForever { info: WorkInfo? ->
|
observedWork?.removeObserver(observer)
|
||||||
info ?: return@observeForever
|
}
|
||||||
|
|
||||||
|
val liveData = workManager.getWorkInfoByIdLiveData(workId)
|
||||||
|
val observer: Observer<WorkInfo?> = Observer { info ->
|
||||||
|
info ?: return@Observer
|
||||||
when (info.state) {
|
when (info.state) {
|
||||||
WorkInfo.State.SUCCEEDED -> {
|
WorkInfo.State.SUCCEEDED -> {
|
||||||
val filePath = info.outputData.getString(TtsDownloadWorker.KEY_OUTPUT_FILE_PATH)
|
val filePath = info.outputData.getString(TtsDownloadWorker.KEY_OUTPUT_FILE_PATH)
|
||||||
|
|
@ -179,7 +189,7 @@ class TtsModel
|
||||||
|
|
||||||
WorkInfo.State.FAILED -> {
|
WorkInfo.State.FAILED -> {
|
||||||
val message = info.outputData.getString(TtsDownloadWorker.KEY_ERROR_MESSAGE)
|
val message = info.outputData.getString(TtsDownloadWorker.KEY_ERROR_MESSAGE)
|
||||||
_state.update { it.copy(error = message ?: "TTS download failed") }
|
_state.update { it.copy(error = message ?: application.getString(R.string.tts_download_failed)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
WorkInfo.State.RUNNING -> {
|
WorkInfo.State.RUNNING -> {
|
||||||
|
|
@ -195,5 +205,16 @@ class TtsModel
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observedWork = liveData
|
||||||
|
workObserver = observer
|
||||||
|
liveData.observeForever(observer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
workObserver?.let { observer ->
|
||||||
|
observedWork?.removeObserver(observer)
|
||||||
|
}
|
||||||
|
super.onCleared()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,12 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import org.dueattendant149.bookshelf.R
|
||||||
import org.dueattendant149.bookshelf.presentation.player.PlayerState
|
import org.dueattendant149.bookshelf.presentation.player.PlayerState
|
||||||
import org.dueattendant149.bookshelf.ui.navigator.LocalNavigator
|
import org.dueattendant149.bookshelf.ui.navigator.LocalNavigator
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
@ -73,7 +75,7 @@ fun PlayerContent(
|
||||||
IconButton(onClick = { navigator.pop() }) {
|
IconButton(onClick = { navigator.pop() }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
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)
|
val currentTrack = playerState.tracks.getOrNull(playerState.currentTrackIndex)
|
||||||
Text(
|
Text(
|
||||||
text = currentTrack?.title ?: "No track",
|
text = currentTrack?.title ?: stringResource(R.string.player_no_track),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
|
@ -145,10 +147,10 @@ fun PlayerContent(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = onSeekBackward) {
|
IconButton(onClick = onSeekBackward) {
|
||||||
Text("-10s")
|
Text(stringResource(R.string.player_seek_backward))
|
||||||
}
|
}
|
||||||
IconButton(onClick = onSkipPrevious) {
|
IconButton(onClick = onSkipPrevious) {
|
||||||
Icon(Icons.Default.SkipPrevious, contentDescription = "Previous")
|
Icon(Icons.Default.SkipPrevious, contentDescription = stringResource(R.string.player_previous))
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onPlayPause,
|
onClick = onPlayPause,
|
||||||
|
|
@ -156,15 +158,17 @@ fun PlayerContent(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (playerState.isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
|
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)
|
modifier = Modifier.size(48.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = onSkipNext) {
|
IconButton(onClick = onSkipNext) {
|
||||||
Icon(Icons.Default.SkipNext, contentDescription = "Next")
|
Icon(Icons.Default.SkipNext, contentDescription = stringResource(R.string.player_next))
|
||||||
}
|
}
|
||||||
IconButton(onClick = onSeekForward) {
|
IconButton(onClick = onSeekForward) {
|
||||||
Text("+30s")
|
Text(stringResource(R.string.player_seek_forward))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,15 @@ private fun VoiceItem(
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(voice.name) },
|
headlineContent = { Text(voice.name) },
|
||||||
supportingContent = { Text("${voice.language} • ${voice.engine}") },
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
R.string.tts_voice_description,
|
||||||
|
voice.language,
|
||||||
|
voice.engine
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
Text(stringResource(R.string.tts_selected_label))
|
Text(stringResource(R.string.tts_selected_label))
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,13 @@
|
||||||
<!-- Audio playback -->
|
<!-- Audio playback -->
|
||||||
<string name="audio_playback_channel">Audio playback</string>
|
<string name="audio_playback_channel">Audio playback</string>
|
||||||
<string name="audio_playback_channel_description">Shows the currently playing audiobook</string>
|
<string name="audio_playback_channel_description">Shows the currently playing audiobook</string>
|
||||||
|
<string name="player_no_track">No track</string>
|
||||||
|
<string name="player_seek_backward">-10s</string>
|
||||||
|
<string name="player_seek_forward">+30s</string>
|
||||||
|
<string name="player_previous">Previous</string>
|
||||||
|
<string name="player_next">Next</string>
|
||||||
|
<string name="player_play">Play</string>
|
||||||
|
<string name="player_pause">Pause</string>
|
||||||
|
|
||||||
<!-- Screens -->
|
<!-- Screens -->
|
||||||
<string name="library_screen">Library</string>
|
<string name="library_screen">Library</string>
|
||||||
|
|
@ -569,5 +576,8 @@
|
||||||
<string name="tts_speed_label">Speed: %1$.2fx</string>
|
<string name="tts_speed_label">Speed: %1$.2fx</string>
|
||||||
<string name="tts_create_job_button">Create TTS job</string>
|
<string name="tts_create_job_button">Create TTS job</string>
|
||||||
<string name="tts_book_id_label">Book: %1$s</string>
|
<string name="tts_book_id_label">Book: %1$s</string>
|
||||||
|
<string name="tts_voice_description">%1$s • %2$s</string>
|
||||||
|
<string name="tts_create_job_failed">Failed to create TTS job</string>
|
||||||
|
<string name="tts_download_failed">TTS download failed</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue