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

View file

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

View file

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