diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBookById.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBookById.kt new file mode 100644 index 00000000..e4955eeb --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBookById.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt index 22d88a63..2a9bd067 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt @@ -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, mainState: State, - loading: State, 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,14 +370,12 @@ private fun ReaderScreen( ), verticalArrangement = Arrangement.spacedBy(paragraphHeight) ) { - if (state.value.text.isNotEmpty()) { - item { - DisableSelection { - ReaderStartItem(state = state) - } - - Spacer(modifier = Modifier.height(18.dp)) + item { + DisableSelection { + ReaderStartItem(state = state) } + + Spacer(modifier = Modifier.height(18.dp)) } customItems( @@ -403,25 +399,23 @@ private fun ReaderScreen( ) } - if (state.value.text.isNotEmpty()) { - item { - Spacer(modifier = Modifier.height(18.dp)) + item { + Spacer(modifier = Modifier.height(18.dp)) - DisableSelection { - ReaderEndItem( - state = state, - onEvent = onEvent, - onLibraryEvent = onLibraryEvent, - onHistoryUpdateEvent = onHistoryEvent, - ) - } + DisableSelection { + ReaderEndItem( + state = state, + onEvent = onEvent, + onLibraryEvent = onLibraryEvent, + onHistoryUpdateEvent = onHistoryEvent, + ) } } } } } - 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), diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt index 4ea7e720..99008bbd 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt @@ -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() diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt index 5b190ebc..75442a85 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt @@ -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, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt index 4533c717..1248bd0f 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt @@ -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) { - when (event) { - is ReaderEvent.OnTextIsEmpty -> { - _state.update { - event.onLoaded() - it.copy( - errorMessage = UIText.StringResource(R.string.error_no_text), - ) + viewModelScope.launch(eventJob + Dispatchers.Main) { + when (event) { + is ReaderEvent.OnTextIsEmpty -> { + _state.update { + it.copy( + loading = false, + errorMessage = UIText.StringResource(R.string.error_no_text) + ) + } } - } - - is ReaderEvent.OnLoadText -> { - viewModelScope.launch { - cancelJobs() - joinJobs() + is ReaderEvent.OnLoadText -> { 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,295 +197,344 @@ class ReaderViewModel @Inject constructor( } } } - } - is ReaderEvent.OnShowHideMenu -> { - if (_state.value.lockMenu) { - return - } - - val shouldShow = event.show ?: !_state.value.showMenu - - val insetsController = WindowCompat.getInsetsController( - event.context.window, - event.context.window.decorView - ) - - insetsController.systemBarsBehavior = - WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - - insetsController.apply { - if (shouldShow) { - show(WindowInsetsCompat.Type.systemBars()) - } else { - hide(WindowInsetsCompat.Type.systemBars()) + is ReaderEvent.OnShowHideMenu -> { + if (_state.value.lockMenu) { + return@launch } - } - _state.update { - it.copy( - showMenu = shouldShow - ) - } - } + val shouldShow = event.show ?: !_state.value.showMenu - is ReaderEvent.OnShowHideTranslatorBottomSheet -> { - _state.update { - it.copy( - showTranslatorBottomSheet = event.show ?: !it.showTranslatorBottomSheet + val insetsController = WindowCompat.getInsetsController( + event.context.window, + event.context.window.decorView ) - } - } - is ReaderEvent.OnChangeTranslatorSettings -> { - viewModelScope.launch(Dispatchers.IO) { - val book = _state.value.book - val updatedBook = book.copy( - enableTranslator = event.enableTranslator ?: book.enableTranslator, - translateFrom = event.translateFrom ?: book.translateFrom, - translateTo = event.translateTo ?: book.translateTo, - doubleClickTranslation = event.doubleClickTranslation - ?: book.doubleClickTranslation, - translateWhenOpen = event.translateWhenOpen ?: book.translateWhenOpen - ) + insetsController.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + + insetsController.apply { + if (shouldShow) { + show(WindowInsetsCompat.Type.systemBars()) + } else { + hide(WindowInsetsCompat.Type.systemBars()) + } + } _state.update { it.copy( - book = updatedBook + showMenu = shouldShow ) } - updateBooks.execute(listOf(updatedBook)) - } - } - - is ReaderEvent.OnShowHideLanguageBottomSheet -> { - _state.update { - it.copy( - showLanguageBottomSheet = event.show ?: !it.showLanguageBottomSheet, - languageBottomSheetTranslateFrom = event.translateFrom ?: false - ) - } - } - - is ReaderEvent.OnTranslateText -> { - _state.update { - it.copy( - isTranslating = true - ) } - viewModelScope.launch { - joinJobs() + is ReaderEvent.OnShowHideTranslatorBottomSheet -> { + _state.update { + it.copy( + showTranslatorBottomSheet = event.show + ?: !it.showTranslatorBottomSheet + ) + } + } - translatorJob = launch(Dispatchers.IO) { - onEvent( - ReaderEvent.OnChangeTranslatorSettings( - enableTranslator = true - ) + is ReaderEvent.OnChangeTranslatorSettings -> { + launch(Dispatchers.IO) { + val book = _state.value.book + val updatedBook = book.copy( + enableTranslator = event.enableTranslator ?: book.enableTranslator, + translateFrom = event.translateFrom ?: book.translateFrom, + translateTo = event.translateTo ?: book.translateTo, + doubleClickTranslation = event.doubleClickTranslation + ?: book.doubleClickTranslation, + translateWhenOpen = event.translateWhenOpen + ?: book.translateWhenOpen ) - val keys = event.keys ?: _state.value.text.keys - var linesToTranslate = if (keys.size >= _state.value.text.size) { - _state.value.text - } else { - _state.value.text.filterKeys { keys.any { key -> key == it } } - } - - val languages = mutableMapOf?>() - - yield() - linesToTranslate = linesToTranslate.updateWithCopy { + _state.update { it.copy( - isTranslationLoading = true, - isTranslationFailed = false, - errorMessage = null, - useTranslation = false + book = updatedBook ) } + updateBooks.execute(listOf(updatedBook)) + } + } - yield() - onUpdateTextLines( - linesToTranslate, - wholeText = linesToTranslate.size >= _state.value.text.size + is ReaderEvent.OnShowHideLanguageBottomSheet -> { + _state.update { + it.copy( + showLanguageBottomSheet = event.show ?: !it.showLanguageBottomSheet, + languageBottomSheetTranslateFrom = event.translateFrom ?: false ) + } + } - yield() - if (_state.value.book.translateFrom == "auto") { - val identifiedLanguages = mutableListOf>() - val errors = mutableMapOf() + is ReaderEvent.OnTranslateText -> { + _state.update { + it.copy( + isTranslating = true + ) + } + + launch { + joinJobs() + + translatorJob = launch(Dispatchers.IO) { + onEvent( + ReaderEvent.OnChangeTranslatorSettings( + enableTranslator = true + ) + ) + + val keys = event.keys ?: _state.value.text.keys + var linesToTranslate = if (keys.size >= _state.value.text.size) { + _state.value.text + } else { + _state.value.text.filterKeys { keys.any { key -> key == it } } + } + + val languages = mutableMapOf?>() + + yield() + linesToTranslate = linesToTranslate.updateWithCopy { + it.copy( + isTranslationLoading = true, + isTranslationFailed = false, + errorMessage = null, + useTranslation = false + ) + } + + yield() + onUpdateTextLines( + linesToTranslate, + wholeText = linesToTranslate.size >= _state.value.text.size + ) + + yield() + if (_state.value.book.translateFrom == "auto") { + val identifiedLanguages = + mutableListOf>() + val errors = mutableMapOf() + + for (line in linesToTranslate.toMap()) { + yield() + val result = + identifyLanguage.execute(line.value.originalLine) + + when (result) { + is Resource.Success -> { + identifiedLanguages.add( + result.data!! to line.key + ) + } + + is Resource.Error -> { + errors[line.key] = line.value.copy( + isTranslationFailed = true, + isTranslationLoading = false, + errorMessage = result.message + ) + } + } + } - for (line in linesToTranslate.toMap()) { yield() - val result = identifyLanguage.execute(line.value.originalLine) + if (errors.isNotEmpty()) { + onUpdateTextLines( + errors, + errors.size >= _state.value.text.size + ) + linesToTranslate = linesToTranslate.removeKeys(errors.keys) + } + + yield() + languages.putAll( + identifiedLanguages + .groupBy { + it.first + }.mapValues { (_, value) -> + value.map { it.second } + } + ) + } else { + languages[_state.value.book.translateFrom] = emptyList() + } + + yield() + if (languages.isEmpty()) { + _state.update { + it.copy( + isTranslating = false + ) + } + + yield() + launch(Dispatchers.Main) { + event.error(UIText.StringResource(R.string.error_languages_not_identified)) + } + return@launch + } + + var shouldShowDialog = false + + if (languages.any { !isLanguageModelDownloaded.execute(it.key) }) { + shouldShowDialog = true + } + + if (!isLanguageModelDownloaded.execute(_state.value.book.translateTo)) { + shouldShowDialog = true + } + + yield() + if (shouldShowDialog) { + val languagesToDownload = mutableListOf() + + val translateToLocale = Locale(_state.value.book.translateTo) + val translateToDisplayLanguage = translateToLocale + .getDisplayLanguage(translateToLocale) + .replaceFirstChar { + it.uppercase() + } + + val translateToCanceled = + _state.value.languagesToTranslate.find { + it.languageCode == _state.value.book.translateTo + }?.isCanceled ?: false + + languagesToDownload.add( + SelectableLanguage( + languageCode = _state.value.book.translateTo, + displayLanguage = translateToDisplayLanguage, + isDownloading = false, + isCanceled = translateToCanceled, + isError = false, + isDownloaded = isLanguageModelDownloaded.execute(_state.value.book.translateTo), + isSelected = true, + canUnselect = false, + occurrences = languages[_state.value.book.translateTo] + ) + ) + languagesToDownload.addAll( + languages + .filterNot { it.key == _state.value.book.translateTo } + .toList() + .sortedByDescending { it.second?.size } + .mapIndexed { index, language -> + val locale = Locale(language.first) + val displayLanguage = locale + .getDisplayLanguage(locale) + .replaceFirstChar { + it.uppercase() + } + + val isCanceled = + _state.value.languagesToTranslate.find { + it.languageCode == language.first + }?.isCanceled ?: false + + SelectableLanguage( + languageCode = language.first, + displayLanguage = displayLanguage, + isDownloading = false, + isCanceled = isCanceled, + isDownloaded = isLanguageModelDownloaded.execute( + language.first + ), + isError = false, + isSelected = index == 0, + canUnselect = index > 0, + occurrences = language.second + ) + } + ) + + yield() + _state.update { + it.copy( + languagesToTranslate = languagesToDownload, + showDownloadLanguageDialog = true + ) + } + return@launch + } + + yield() + for (i in 0.. result.data!! + is Resource.Error -> continue + } + } else { + _state.value.book.translateFrom + } + val targetLanguage = _state.value.book.translateTo + + yield() + if ( + line.translatingFrom == sourceLanguage && + line.translatingTo == targetLanguage && + line.translatedLine != null + ) { + val translatedLine = line.copy( + useTranslation = true, + isTranslationFailed = false, + isTranslationLoading = false, + errorMessage = null + ) + + yield() + _state.update { + it.copy( + text = it.text.update( + closestLine to translatedLine + ) + ) + } + linesToTranslate = linesToTranslate.removeKey(closestLine) + continue + } + + var translation: String? = null + var error: UIText? = null + + yield() + val result = translateText.execute( + sourceLanguage = sourceLanguage, + targetLanguage = targetLanguage, + text = line.originalLine + ) when (result) { is Resource.Success -> { - identifiedLanguages.add( - result.data!! to line.key - ) + translation = result.data } is Resource.Error -> { - errors[line.key] = line.value.copy( - isTranslationFailed = true, - isTranslationLoading = false, - errorMessage = result.message - ) + error = result.message } } - } - yield() - if (errors.isNotEmpty()) { - onUpdateTextLines( - errors, - errors.size >= _state.value.text.size - ) - linesToTranslate = linesToTranslate.removeKeys(errors.keys) - } - - yield() - languages.putAll( - identifiedLanguages - .groupBy { - it.first - }.mapValues { (_, value) -> - value.map { it.second } - } - ) - } else { - languages[_state.value.book.translateFrom] = emptyList() - } - - yield() - if (languages.isEmpty()) { - _state.update { - it.copy( - isTranslating = false - ) - } - - yield() - launch(Dispatchers.Main) { - event.error(UIText.StringResource(R.string.error_languages_not_identified)) - } - return@launch - } - - var shouldShowDialog = false - - if (languages.any { !isLanguageModelDownloaded.execute(it.key) }) { - shouldShowDialog = true - } - - if (!isLanguageModelDownloaded.execute(_state.value.book.translateTo)) { - shouldShowDialog = true - } - - yield() - if (shouldShowDialog) { - val languagesToDownload = mutableListOf() - - val translateToLocale = Locale(_state.value.book.translateTo) - val translateToDisplayLanguage = translateToLocale - .getDisplayLanguage(translateToLocale) - .replaceFirstChar { - it.uppercase() - } - - val translateToCanceled = _state.value.languagesToTranslate.find { - it.languageCode == _state.value.book.translateTo - }?.isCanceled ?: false - - languagesToDownload.add( - SelectableLanguage( - languageCode = _state.value.book.translateTo, - displayLanguage = translateToDisplayLanguage, - isDownloading = false, - isCanceled = translateToCanceled, - isError = false, - isDownloaded = isLanguageModelDownloaded.execute(_state.value.book.translateTo), - isSelected = true, - canUnselect = false, - occurrences = languages[_state.value.book.translateTo] - ) - ) - languagesToDownload.addAll( - languages - .filterNot { it.key == _state.value.book.translateTo } - .toList() - .sortedByDescending { it.second?.size } - .mapIndexed { index, language -> - val locale = Locale(language.first) - val displayLanguage = locale - .getDisplayLanguage(locale) - .replaceFirstChar { - it.uppercase() - } - - val isCanceled = _state.value.languagesToTranslate.find { - it.languageCode == language.first - }?.isCanceled ?: false - - SelectableLanguage( - languageCode = language.first, - displayLanguage = displayLanguage, - isDownloading = false, - isCanceled = isCanceled, - isDownloaded = isLanguageModelDownloaded.execute( - language.first - ), - isError = false, - isSelected = index == 0, - canUnselect = index > 0, - occurrences = language.second - ) - } - ) - - yield() - _state.update { - it.copy( - languagesToTranslate = languagesToDownload, - showDownloadLanguageDialog = true - ) - } - return@launch - } - - yield() - for (i in 0.. result.data!! - is Resource.Error -> continue - } - } else { - _state.value.book.translateFrom - } - val targetLanguage = _state.value.book.translateTo - - yield() - if ( - line.translatingFrom == sourceLanguage && - line.translatingTo == targetLanguage && - line.translatedLine != null - ) { val translatedLine = line.copy( - useTranslation = true, - isTranslationFailed = false, + useTranslation = translation != null, + translatedLine = translation, + translatingFrom = sourceLanguage, + translatingTo = targetLanguage, + isTranslationFailed = translation == null, isTranslationLoading = false, - errorMessage = null + errorMessage = error ) yield() @@ -494,209 +546,266 @@ class ReaderViewModel @Inject constructor( ) } linesToTranslate = linesToTranslate.removeKey(closestLine) - continue } - var translation: String? = null - var error: UIText? = null - - yield() - val result = translateText.execute( - sourceLanguage = sourceLanguage, - targetLanguage = targetLanguage, - text = line.originalLine - ) - - when (result) { - is Resource.Success -> { - translation = result.data - } - - is Resource.Error -> { - error = result.message - } - } - - val translatedLine = line.copy( - useTranslation = translation != null, - translatedLine = translation, - translatingFrom = sourceLanguage, - translatingTo = targetLanguage, - isTranslationFailed = translation == null, - isTranslationLoading = false, - errorMessage = error - ) - yield() _state.update { it.copy( - text = it.text.update( - closestLine to translatedLine - ) + isTranslating = false ) } - linesToTranslate = linesToTranslate.removeKey(closestLine) - } - - yield() - _state.update { - it.copy( - isTranslating = false - ) } } } - } - is ReaderEvent.OnDismissDownloadLanguageDialog -> { - _state.update { - it.copy( - showDownloadLanguageDialog = false, - isTranslating = false - ) - } + is ReaderEvent.OnDismissDownloadLanguageDialog -> { + _state.update { + it.copy( + showDownloadLanguageDialog = false, + isTranslating = false + ) + } - viewModelScope.launch { - joinJobs() + launch { + joinJobs() - dismissTranslatorJob = launch(Dispatchers.IO) { - val languagesToDownload = _state.value.languagesToTranslate + dismissTranslatorJob = launch(Dispatchers.IO) { + val languagesToDownload = _state.value.languagesToTranslate - yield() - _state.update { - it.copy( - languagesToTranslate = it.languagesToTranslate.map { lang -> - lang.copy( - isCanceled = true, - isDownloading = false, - isError = false + yield() + _state.update { + it.copy( + languagesToTranslate = it.languagesToTranslate.map { lang -> + lang.copy( + isCanceled = true, + isDownloading = false, + isError = false + ) + }, + showDownloadLanguageDialog = false + ) + } + + yield() + + downloadLanguageJob?.cancel() + val lines = getLinesFromLanguages(languagesToDownload) + onUpdateTextLines( + lines.updateWithCopy { + it.copy( + isTranslationLoading = false, + useTranslation = false ) }, - showDownloadLanguageDialog = false + wholeText = lines.size >= _state.value.text.size ) } - - yield() - - downloadLanguageJob?.cancel() - val lines = getLinesFromLanguages(languagesToDownload) - onUpdateTextLines( - lines.updateWithCopy { - it.copy( - isTranslationLoading = false, - useTranslation = false - ) - }, - wholeText = lines.size >= _state.value.text.size - ) } } - } - is ReaderEvent.OnCancelTranslation -> { - viewModelScope.launch { - cancelTranslationJob?.join() - undoTranslationJob?.join() - dismissTranslatorJob?.cancel() - translatorJob?.cancel() + is ReaderEvent.OnCancelTranslation -> { + launch { + cancelTranslationJob?.join() + undoTranslationJob?.join() + dismissTranslatorJob?.cancel() + translatorJob?.cancel() - cancelTranslationJob = launch(Dispatchers.IO) { - _state.update { - it.copy( - isTranslating = false, - showDownloadLanguageDialog = false - ) - } - - yield() - onUpdateTextLines( - _state.value.text.updateWithCopy { - if (it.useTranslation) { - return@updateWithCopy it - } - - it.copy( - isTranslationLoading = false, - useTranslation = false - ) - }, - wholeText = true - ) - } - } - } - - is ReaderEvent.OnUndoTranslation -> { - viewModelScope.launch { - cancelTranslationJob?.join() - undoTranslationJob?.join() - - cancelTranslationJob = launch(Dispatchers.IO) { - yield() - - if (event.id != null) { + cancelTranslationJob = launch(Dispatchers.IO) { _state.update { - val line = it.text[event.id]?.copy( - useTranslation = false, - isTranslationLoading = false - ) - - if (line == null) { - return@update it - } - it.copy( - text = it.text.update(event.id to line) + isTranslating = false, + showDownloadLanguageDialog = false ) } - } else { + + yield() + onUpdateTextLines( + _state.value.text.updateWithCopy { + if (it.useTranslation) { + return@updateWithCopy it + } + + it.copy( + isTranslationLoading = false, + useTranslation = false + ) + }, + wholeText = true + ) + } + } + } + + is ReaderEvent.OnUndoTranslation -> { + launch { + cancelTranslationJob?.join() + undoTranslationJob?.join() + + cancelTranslationJob = launch(Dispatchers.IO) { + yield() + + if (event.id != null) { + _state.update { + val line = it.text[event.id]?.copy( + useTranslation = false, + isTranslationLoading = false + ) + + if (line == null) { + return@update it + } + + it.copy( + text = it.text.update(event.id to line) + ) + } + } else { + _state.update { + it.copy( + text = it.text.updateWithCopy { line -> + if (!line.useTranslation) { + return@updateWithCopy line + } + + line.copy( + isTranslationLoading = false, + useTranslation = false + ) + } + ) + } + } + } + } + } + + is ReaderEvent.OnDownloadLanguages -> { + launch { + downloadLanguageJob?.cancel() + + downloadLanguageJob = launch(Dispatchers.IO) { _state.update { it.copy( - text = it.text.updateWithCopy { line -> - if (!line.useTranslation) { - return@updateWithCopy line - } + languagesToTranslate = it.languagesToTranslate.map { lang -> + val isDownloaded = isLanguageModelDownloaded.execute( + lang.languageCode + ) - line.copy( - isTranslationLoading = false, - useTranslation = false + lang.copy( + isDownloading = !isDownloaded && lang.isSelected, + isCanceled = false, + isDownloaded = isDownloaded ) } ) } - } - } - } - } - is ReaderEvent.OnDownloadLanguages -> { - viewModelScope.launch { - downloadLanguageJob?.cancel() + val shouldDownload = _state.value.languagesToTranslate.filter { + it.isSelected + }.any { + it.isDownloading + } - downloadLanguageJob = launch(Dispatchers.IO) { - _state.update { - it.copy( - languagesToTranslate = it.languagesToTranslate.map { lang -> - val isDownloaded = isLanguageModelDownloaded.execute( - lang.languageCode - ) - - lang.copy( - isDownloading = !isDownloaded && lang.isSelected, - isCanceled = false, - isDownloaded = isDownloaded + if (!shouldDownload) { + _state.update { + it.copy( + languagesToTranslate = it.languagesToTranslate.filter { lang -> + lang.isDownloaded + }, + showDownloadLanguageDialog = false ) } - ) - } - val shouldDownload = _state.value.languagesToTranslate.filter { - it.isSelected - }.any { - it.isDownloading - } + onUnselectLines() + launch(Dispatchers.Main) { + event.onSuccess(getLinesFromLanguages(_state.value.languagesToTranslate)) + } + return@launch + } + + val languagesToDownload = _state.value.languagesToTranslate.filter { + it.isSelected + } + val jobs = languagesToDownload.map { lang -> + async { + downloadLanguageModel.execute( + languageCode = lang.languageCode, + onCompleted = { + launch(Dispatchers.IO) { + val modelDownloaded = + isLanguageModelDownloaded.execute( + lang.languageCode + ) + + if (!modelDownloaded) { + onEvent( + ReaderEvent.OnUpdateLanguage(lang.languageCode) { + it.copy( + isError = true, + isDownloading = false, + isDownloaded = false, + errorMessage = UIText.StringResource( + R.string.error_could_not_download_language + ) + ) + } + ) + return@launch + } + + onEvent( + ReaderEvent.OnUpdateLanguage(lang.languageCode) { + it.copy( + isDownloading = false, + isDownloaded = true + ) + } + ) + + val isCanceled = + _state.value.languagesToTranslate.find { + it.languageCode == lang.languageCode + }?.isCanceled ?: true + + if (isCanceled) { + deleteLanguageModel.execute( + languageCode = lang.languageCode + ) + onEvent( + ReaderEvent.OnUpdateLanguage(lang.languageCode) { + it.copy( + isDownloading = false, + isDownloaded = false, + isCanceled = false + ) + } + ) + return@launch + } + } + }, + onFailure = { error -> + onEvent( + ReaderEvent.OnUpdateLanguage(lang.languageCode) { + it.copy( + isError = true, + isDownloading = false, + isDownloaded = false, + errorMessage = UIText.StringResource( + R.string.error_query, + error.message ?: "" + ) + ) + } + ) + } + ) + } + } + jobs.awaitAll() + + yield() - if (!shouldDownload) { _state.update { it.copy( languagesToTranslate = it.languagesToTranslate.filter { lang -> @@ -706,375 +815,248 @@ class ReaderViewModel @Inject constructor( ) } + if (languagesToDownload.isEmpty()) { + yield() + launch(Dispatchers.Main) { + event.error( + UIText.StringResource( + R.string.error_could_not_download_languages + ) + ) + } + onEvent(ReaderEvent.OnDismissDownloadLanguageDialog) + return@launch + } + + yield() + onUnselectLines() launch(Dispatchers.Main) { event.onSuccess(getLinesFromLanguages(_state.value.languagesToTranslate)) } - return@launch } + } + } - val languagesToDownload = _state.value.languagesToTranslate.filter { - it.isSelected - } - val jobs = languagesToDownload.map { lang -> - async { - downloadLanguageModel.execute( - languageCode = lang.languageCode, - onCompleted = { - launch(Dispatchers.IO) { - val modelDownloaded = isLanguageModelDownloaded.execute( - lang.languageCode - ) - - if (!modelDownloaded) { - onEvent( - ReaderEvent.OnUpdateLanguage(lang.languageCode) { - it.copy( - isError = true, - isDownloading = false, - isDownloaded = false, - errorMessage = UIText.StringResource( - R.string.error_could_not_download_language - ) - ) - } - ) - return@launch - } - - onEvent( - ReaderEvent.OnUpdateLanguage(lang.languageCode) { - it.copy( - isDownloading = false, - isDownloaded = true - ) - } - ) - - val isCanceled = - _state.value.languagesToTranslate.find { - it.languageCode == lang.languageCode - }?.isCanceled ?: true - - if (isCanceled) { - deleteLanguageModel.execute( - languageCode = lang.languageCode - ) - onEvent( - ReaderEvent.OnUpdateLanguage(lang.languageCode) { - it.copy( - isDownloading = false, - isDownloaded = false, - isCanceled = false - ) - } - ) - return@launch - } - } - }, - onFailure = { error -> - onEvent( - ReaderEvent.OnUpdateLanguage(lang.languageCode) { - it.copy( - isError = true, - isDownloading = false, - isDownloaded = false, - errorMessage = UIText.StringResource( - R.string.error_query, - error.message ?: "" - ) - ) - } - ) - } + is ReaderEvent.OnSelectLanguage -> { + launch(Dispatchers.IO) { + val editedList = _state.value.languagesToTranslate.map { + if (it.languageCode == event.language.languageCode) { + it.copy( + isSelected = !it.isSelected ) + } else { + it } } - jobs.awaitAll() - - yield() _state.update { it.copy( - languagesToTranslate = it.languagesToTranslate.filter { lang -> - lang.isDownloaded - }, - showDownloadLanguageDialog = false + languagesToTranslate = editedList + ) + } + } + } + + is ReaderEvent.OnUpdateLanguage -> { + launch(Dispatchers.IO) { + val editedList = _state.value.languagesToTranslate.map { + if (it.languageCode == event.languageCode) { + event.calculation(it) + } else { + it + } + } + + _state.update { + it.copy( + languagesToTranslate = editedList + ) + } + } + } + + is ReaderEvent.OnGoBack -> { + launch { + _state.update { + it.copy( + lockMenu = true ) } - if (languagesToDownload.isEmpty()) { - yield() - launch(Dispatchers.Main) { - event.error( - UIText.StringResource( - R.string.error_could_not_download_languages + val insetsController = WindowCompat.getInsetsController( + event.context.window, + event.context.window.decorView + ) + + 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 = calculateProgress(), + scrollIndex = listState.firstVisibleItemIndex, + scrollOffset = listState.firstVisibleItemScrollOffset ) ) } - onEvent(ReaderEvent.OnDismissDownloadLanguageDialog) - return@launch - } - yield() - - onUnselectLines() - launch(Dispatchers.Main) { - event.onSuccess(getLinesFromLanguages(_state.value.languagesToTranslate)) - } - } - } - } - - is ReaderEvent.OnSelectLanguage -> { - viewModelScope.launch(Dispatchers.IO) { - val editedList = _state.value.languagesToTranslate.map { - if (it.languageCode == event.language.languageCode) { - it.copy( - isSelected = !it.isSelected + updateBooks.execute( + listOf(_state.value.book) ) - } else { - it + event.refreshList(_state.value.book) } - } - _state.update { - it.copy( - languagesToTranslate = editedList + insetsController.show(WindowInsetsCompat.Type.systemBars()) + event.navigate(event.navigator) + } + } + + is ReaderEvent.OnScroll -> { + launch { + val scrollTo = (_state.value.text.size * event.progress).roundToInt() + + _state.value.listState.scrollToItem( + scrollTo ) } } - } - - is ReaderEvent.OnUpdateLanguage -> { - viewModelScope.launch(Dispatchers.IO) { - val editedList = _state.value.languagesToTranslate.map { - if (it.languageCode == event.languageCode) { - event.calculation(it) - } else { - it - } - } - - _state.update { - it.copy( - languagesToTranslate = editedList - ) - } - } - } - - is ReaderEvent.OnGoBack -> { - viewModelScope.launch { - cancelJobs() - _state.update { - it.copy( - lockMenu = true - ) - } - - val insetsController = WindowCompat.getInsetsController( - event.context.window, - 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 - } + is ReaderEvent.OnChangeProgress -> { + launch(Dispatchers.IO) { _state.update { it.copy( book = it.book.copy( - progress = progress, - scrollIndex = firstVisibleItemIndex, - scrollOffset = firstVisibleItemOffset + progress = event.progress, + scrollIndex = event.firstVisibleItemIndex, + scrollOffset = event.firstVisibleItemOffset ) ) } + + updateBooks.execute( + listOf(_state.value.book) + ) + event.refreshList(_state.value.book) } - - 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 { - val scrollTo = (_state.value.text.size * event.progress).roundToInt() - - _state.value.listState.scrollToItem( - scrollTo - ) + is ReaderEvent.OnShowHideSettingsBottomSheet -> { + launch(Dispatchers.IO) { + _state.update { + it.copy( + currentPage = if (it.showSettingsBottomSheet) it.currentPage else 0, + showSettingsBottomSheet = !it.showSettingsBottomSheet + ) + } + } } - } - is ReaderEvent.OnChangeProgress -> { - viewModelScope.launch(Dispatchers.IO) { - _state.update { - it.copy( - book = it.book.copy( - progress = event.progress, - scrollIndex = event.firstVisibleItemIndex, - scrollOffset = event.firstVisibleItemOffset + is ReaderEvent.OnScrollToSettingsPage -> { + launch { + event.pagerState.scrollToPage(event.page) + } + } + + is ReaderEvent.OnMoveBookToAlreadyRead -> { + launch { + onEvent( + ReaderEvent.OnGoBack( + event.context, + event.navigator, + refreshList = {}, + navigate = {} ) ) - } - 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) { - _state.update { - it.copy( - currentPage = if (it.showSettingsBottomSheet) it.currentPage else 0, - showSettingsBottomSheet = !it.showSettingsBottomSheet - ) - } - } - } - - is ReaderEvent.OnScrollToSettingsPage -> { - viewModelScope.launch { - event.pagerState.scrollToPage(event.page) - } - } - - is ReaderEvent.OnMoveBookToAlreadyRead -> { - viewModelScope.launch { - onEvent( - ReaderEvent.OnGoBack( - event.context, - event.navigator, - refreshList = {}, - navigate = {} - ) - ) - - val firstVisibleItemIndex = _state.value.listState.firstVisibleItemIndex - val firstVisibleItemOffset = _state.value.listState.firstVisibleItemScrollOffset - - _state.update { - it.copy( - book = it.book.copy( - category = Category.ALREADY_READ, - progress = 1f, - scrollIndex = firstVisibleItemIndex, - scrollOffset = firstVisibleItemOffset + _state.update { + val listState = it.listState + it.copy( + book = it.book.copy( + category = Category.ALREADY_READ, + progress = 1f, + scrollIndex = listState.firstVisibleItemIndex, + scrollOffset = listState.firstVisibleItemScrollOffset + ) ) + } + updateBooks.execute(listOf(_state.value.book)) + + event.onUpdateCategories( + _state.value.book ) + event.updatePage( + Category.entries.dropLastWhile { + it != Category.ALREADY_READ + }.size - 1 + ) + + event.navigator.navigate(Screen.LIBRARY, true) } - updateBooks.execute(listOf(_state.value.book)) - - event.onUpdateCategories( - _state.value.book - ) - event.updatePage( - Category.entries.dropLastWhile { - it != Category.ALREADY_READ - }.size - 1 - ) - - cancelJobs() - - event.navigator.navigate(Screen.LIBRARY, true) } - } - is ReaderEvent.OnOpenTranslator -> { - viewModelScope.launch { - val translatorIntent = Intent() - val browserIntent = Intent() + is ReaderEvent.OnOpenTranslator -> { + launch { + val translatorIntent = Intent() + val browserIntent = Intent() - translatorIntent.type = "text/plain" - translatorIntent.action = Intent.ACTION_PROCESS_TEXT + translatorIntent.type = "text/plain" + translatorIntent.action = Intent.ACTION_PROCESS_TEXT - 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_READONLY, true) + translatorIntent.putExtra( + Intent.EXTRA_PROCESS_TEXT, + event.textToTranslate + ) + translatorIntent.putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, true) - browserIntent.putExtra( - SearchManager.QUERY, - "translate: ${event.textToTranslate.trim()}" - ) + browserIntent.putExtra( + SearchManager.QUERY, + "translate: ${event.textToTranslate.trim()}" + ) - if (translatorIntent.resolveActivity(event.context.packageManager) != null) { - event.context.startActivity(translatorIntent) - return@launch + if (translatorIntent.resolveActivity(event.context.packageManager) != null) { + event.context.startActivity(translatorIntent) + return@launch + } + + if (browserIntent.resolveActivity(event.context.packageManager) != null) { + event.context.startActivity(browserIntent) + return@launch + } + + event.noAppsFound() } - - if (browserIntent.resolveActivity(event.context.packageManager) != null) { - event.context.startActivity(browserIntent) - return@launch - } - - event.noAppsFound() } - } - is ReaderEvent.OnOpenDictionary -> { - viewModelScope.launch { - val browserIntent = Intent() + is ReaderEvent.OnOpenDictionary -> { + launch { + val browserIntent = Intent() - browserIntent.action = Intent.ACTION_WEB_SEARCH - browserIntent.putExtra( - SearchManager.QUERY, - "dictionary" + - ": ${event.textToDefine.trim()}" - ) + browserIntent.action = Intent.ACTION_WEB_SEARCH + browserIntent.putExtra( + SearchManager.QUERY, + "dictionary" + + ": ${event.textToDefine.trim()}" + ) - if (browserIntent.resolveActivity(event.context.packageManager) != null) { - event.context.startActivity(browserIntent) - return@launch + if (browserIntent.resolveActivity(event.context.packageManager) != null) { + event.context.startActivity(browserIntent) + return@launch + } + + event.noAppsFound() } - - event.noAppsFound() } } } } - private fun cancelJobs() { - cancelTranslationJob?.cancel() - translatorJob?.cancel() - dismissTranslatorJob?.cancel() - undoTranslationJob?.cancel() - downloadLanguageJob?.cancel() - } - private suspend fun joinJobs() { cancelTranslationJob?.join() translatorJob?.join() @@ -1082,7 +1064,6 @@ class ReaderViewModel @Inject constructor( undoTranslationJob?.join() } - private suspend fun findClosestLineToTranslate( translatingLines: Map ): 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,35 +1239,32 @@ 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.OnLoadText( + refreshList = { refreshList(it) }, + onError = { + onError(it) + }, + onTextIsEmpty = { + onEvent(ReaderEvent.OnTextIsEmpty) + } + ) ) - ) + } } } @@ -1296,46 +1273,94 @@ class ReaderViewModel @Inject constructor( firstVisibleItemIndex: Int, firstVisibleItemOffset: Int, navigator: Navigator, - isReaderLoading: Boolean, onLibraryEvent: (LibraryEvent) -> Unit, onHistoryEvent: (HistoryEvent) -> Unit ) { snapshotFlow { firstVisibleItemIndex to firstVisibleItemOffset - }.debounce(500).collectLatest { items -> - if ( - !isReaderLoading && - _state.value.text.isNotEmpty() && - _state.value.listState.layoutInfo.totalItemsCount > 0 - ) { - val lastVisibleItemIndex = _state.value.listState.layoutInfo.visibleItemsInfo - .last().index + } + .debounce(200) + .collectLatest { items -> + val listState = _state.value.listState + if ( + !_state.value.loading && + _state.value.text.isNotEmpty() && + listState.layoutInfo.totalItemsCount > 0 + ) { + 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) - ) { - 1f - } else { - (items.first.toFloat() / (_state.value.text.size - 1).toFloat()) - } - } else { - 0f - } - - onEvent( - ReaderEvent.OnChangeProgress( - progress = progress, - navigator = navigator, - firstVisibleItemIndex = items.first, - firstVisibleItemOffset = items.second, - refreshList = { book -> - onLibraryEvent(LibraryEvent.OnUpdateBook(book)) - onHistoryEvent(HistoryEvent.OnUpdateBook(book)) + val progress = if (items.first > 0) { + if (lastVisibleItemIndex >= totalItemsCount) { + 1f + } else { + items.first / (_state.value.text.size - 1).toFloat() } + } else { + 0f + } + + onEvent( + ReaderEvent.OnChangeProgress( + progress = progress, + navigator = navigator, + firstVisibleItemIndex = items.first, + firstVisibleItemOffset = items.second, + refreshList = { book -> + onLibraryEvent(LibraryEvent.OnUpdateBook(book)) + onHistoryEvent(HistoryEvent.OnUpdateBook(book)) + } + ) ) - ) + } } + } + + 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() } } } \ No newline at end of file