GetBooksById → GetBookById(single book) + Canceling all ReaderViewModel.onEvent calls on screen quit(fixes bugs with text being translated after quitting) + Fixed: Progress resets to 0% if leave when Reader is loading + Fixed: Book text does not show correctly after loading (v1.0.1)

This commit is contained in:
acclorite 2024-06-09 15:54:50 +03:00
parent 9dfa06973f
commit e033294df0
5 changed files with 862 additions and 831 deletions

View file

@ -0,0 +1,12 @@
package ua.acclorite.book_story.domain.use_case
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class GetBookById @Inject constructor(private val repository: BookRepository) {
suspend fun execute(id: Int): Book? {
return repository.getBooksById(listOf(id)).firstOrNull()
}
}

View file

@ -22,12 +22,12 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -84,8 +84,6 @@ fun ReaderScreenRoot() {
val libraryViewModel: LibraryViewModel = hiltViewModel()
val historyViewModel: HistoryViewModel = hiltViewModel()
val loading = remember { mutableStateOf(true) }
val state = viewModel.state.collectAsState()
val mainState = mainViewModel.state.collectAsState()
@ -119,14 +117,11 @@ fun ReaderScreenRoot() {
it.asString(context),
Toast.LENGTH_LONG
).show()
},
onLoaded = {
loading.value = false
}
)
}
LaunchedEffect(canScroll) {
if (!canScroll && !state.value.showMenu && !loading.value) {
if (!canScroll && !state.value.showMenu && !state.value.loading) {
viewModel.onEvent(ReaderEvent.OnShowHideMenu(context = context))
}
}
@ -135,16 +130,20 @@ fun ReaderScreenRoot() {
firstVisibleItemIndex = firstVisibleItemIndex.value,
firstVisibleItemOffset = firstVisibleItemOffset.value,
navigator = navigator,
isReaderLoading = loading.value,
onLibraryEvent = libraryViewModel::onEvent,
onHistoryEvent = historyViewModel::onEvent
)
}
DisposableEffect(Unit) {
onDispose {
viewModel.clearViewModel()
}
}
ReaderScreen(
state = state,
mainState = mainState,
loading = loading,
navigator = navigator,
onEvent = viewModel::onEvent,
onMainEvent = mainViewModel::onEvent,
@ -158,7 +157,6 @@ fun ReaderScreenRoot() {
private fun ReaderScreen(
state: State<ReaderState>,
mainState: State<MainState>,
loading: State<Boolean>,
navigator: Navigator,
onEvent: (ReaderEvent) -> Unit,
onMainEvent: (MainEvent) -> Unit,
@ -355,7 +353,7 @@ private fun ReaderScreen(
.fillMaxSize()
.background(backgroundColor)
.then(
if (!loading.value && toolbarHidden) {
if (!state.value.loading && toolbarHidden) {
Modifier
.clickable(
interactionSource = null,
@ -372,7 +370,6 @@ private fun ReaderScreen(
),
verticalArrangement = Arrangement.spacedBy(paragraphHeight)
) {
if (state.value.text.isNotEmpty()) {
item {
DisableSelection {
ReaderStartItem(state = state)
@ -380,7 +377,6 @@ private fun ReaderScreen(
Spacer(modifier = Modifier.height(18.dp))
}
}
customItems(
text, key = { key -> key.first }
@ -403,7 +399,6 @@ private fun ReaderScreen(
)
}
if (state.value.text.isNotEmpty()) {
item {
Spacer(modifier = Modifier.height(18.dp))
@ -419,9 +414,8 @@ private fun ReaderScreen(
}
}
}
}
if (loading.value || state.value.errorMessage != null) {
if (state.value.loading || state.value.errorMessage != null) {
Column(
modifier = Modifier
.fillMaxSize()
@ -429,7 +423,7 @@ private fun ReaderScreen(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
if (!loading.value) {
if (!state.value.loading) {
IsError(
errorMessage = state.value.errorMessage!!.asString(),
icon = painterResource(id = R.drawable.error),

View file

@ -13,10 +13,9 @@ import ua.acclorite.book_story.presentation.data.Navigator
@Immutable
sealed class ReaderEvent {
data class OnTextIsEmpty(val onLoaded: () -> Unit) : ReaderEvent()
data object OnTextIsEmpty : ReaderEvent()
data class OnLoadText(
val refreshList: (Book) -> Unit,
val onLoaded: () -> Unit,
val onError: (UIText) -> Unit,
val onTextIsEmpty: () -> Unit
) : ReaderEvent()

View file

@ -22,6 +22,7 @@ data class ReaderState(
val isTranslating: Boolean = false,
val errorMessage: UIText? = null,
val loading: Boolean = true,
val showMenu: Boolean = false,
val lockMenu: Boolean = false,

View file

@ -14,6 +14,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
@ -31,7 +32,7 @@ import ua.acclorite.book_story.domain.model.LineWithTranslation
import ua.acclorite.book_story.domain.model.SelectableLanguage
import ua.acclorite.book_story.domain.use_case.DeleteLanguageModel
import ua.acclorite.book_story.domain.use_case.DownloadLanguageModel
import ua.acclorite.book_story.domain.use_case.GetBooksById
import ua.acclorite.book_story.domain.use_case.GetBookById
import ua.acclorite.book_story.domain.use_case.GetLatestHistory
import ua.acclorite.book_story.domain.use_case.GetText
import ua.acclorite.book_story.domain.use_case.IdentifyLanguage
@ -42,7 +43,6 @@ import ua.acclorite.book_story.domain.util.ID
import ua.acclorite.book_story.domain.util.LanguageCode
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.domain.util.UIText
import ua.acclorite.book_story.presentation.data.Argument
import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.data.removeKey
@ -61,7 +61,7 @@ class ReaderViewModel @Inject constructor(
private val updateBooks: UpdateBooks,
private val getText: GetText,
private val getLatestHistory: GetLatestHistory,
private val getBooksById: GetBooksById,
private val getBookById: GetBookById,
private val identifyLanguage: IdentifyLanguage,
private val isLanguageModelDownloaded: IsLanguageModelDownloaded,
private val downloadLanguageModel: DownloadLanguageModel,
@ -72,6 +72,8 @@ class ReaderViewModel @Inject constructor(
private val _state = MutableStateFlow(ReaderState())
val state = _state.asStateFlow()
private var eventJob = SupervisorJob()
private var dismissTranslatorJob: Job? = null
private var cancelTranslationJob: Job? = null
private var translatorJob: Job? = null
@ -79,21 +81,18 @@ class ReaderViewModel @Inject constructor(
private var downloadLanguageJob: Job? = null
fun onEvent(event: ReaderEvent) {
viewModelScope.launch(eventJob + Dispatchers.Main) {
when (event) {
is ReaderEvent.OnTextIsEmpty -> {
_state.update {
event.onLoaded()
it.copy(
errorMessage = UIText.StringResource(R.string.error_no_text),
loading = false,
errorMessage = UIText.StringResource(R.string.error_no_text)
)
}
}
is ReaderEvent.OnLoadText -> {
viewModelScope.launch {
cancelJobs()
joinJobs()
launch(Dispatchers.IO) {
val text = getText.execute(_state.value.book.textPath)
@ -142,7 +141,7 @@ class ReaderViewModel @Inject constructor(
)
event.refreshList(_state.value.book)
viewModelScope.launch {
launch {
snapshotFlow {
_state.value.listState.layoutInfo.totalItemsCount
}.collectLatest { itemsCount ->
@ -154,7 +153,7 @@ class ReaderViewModel @Inject constructor(
var loaded = false
for (i in 1..100) {
try {
_state.value.listState.scrollToItem(
_state.value.listState.requestScrollToItem(
index,
offset
)
@ -175,7 +174,11 @@ class ReaderViewModel @Inject constructor(
}
delay(100)
event.onLoaded()
_state.update {
it.copy(
loading = false
)
}
if (
_state.value.book.translateWhenOpen &&
@ -194,11 +197,10 @@ class ReaderViewModel @Inject constructor(
}
}
}
}
is ReaderEvent.OnShowHideMenu -> {
if (_state.value.lockMenu) {
return
return@launch
}
val shouldShow = event.show ?: !_state.value.showMenu
@ -229,13 +231,14 @@ class ReaderViewModel @Inject constructor(
is ReaderEvent.OnShowHideTranslatorBottomSheet -> {
_state.update {
it.copy(
showTranslatorBottomSheet = event.show ?: !it.showTranslatorBottomSheet
showTranslatorBottomSheet = event.show
?: !it.showTranslatorBottomSheet
)
}
}
is ReaderEvent.OnChangeTranslatorSettings -> {
viewModelScope.launch(Dispatchers.IO) {
launch(Dispatchers.IO) {
val book = _state.value.book
val updatedBook = book.copy(
enableTranslator = event.enableTranslator ?: book.enableTranslator,
@ -243,7 +246,8 @@ class ReaderViewModel @Inject constructor(
translateTo = event.translateTo ?: book.translateTo,
doubleClickTranslation = event.doubleClickTranslation
?: book.doubleClickTranslation,
translateWhenOpen = event.translateWhenOpen ?: book.translateWhenOpen
translateWhenOpen = event.translateWhenOpen
?: book.translateWhenOpen
)
_state.update {
@ -271,7 +275,7 @@ class ReaderViewModel @Inject constructor(
)
}
viewModelScope.launch {
launch {
joinJobs()
translatorJob = launch(Dispatchers.IO) {
@ -308,12 +312,14 @@ class ReaderViewModel @Inject constructor(
yield()
if (_state.value.book.translateFrom == "auto") {
val identifiedLanguages = mutableListOf<Pair<LanguageCode, ID>>()
val identifiedLanguages =
mutableListOf<Pair<LanguageCode, ID>>()
val errors = mutableMapOf<ID, LineWithTranslation>()
for (line in linesToTranslate.toMap()) {
yield()
val result = identifyLanguage.execute(line.value.originalLine)
val result =
identifyLanguage.execute(line.value.originalLine)
when (result) {
is Resource.Success -> {
@ -390,7 +396,8 @@ class ReaderViewModel @Inject constructor(
it.uppercase()
}
val translateToCanceled = _state.value.languagesToTranslate.find {
val translateToCanceled =
_state.value.languagesToTranslate.find {
it.languageCode == _state.value.book.translateTo
}?.isCanceled ?: false
@ -420,7 +427,8 @@ class ReaderViewModel @Inject constructor(
it.uppercase()
}
val isCanceled = _state.value.languagesToTranslate.find {
val isCanceled =
_state.value.languagesToTranslate.find {
it.languageCode == language.first
}?.isCanceled ?: false
@ -460,9 +468,11 @@ class ReaderViewModel @Inject constructor(
val line = linesToTranslate[closestLine] ?: continue
yield()
val sourceLanguage = if (_state.value.book.translateFrom == "auto") {
val sourceLanguage =
if (_state.value.book.translateFrom == "auto") {
when (
val result = identifyLanguage.execute(line.originalLine)
val result =
identifyLanguage.execute(line.originalLine)
) {
is Resource.Success -> result.data!!
is Resource.Error -> continue
@ -556,7 +566,7 @@ class ReaderViewModel @Inject constructor(
)
}
viewModelScope.launch {
launch {
joinJobs()
dismissTranslatorJob = launch(Dispatchers.IO) {
@ -594,7 +604,7 @@ class ReaderViewModel @Inject constructor(
}
is ReaderEvent.OnCancelTranslation -> {
viewModelScope.launch {
launch {
cancelTranslationJob?.join()
undoTranslationJob?.join()
dismissTranslatorJob?.cancel()
@ -627,7 +637,7 @@ class ReaderViewModel @Inject constructor(
}
is ReaderEvent.OnUndoTranslation -> {
viewModelScope.launch {
launch {
cancelTranslationJob?.join()
undoTranslationJob?.join()
@ -670,7 +680,7 @@ class ReaderViewModel @Inject constructor(
}
is ReaderEvent.OnDownloadLanguages -> {
viewModelScope.launch {
launch {
downloadLanguageJob?.cancel()
downloadLanguageJob = launch(Dispatchers.IO) {
@ -722,7 +732,8 @@ class ReaderViewModel @Inject constructor(
languageCode = lang.languageCode,
onCompleted = {
launch(Dispatchers.IO) {
val modelDownloaded = isLanguageModelDownloaded.execute(
val modelDownloaded =
isLanguageModelDownloaded.execute(
lang.languageCode
)
@ -828,7 +839,7 @@ class ReaderViewModel @Inject constructor(
}
is ReaderEvent.OnSelectLanguage -> {
viewModelScope.launch(Dispatchers.IO) {
launch(Dispatchers.IO) {
val editedList = _state.value.languagesToTranslate.map {
if (it.languageCode == event.language.languageCode) {
it.copy(
@ -848,7 +859,7 @@ class ReaderViewModel @Inject constructor(
}
is ReaderEvent.OnUpdateLanguage -> {
viewModelScope.launch(Dispatchers.IO) {
launch(Dispatchers.IO) {
val editedList = _state.value.languagesToTranslate.map {
if (it.languageCode == event.languageCode) {
event.calculation(it)
@ -866,8 +877,7 @@ class ReaderViewModel @Inject constructor(
}
is ReaderEvent.OnGoBack -> {
viewModelScope.launch {
cancelJobs()
launch {
_state.update {
it.copy(
lockMenu = true
@ -879,49 +889,35 @@ class ReaderViewModel @Inject constructor(
event.context.window.decorView
)
if (_state.value.listState.layoutInfo.totalItemsCount > 0) {
val firstVisibleItemIndex = _state.value.listState.firstVisibleItemIndex
val firstVisibleItemOffset =
_state.value.listState.firstVisibleItemScrollOffset
val lastVisibleItemIndex = _state.value.listState.layoutInfo
.visibleItemsInfo.last().index
val progress = if (firstVisibleItemIndex > 0) {
if (lastVisibleItemIndex >= (_state.value.listState.layoutInfo.totalItemsCount - 1)) {
1f
} else {
(firstVisibleItemIndex.toFloat() / (_state.value.text.size - 1)
.toFloat())
}
} else {
0f
}
val listState = _state.value.listState
if (
!_state.value.loading &&
listState.layoutInfo.totalItemsCount != 0 &&
_state.value.text.isNotEmpty()
) {
_state.update {
it.copy(
book = it.book.copy(
progress = progress,
scrollIndex = firstVisibleItemIndex,
scrollOffset = firstVisibleItemOffset
progress = calculateProgress(),
scrollIndex = listState.firstVisibleItemIndex,
scrollOffset = listState.firstVisibleItemScrollOffset
)
)
}
}
updateBooks.execute(
listOf(_state.value.book)
)
event.navigator.putArgument(
Argument("book", _state.value.book.id)
)
event.refreshList(_state.value.book)
}
insetsController.show(WindowInsetsCompat.Type.systemBars())
event.navigate(event.navigator)
}
}
is ReaderEvent.OnScroll -> {
viewModelScope.launch {
launch {
val scrollTo = (_state.value.text.size * event.progress).roundToInt()
_state.value.listState.scrollToItem(
@ -931,7 +927,7 @@ class ReaderViewModel @Inject constructor(
}
is ReaderEvent.OnChangeProgress -> {
viewModelScope.launch(Dispatchers.IO) {
launch(Dispatchers.IO) {
_state.update {
it.copy(
book = it.book.copy(
@ -945,18 +941,12 @@ class ReaderViewModel @Inject constructor(
updateBooks.execute(
listOf(_state.value.book)
)
event.navigator.putArgument(
Argument(
"book",
_state.value.book.id
)
)
event.refreshList(_state.value.book)
}
}
is ReaderEvent.OnShowHideSettingsBottomSheet -> {
viewModelScope.launch(Dispatchers.IO) {
launch(Dispatchers.IO) {
_state.update {
it.copy(
currentPage = if (it.showSettingsBottomSheet) it.currentPage else 0,
@ -967,13 +957,13 @@ class ReaderViewModel @Inject constructor(
}
is ReaderEvent.OnScrollToSettingsPage -> {
viewModelScope.launch {
launch {
event.pagerState.scrollToPage(event.page)
}
}
is ReaderEvent.OnMoveBookToAlreadyRead -> {
viewModelScope.launch {
launch {
onEvent(
ReaderEvent.OnGoBack(
event.context,
@ -983,16 +973,14 @@ class ReaderViewModel @Inject constructor(
)
)
val firstVisibleItemIndex = _state.value.listState.firstVisibleItemIndex
val firstVisibleItemOffset = _state.value.listState.firstVisibleItemScrollOffset
_state.update {
val listState = it.listState
it.copy(
book = it.book.copy(
category = Category.ALREADY_READ,
progress = 1f,
scrollIndex = firstVisibleItemIndex,
scrollOffset = firstVisibleItemOffset
scrollIndex = listState.firstVisibleItemIndex,
scrollOffset = listState.firstVisibleItemScrollOffset
)
)
}
@ -1007,14 +995,12 @@ class ReaderViewModel @Inject constructor(
}.size - 1
)
cancelJobs()
event.navigator.navigate(Screen.LIBRARY, true)
}
}
is ReaderEvent.OnOpenTranslator -> {
viewModelScope.launch {
launch {
val translatorIntent = Intent()
val browserIntent = Intent()
@ -1023,7 +1009,10 @@ class ReaderViewModel @Inject constructor(
browserIntent.action = Intent.ACTION_WEB_SEARCH
translatorIntent.putExtra(Intent.EXTRA_PROCESS_TEXT, event.textToTranslate)
translatorIntent.putExtra(
Intent.EXTRA_PROCESS_TEXT,
event.textToTranslate
)
translatorIntent.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, true)
browserIntent.putExtra(
@ -1046,7 +1035,7 @@ class ReaderViewModel @Inject constructor(
}
is ReaderEvent.OnOpenDictionary -> {
viewModelScope.launch {
launch {
val browserIntent = Intent()
browserIntent.action = Intent.ACTION_WEB_SEARCH
@ -1066,13 +1055,6 @@ class ReaderViewModel @Inject constructor(
}
}
}
private fun cancelJobs() {
cancelTranslationJob?.cancel()
translatorJob?.cancel()
dismissTranslatorJob?.cancel()
undoTranslationJob?.cancel()
downloadLanguageJob?.cancel()
}
private suspend fun joinJobs() {
@ -1082,7 +1064,6 @@ class ReaderViewModel @Inject constructor(
undoTranslationJob?.join()
}
private suspend fun findClosestLineToTranslate(
translatingLines: Map<ID, LineWithTranslation>
): ID? {
@ -1248,8 +1229,7 @@ class ReaderViewModel @Inject constructor(
navigator: Navigator,
context: ComponentActivity,
refreshList: (Book) -> Unit,
onError: (UIText) -> Unit,
onLoaded: () -> Unit
onError: (UIText) -> Unit
) {
viewModelScope.launch(Dispatchers.IO) {
val bookId = navigator.retrieveArgument("book") as? Int
@ -1259,65 +1239,66 @@ class ReaderViewModel @Inject constructor(
return@launch
}
val book = getBooksById.execute(listOf(bookId))
val book = getBookById.execute(bookId)
if (book.isEmpty()) {
if (book == null) {
navigator.navigateBack()
return@launch
}
_state.update {
ReaderState(book = book.first())
ReaderState(book = book)
}
viewModelScope.launch {
clear()
launch {
onEvent(ReaderEvent.OnShowHideMenu(false, context))
}
onEvent(
ReaderEvent.OnLoadText(
refreshList = { refreshList(it) },
onLoaded = {
onLoaded()
},
onError = {
onError(it)
},
onTextIsEmpty = {
onEvent(ReaderEvent.OnTextIsEmpty(onLoaded = { onLoaded() }))
onEvent(ReaderEvent.OnTextIsEmpty)
}
)
)
}
}
}
@OptIn(FlowPreview::class)
suspend fun onUpdateProgress(
firstVisibleItemIndex: Int,
firstVisibleItemOffset: Int,
navigator: Navigator,
isReaderLoading: Boolean,
onLibraryEvent: (LibraryEvent) -> Unit,
onHistoryEvent: (HistoryEvent) -> Unit
) {
snapshotFlow {
firstVisibleItemIndex to firstVisibleItemOffset
}.debounce(500).collectLatest { items ->
}
.debounce(200)
.collectLatest { items ->
val listState = _state.value.listState
if (
!isReaderLoading &&
!_state.value.loading &&
_state.value.text.isNotEmpty() &&
_state.value.listState.layoutInfo.totalItemsCount > 0
listState.layoutInfo.totalItemsCount > 0
) {
val lastVisibleItemIndex = _state.value.listState.layoutInfo.visibleItemsInfo
.last().index
val lastVisibleItemIndex = listState
.layoutInfo
.visibleItemsInfo
.last()
.index
val totalItemsCount = listState.layoutInfo.totalItemsCount - 1
val progress = if (items.first > 0) {
if (
lastVisibleItemIndex >= (_state.value.listState.layoutInfo.totalItemsCount - 1)
) {
if (lastVisibleItemIndex >= totalItemsCount) {
1f
} else {
(items.first.toFloat() / (_state.value.text.size - 1).toFloat())
items.first / (_state.value.text.size - 1).toFloat()
}
} else {
0f
@ -1338,4 +1319,48 @@ class ReaderViewModel @Inject constructor(
}
}
}
private fun calculateProgress(): Float {
val listState = _state.value.listState
if (
_state.value.loading ||
listState.layoutInfo.totalItemsCount == 0 ||
_state.value.text.isEmpty()
) {
return _state.value.book.progress
}
val lastVisibleItemIndex = listState.layoutInfo
.visibleItemsInfo.last().index
val totalItemsCount = listState.layoutInfo.totalItemsCount - 1
if (listState.firstVisibleItemIndex == 0) {
return 0f
}
if (lastVisibleItemIndex >= totalItemsCount) {
return 1f
}
return listState.firstVisibleItemIndex / (_state.value.text.size - 1).toFloat()
}
private suspend fun clear() {
eventJob.cancel()
eventJob.join()
eventJob = SupervisorJob()
}
fun clearViewModel() {
viewModelScope.launch(Dispatchers.Main) {
_state.update {
ReaderState()
}
eventJob.cancel()
eventJob.join()
eventJob = SupervisorJob()
}
}
}