🚀 Automatic check for text update

* Option to enable automatic check for text update when you open book in Reader
* Navigates user to BookInfo screen if update is found
* Shows toast(up-to-date) if corresponding setting is enabled and there is not update found
This commit is contained in:
Acclorite 2024-09-22 16:33:10 +03:00
parent d2cfcd71a1
commit 8145cafc5a
25 changed files with 463 additions and 47 deletions

View file

@ -37,6 +37,8 @@ object DataStoreConstants {
val PERCEPTION_EXPANDER = booleanPreferencesKey("perception_expander")
val PERCEPTION_EXPANDER_PADDING = intPreferencesKey("perception_expander_padding")
val PERCEPTION_EXPANDER_THICKNESS = intPreferencesKey("perception_expander_thickness")
val CHECK_FOR_TEXT_UPDATE = booleanPreferencesKey("check_for_text_update")
val CHECK_FOR_TEXT_UPDATE_TOAST = booleanPreferencesKey("check_for_text_update_toast")
// Browse settings
val BROWSE_FILES_STRUCTURE = stringPreferencesKey("browse_files_structure")

View file

@ -88,7 +88,8 @@ sealed class Screen : Parcelable {
@Parcelize
@Keep
data class BookInfo(
val bookId: Int
val bookId: Int,
val startUpdate: Boolean
) : Screen()
@Parcelize

View file

@ -40,4 +40,6 @@ sealed class MainEvent {
data class OnChangePerceptionExpander(val bool: Boolean) : MainEvent()
data class OnChangePerceptionExpanderPadding(val padding: Int) : MainEvent()
data class OnChangePerceptionExpanderThickness(val thickness: Int) : MainEvent()
data class OnChangeCheckForTextUpdate(val bool: Boolean) : MainEvent()
data class OnChangeCheckForTextUpdateToast(val bool: Boolean) : MainEvent()
}

View file

@ -77,6 +77,8 @@ data class MainState(
val perceptionExpander: Boolean = provideDefaultValue { false },
val perceptionExpanderPadding: Int = provideDefaultValue { 5 },
val perceptionExpanderThickness: Int = provideDefaultValue { 4 },
val checkForTextUpdate: Boolean = provideDefaultValue { true },
val checkForTextUpdateToast: Boolean = provideDefaultValue { true },
// Browse Settings
val browseFilesStructure: BrowseFilesStructure = provideDefaultValue {
@ -254,6 +256,14 @@ data class MainState(
perceptionExpanderThickness = provideValue(
PERCEPTION_EXPANDER_THICKNESS
) { perceptionExpanderThickness },
checkForTextUpdate = provideValue(
CHECK_FOR_TEXT_UPDATE
) { checkForTextUpdate },
checkForTextUpdateToast = provideValue(
CHECK_FOR_TEXT_UPDATE_TOAST
) { checkForTextUpdateToast },
)
}
}

View file

@ -502,6 +502,34 @@ class MainViewModel @Inject constructor(
}
}
}
is MainEvent.OnChangeCheckForTextUpdate -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(
DataStoreConstants.CHECK_FOR_TEXT_UPDATE,
event.bool
)
updateStateWithSavedHandle {
it.copy(
checkForTextUpdate = event.bool
)
}
}
}
is MainEvent.OnChangeCheckForTextUpdateToast -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(
DataStoreConstants.CHECK_FOR_TEXT_UPDATE_TOAST,
event.bool
)
updateStateWithSavedHandle {
it.copy(
checkForTextUpdateToast = event.bool
)
}
}
}
}
}

View file

@ -55,8 +55,8 @@ import ua.acclorite.book_story.presentation.screens.book_info.components.BookInf
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoStatisticSection
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoTopBar
import ua.acclorite.book_story.presentation.screens.book_info.components.change_cover_bottom_sheet.BookInfoChangeCoverBottomSheet
import ua.acclorite.book_story.presentation.screens.book_info.components.confirm_update_dialog.BookInfoConfirmUpdateDialog
import ua.acclorite.book_story.presentation.screens.book_info.components.details_bottom_sheet.BookInfoDetailsBottomSheet
import ua.acclorite.book_story.presentation.screens.book_info.components.dialog.BookInfoConfirmUpdateDialog
import ua.acclorite.book_story.presentation.screens.book_info.components.dialog.BookInfoDeleteDialog
import ua.acclorite.book_story.presentation.screens.book_info.components.dialog.BookInfoMoveDialog
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
@ -65,15 +65,22 @@ import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
fun BookInfoScreenRoot(screen: Screen.BookInfo) {
val viewModel = LocalBookInfoViewModel.current.viewModel
val onNavigate = LocalOnNavigate.current
val context = LocalContext.current
val snackbarState = remember { SnackbarHostState() }
LaunchedEffect(Unit) {
viewModel.init(
screen = screen,
onNavigate = onNavigate
snackbarState = snackbarState,
onNavigate = onNavigate,
context = context
)
}
BookInfoScreen()
BookInfoScreen(
snackbarState = snackbarState
)
DisposableEffect(Unit) {
onDispose {
@ -84,19 +91,18 @@ fun BookInfoScreenRoot(screen: Screen.BookInfo) {
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun BookInfoScreen() {
private fun BookInfoScreen(snackbarState: SnackbarHostState) {
val context = LocalContext.current
val state = LocalBookInfoViewModel.current.state
val onEvent = LocalBookInfoViewModel.current.onEvent
val onNavigate = LocalOnNavigate.current
val listState = rememberLazyListState()
val snackbarState = remember { SnackbarHostState() }
val refreshState = rememberPullRefreshState(
refreshing = state.value.isRefreshing || state.value.isLoadingUpdate,
refreshing = state.value.updating || state.value.checkingForUpdate,
onRefresh = {
onEvent(
BookInfoEvent.OnLoadUpdate(
BookInfoEvent.OnCheckForUpdate(
snackbarState,
context
)
@ -181,7 +187,7 @@ private fun BookInfoScreen() {
FloatingActionButton(
onClick = {
if (!state.value.isRefreshing) {
if (!state.value.updating) {
onEvent(BookInfoEvent.OnNavigateToReaderScreen(onNavigate = onNavigate))
}
},
@ -221,7 +227,7 @@ private fun BookInfoScreen() {
)
PullRefreshIndicator(
state.value.isRefreshing || state.value.isLoadingUpdate,
state.value.updating || state.value.checkingForUpdate,
refreshState,
Modifier
.align(Alignment.TopCenter)
@ -254,7 +260,7 @@ private fun BookInfoScreen() {
return@BackHandler
}
if (!state.value.isRefreshing) {
if (!state.value.updating) {
onEvent(BookInfoEvent.OnCancelUpdate)
onNavigate {
navigateBack()

View file

@ -38,7 +38,7 @@ fun BookInfoMoreDropDown(snackbarState: SnackbarHostState) {
contentDescription = R.string.show_dropdown_content_desc,
disableOnClick = false,
enabled = !showDropDown &&
!state.value.isRefreshing
!state.value.updating
) {
showDropDown = true
}

View file

@ -83,7 +83,7 @@ fun BookInfoTopBar(
}
}
} else {
GoBackButton(onNavigate = onNavigate, enabled = !state.value.isRefreshing) {
GoBackButton(onNavigate = onNavigate, enabled = !state.value.updating) {
onEvent(BookInfoEvent.OnCancelUpdate)
}
}
@ -106,10 +106,10 @@ fun BookInfoTopBar(
icon = Icons.Outlined.Refresh,
contentDescription = R.string.refresh_book_content_desc,
disableOnClick = false,
enabled = !state.value.isRefreshing && !state.value.isLoadingUpdate
enabled = !state.value.updating && !state.value.checkingForUpdate
) {
onEvent(
BookInfoEvent.OnLoadUpdate(
BookInfoEvent.OnCheckForUpdate(
snackbarState,
context
)
@ -135,7 +135,7 @@ fun BookInfoTopBar(
icon = Icons.Outlined.Done,
contentDescription = R.string.apply_changes_content_desc,
disableOnClick = true,
enabled = !state.value.isRefreshing &&
enabled = !state.value.updating &&
(state.value.titleValue.isNotBlank() &&
state.value.titleValue.trim() !=
state.value.book.title.trim()) ||

View file

@ -1,4 +1,4 @@
package ua.acclorite.book_story.presentation.screens.book_info.components.confirm_update_dialog
package ua.acclorite.book_story.presentation.screens.book_info.components.dialog
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Update

View file

@ -101,7 +101,7 @@ sealed class BookInfoEvent {
val snackbarState: SnackbarHostState
) : BookInfoEvent()
data class OnLoadUpdate(
data class OnCheckForUpdate(
val snackbarState: SnackbarHostState,
val context: Context
) : BookInfoEvent()

View file

@ -10,8 +10,8 @@ import ua.acclorite.book_story.presentation.core.constants.Constants
data class BookInfoState(
val book: Book = Constants.EMPTY_BOOK,
val isLoadingUpdate: Boolean = false,
val isRefreshing: Boolean = false,
val checkingForUpdate: Boolean = false,
val updating: Boolean = false,
val showConfirmUpdateDialog: Boolean = false,
val updatedText: List<String>? = null,

View file

@ -4,9 +4,11 @@ package ua.acclorite.book_story.presentation.screens.book_info.data
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Context.CLIPBOARD_SERVICE
import android.graphics.BitmapFactory
import android.os.Build
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
@ -451,13 +453,13 @@ class BookInfoViewModel @Inject constructor(
}
}
is BookInfoEvent.OnLoadUpdate -> {
is BookInfoEvent.OnCheckForUpdate -> {
updateJob?.cancel()
updateJob = launch(Dispatchers.IO) {
_state.update {
it.copy(
showConfirmUpdateDialog = false,
isLoadingUpdate = true,
checkingForUpdate = true,
editTitle = false,
editAuthor = false,
editDescription = false
@ -477,7 +479,7 @@ class BookInfoViewModel @Inject constructor(
action = event.context.getString(R.string.retry),
onAction = {
onEvent(
BookInfoEvent.OnLoadUpdate(
BookInfoEvent.OnCheckForUpdate(
snackbarState = event.snackbarState,
context = event.context
)
@ -491,7 +493,7 @@ class BookInfoViewModel @Inject constructor(
delay(500)
_state.update {
it.copy(
isLoadingUpdate = false
checkingForUpdate = false
)
}
return@launch
@ -508,7 +510,7 @@ class BookInfoViewModel @Inject constructor(
action = event.context.getString(R.string.retry),
onAction = {
onEvent(
BookInfoEvent.OnLoadUpdate(
BookInfoEvent.OnCheckForUpdate(
snackbarState = event.snackbarState,
context = event.context
)
@ -522,7 +524,7 @@ class BookInfoViewModel @Inject constructor(
delay(500)
_state.update {
it.copy(
isLoadingUpdate = false
checkingForUpdate = false
)
}
return@launch
@ -554,7 +556,7 @@ class BookInfoViewModel @Inject constructor(
delay(500)
_state.update {
it.copy(
isLoadingUpdate = false
checkingForUpdate = false
)
}
return@launch
@ -571,7 +573,7 @@ class BookInfoViewModel @Inject constructor(
_state.update {
it.copy(
isLoadingUpdate = false
checkingForUpdate = false
)
}
}
@ -601,7 +603,7 @@ class BookInfoViewModel @Inject constructor(
launch(Dispatchers.IO) {
_state.update {
it.copy(
isRefreshing = true,
updating = true,
showConfirmUpdateDialog = false
)
}
@ -615,7 +617,7 @@ class BookInfoViewModel @Inject constructor(
action = event.context.getString(R.string.retry),
onAction = {
onEvent(
BookInfoEvent.OnLoadUpdate(
BookInfoEvent.OnCheckForUpdate(
snackbarState = event.snackbarState,
context = event.context
)
@ -629,7 +631,7 @@ class BookInfoViewModel @Inject constructor(
delay(500)
_state.update {
it.copy(
isRefreshing = false,
updating = false,
updatedText = null,
updatedChapters = null
)
@ -660,7 +662,7 @@ class BookInfoViewModel @Inject constructor(
action = event.context.getString(R.string.retry),
onAction = {
onEvent(
BookInfoEvent.OnLoadUpdate(
BookInfoEvent.OnCheckForUpdate(
snackbarState = event.snackbarState,
context = event.context
)
@ -674,7 +676,7 @@ class BookInfoViewModel @Inject constructor(
delay(500)
_state.update {
it.copy(
isRefreshing = false,
updating = false,
updatedText = null,
updatedChapters = null
)
@ -692,7 +694,7 @@ class BookInfoViewModel @Inject constructor(
action = event.context.getString(R.string.retry),
onAction = {
onEvent(
BookInfoEvent.OnLoadUpdate(
BookInfoEvent.OnCheckForUpdate(
snackbarState = event.snackbarState,
context = event.context
)
@ -706,7 +708,7 @@ class BookInfoViewModel @Inject constructor(
delay(500)
_state.update {
it.copy(
isRefreshing = false,
updating = false,
updatedText = null,
updatedChapters = null
)
@ -735,7 +737,7 @@ class BookInfoViewModel @Inject constructor(
delay(500)
_state.update {
it.copy(
isRefreshing = false
updating = false
)
}
}
@ -748,7 +750,7 @@ class BookInfoViewModel @Inject constructor(
it.copy(
showConfirmUpdateDialog = false,
isLoadingUpdate = false,
checkingForUpdate = false,
updatedText = null,
updatedChapters = null
)
@ -778,7 +780,12 @@ class BookInfoViewModel @Inject constructor(
}
}
fun init(screen: Screen.BookInfo, onNavigate: OnNavigate) {
fun init(
screen: Screen.BookInfo,
snackbarState: SnackbarHostState,
context: Context,
onNavigate: OnNavigate
) {
viewModelScope.launch(Dispatchers.IO) {
val book = getBookById.execute(screen.bookId)
@ -796,6 +803,22 @@ class BookInfoViewModel @Inject constructor(
}
clear()
if (screen.startUpdate) {
onEvent(
BookInfoEvent.OnCheckForUpdate(
snackbarState = snackbarState,
context = context
)
)
onNavigate {
putScreen(
Screen.BookInfo(
screen.bookId,
false
)
)
}
}
onEvent(BookInfoEvent.OnCheckCoverReset)
}
}

View file

@ -214,7 +214,10 @@ private fun HistoryScreen() {
onBodyClick = {
onNavigate {
navigate(
Screen.BookInfo(it.bookId)
Screen.BookInfo(
bookId = it.bookId,
startUpdate = false
)
)
}
},

View file

@ -174,7 +174,12 @@ private fun LibraryScreen(
onEvent(LibraryEvent.OnSelectBook(it))
} else {
onNavigate {
navigate(Screen.BookInfo(it.first.id))
navigate(
Screen.BookInfo(
bookId = it.first.id,
startUpdate = false
)
)
}
}
},

View file

@ -81,6 +81,7 @@ import ua.acclorite.book_story.presentation.screens.reader.components.ReaderChap
import ua.acclorite.book_story.presentation.screens.reader.components.ReaderChaptersDrawer
import ua.acclorite.book_story.presentation.screens.reader.components.ReaderPerceptionExpander
import ua.acclorite.book_story.presentation.screens.reader.components.ReaderTextParagraph
import ua.acclorite.book_story.presentation.screens.reader.components.ReaderUpdateDialog
import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.ReaderBottomBar
import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.ReaderTopBar
import ua.acclorite.book_story.presentation.screens.reader.components.readerFastColorPresetChange
@ -117,6 +118,15 @@ fun ReaderScreenRoot(screen: Screen.Reader) {
screen = screen,
onNavigate = onNavigate,
fullscreenMode = mainState.value.fullscreen,
checkForTextUpdate = mainState.value.checkForTextUpdate,
checkForTextUpdateToast = {
if (mainState.value.checkForTextUpdateToast) {
context.getString(R.string.nothing_changed).showToast(
context = context,
longToast = false
)
}
},
activity = context,
refreshList = {
onLibraryEvent(LibraryEvent.OnUpdateBook(it))
@ -350,6 +360,9 @@ private fun ReaderScreen(lazyListState: LazyListState) {
if (state.value.showSettingsBottomSheet) {
ReaderSettingsBottomSheet()
}
if (state.value.showUpdateDialog) {
ReaderUpdateDialog()
}
Scaffold(
Modifier

View file

@ -0,0 +1,47 @@
package ua.acclorite.book_story.presentation.screens.reader.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Upgrade
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.core.components.LocalReaderViewModel
import ua.acclorite.book_story.presentation.core.components.custom_dialog.CustomDialogWithContent
import ua.acclorite.book_story.presentation.core.navigation.LocalOnNavigate
import ua.acclorite.book_story.presentation.core.navigation.Screen
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
/**
* Reader Update Dialog.
* Navigates user to BookInfo and starts book update.
*/
@Composable
fun ReaderUpdateDialog() {
val state = LocalReaderViewModel.current.state
val onEvent = LocalReaderViewModel.current.onEvent
val onNavigate = LocalOnNavigate.current
CustomDialogWithContent(
title = stringResource(id = R.string.update_book),
imageVectorIcon = Icons.Rounded.Upgrade,
description = stringResource(
id = R.string.update_book_description
),
actionText = stringResource(id = R.string.proceed),
isActionEnabled = true,
onDismiss = { onEvent(ReaderEvent.OnShowHideUpdateDialog(false)) },
onAction = {
onNavigate {
navigate(
Screen.BookInfo(
bookId = state.value.book.id,
startUpdate = true
),
useBackAnimation = true,
saveInBackStack = false
)
}
},
withDivider = false
)
}

View file

@ -2,15 +2,23 @@ package ua.acclorite.book_story.presentation.screens.reader.components.app_bar
import androidx.activity.ComponentActivity
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.rounded.Menu
import androidx.compose.material.icons.rounded.Upgrade
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
@ -18,14 +26,17 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.core.components.CustomAnimatedVisibility
import ua.acclorite.book_story.presentation.core.components.CustomIconButton
import ua.acclorite.book_story.presentation.core.components.LocalHistoryViewModel
import ua.acclorite.book_story.presentation.core.components.LocalLibraryViewModel
@ -92,8 +103,10 @@ fun ReaderTopBar() {
fontSize = 20.sp,
lineHeight = 20.sp,
color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier = Modifier
.padding(end = 8.dp)
.noRippleClickable(
enabled = !state.value.lockMenu,
onClick = {
@ -107,7 +120,10 @@ fun ReaderTopBar() {
navigate = {
onNavigate {
navigate(
Screen.BookInfo(state.value.book.id),
Screen.BookInfo(
bookId = state.value.book.id,
startUpdate = false
),
useBackAnimation = true,
saveInBackStack = false
)
@ -117,8 +133,9 @@ fun ReaderTopBar() {
)
}
)
.basicMarquee(
iterations = Int.MAX_VALUE
.then(
if (state.value.checkingForUpdate || state.value.updateFound) Modifier
else Modifier.basicMarquee(iterations = Int.MAX_VALUE)
)
)
Text(
@ -132,6 +149,40 @@ fun ReaderTopBar() {
}
},
actions = {
CustomAnimatedVisibility(
visible = state.value.checkingForUpdate || state.value.updateFound,
enter = fadeIn(),
exit = shrinkHorizontally() + fadeOut()
) {
Box(
modifier = Modifier.size(40.dp),
contentAlignment = Alignment.Center
) {
if (state.value.updateFound) {
CustomIconButton(
icon = Icons.Rounded.Upgrade,
contentDescription = R.string.update_text_content_desc,
color = MaterialTheme.colorScheme.primary,
disableOnClick = false,
enabled = !state.value.lockMenu &&
state.value.updateFound &&
!state.value.checkingForUpdate
) {
onEvent(ReaderEvent.OnShowHideUpdateDialog(true))
}
} else {
CircularProgressIndicator(
modifier = Modifier
.size(19.dp)
.noRippleClickable {
onEvent(ReaderEvent.OnCancelCheckTextForUpdate)
},
strokeWidth = 2.4.dp
)
}
}
}
if (state.value.currentChapter != null) {
CustomIconButton(
icon = Icons.Rounded.Menu,

View file

@ -11,6 +11,7 @@ sealed class ReaderEvent {
data object OnTextIsEmpty : ReaderEvent()
data class OnLoadText(
val checkForUpdate: () -> Unit,
val refreshList: (Book) -> Unit,
val onError: (UIText) -> Unit,
val onTextIsEmpty: () -> Unit
@ -86,4 +87,14 @@ sealed class ReaderEvent {
val context: ComponentActivity,
val noAppsFound: () -> Unit
) : ReaderEvent()
data class OnCheckTextForUpdate(
val noUpdateFound: () -> Unit
) : ReaderEvent()
data class OnShowHideUpdateDialog(
val show: Boolean
) : ReaderEvent()
data object OnCancelCheckTextForUpdate : ReaderEvent()
}

View file

@ -24,6 +24,10 @@ data class ReaderState(
val checkpoint: Pair<Int, Int> = 0 to 0,
val lockMenu: Boolean = false,
val checkingForUpdate: Boolean = false,
val updateFound: Boolean = false,
val showUpdateDialog: Boolean = false,
val showSettingsBottomSheet: Boolean = false,
val currentPage: Int = 0,
)

View file

@ -22,18 +22,22 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book
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.ParseText
import ua.acclorite.book_story.domain.use_case.UpdateBook
import ua.acclorite.book_story.domain.util.OnNavigate
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.domain.util.UIText
import ua.acclorite.book_story.presentation.core.navigation.Screen
import ua.acclorite.book_story.presentation.core.util.BaseViewModel
import ua.acclorite.book_story.presentation.core.util.launchActivity
import java.io.File
import javax.inject.Inject
import kotlin.math.roundToInt
@ -43,7 +47,8 @@ class ReaderViewModel @Inject constructor(
private val updateBook: UpdateBook,
private val getText: GetText,
private val getLatestHistory: GetLatestHistory,
private val getBookById: GetBookById
private val getBookById: GetBookById,
private val parseText: ParseText
) : BaseViewModel<ReaderState, ReaderEvent>() {
private val _state = MutableStateFlow(ReaderState())
@ -51,6 +56,7 @@ class ReaderViewModel @Inject constructor(
private var eventJob = SupervisorJob()
private var scrollJob: Job? = null
private var updateJob: Job? = null
override fun onEvent(event: ReaderEvent) {
viewModelScope.launch(eventJob + Dispatchers.Main) {
@ -95,6 +101,8 @@ class ReaderViewModel @Inject constructor(
_state.value.book.scrollOffset
)
event.checkForUpdate()
delay(100)
_state.update {
it.copy(
@ -399,6 +407,97 @@ class ReaderViewModel @Inject constructor(
event.noAppsFound()
}
}
is ReaderEvent.OnCheckTextForUpdate -> {
updateJob?.cancel()
updateJob = launch(Dispatchers.IO) {
_state.update {
it.copy(
checkingForUpdate = true,
updateFound = false,
showUpdateDialog = false
)
}
yield()
val currentBook = _state.value.book.apply {
if (!File(filePath).exists()) {
_state.update {
it.copy(
checkingForUpdate = false,
updateFound = false
)
}
return@launch
}
}
val updatedBook = parseText.execute(File(currentBook.filePath)).apply {
if (this is Resource.Error || data == null) {
_state.update {
it.copy(
checkingForUpdate = false,
updateFound = false
)
}
return@launch
}
}.data!!
yield()
val updatedText = updatedBook.map { it.text }.flatten()
val text = getText.execute(currentBook.textPath)
val updatedChapters = updatedBook.map { it.chapter }.run {
if (size < 2) return@run emptyList()
return@run this
}
val chapters = currentBook.chapters
yield()
if (updatedText == text && updatedChapters == chapters) {
withContext(Dispatchers.Main) {
event.noUpdateFound()
}
_state.update {
it.copy(
checkingForUpdate = false,
updateFound = false
)
}
return@launch
}
_state.update {
it.copy(
updateFound = true,
checkingForUpdate = false
)
}
onEvent(ReaderEvent.OnShowHideUpdateDialog(true))
}
}
is ReaderEvent.OnShowHideUpdateDialog -> {
_state.update {
it.copy(
showUpdateDialog = event.show
)
}
}
is ReaderEvent.OnCancelCheckTextForUpdate -> {
_state.update {
updateJob?.cancel()
it.copy(
showUpdateDialog = false,
checkingForUpdate = false
)
}
}
}
}
}
@ -407,6 +506,8 @@ class ReaderViewModel @Inject constructor(
screen: Screen.Reader,
onNavigate: OnNavigate,
fullscreenMode: Boolean,
checkForTextUpdate: Boolean,
checkForTextUpdateToast: () -> Unit,
activity: ComponentActivity,
refreshList: (Book) -> Unit,
onError: (UIText) -> Unit
@ -437,6 +538,15 @@ class ReaderViewModel @Inject constructor(
)
onEvent(
ReaderEvent.OnLoadText(
checkForUpdate = {
if (checkForTextUpdate) {
onEvent(
ReaderEvent.OnCheckTextForUpdate {
checkForTextUpdateToast()
}
)
}
},
refreshList = { refreshList(it) },
onError = {
onError(it)

View file

@ -0,0 +1,31 @@
package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.core.components.LocalMainViewModel
import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.screens.settings.components.SwitchWithTitle
/**
* Check for Text Update setting.
* If true, automatically checks whether there is book's text update.
*/
@Composable
fun CheckForTextUpdateSetting() {
val state = LocalMainViewModel.current.state
val onMainEvent = LocalMainViewModel.current.onEvent
SwitchWithTitle(
selected = state.value.checkForTextUpdate,
title = stringResource(id = R.string.check_for_text_update_option),
description = stringResource(id = R.string.check_for_text_update_option_desc),
onClick = {
onMainEvent(
MainEvent.OnChangeCheckForTextUpdate(
!state.value.checkForTextUpdate
)
)
}
)
}

View file

@ -0,0 +1,33 @@
package ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.core.components.LocalMainViewModel
import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.screens.settings.components.SwitchWithTitle
import ua.acclorite.book_story.presentation.ui.ExpandingTransition
/**
* Check for Text Update Toast setting.
* If true, shows a toast when the text is up-to-date.
*/
@Composable
fun CheckForTextUpdateToastSetting() {
val state = LocalMainViewModel.current.state
val onMainEvent = LocalMainViewModel.current.onEvent
ExpandingTransition(visible = state.value.checkForTextUpdate) {
SwitchWithTitle(
selected = state.value.checkForTextUpdateToast,
title = stringResource(id = R.string.check_for_text_update_toast_option),
onClick = {
onMainEvent(
MainEvent.OnChangeCheckForTextUpdateToast(
!state.value.checkForTextUpdateToast
)
)
}
)
}
}

View file

@ -10,6 +10,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.screens.settings.components.SettingsSubcategory
import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.CheckForTextUpdateSetting
import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.CheckForTextUpdateToastSetting
import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.FullscreenSetting
import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.HideBarsOnFastScrollSetting
import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.KeepScreenOnSetting
@ -34,6 +36,14 @@ fun LazyListScope.MiscSubcategory(
topPadding = topPadding,
bottomPadding = bottomPadding
) {
item {
CheckForTextUpdateSetting()
}
item {
CheckForTextUpdateToastSetting()
}
item {
FullscreenSetting()
}

View file

@ -29,6 +29,7 @@
<string name="delete_history">Видалити історію читання?</string>
<string name="confirm_update">Підтвердити оновлення?</string>
<string name="enable_check_for_updates">Увімнкути перевірку на оновлення?</string>
<string name="update_book">Оновити книгу?</string>
<!-- Dialog Descriptions -->
<string name="storage_permission_description">
@ -75,6 +76,13 @@
з «api.github.com» кожен раз, коли застосунок запускається.
Хочете продовжити та увімкнути «Перевіряти на оновлення»?
</string>
<string name="update_book_description">
Знайдено оновлення тексту книги.
Оновлення необхідне для того, щоб забезпечити використання найновіших методів парсингу.
Це автономний процес і в більшості випадків пов\'язаний з оновленням парсеру текста.
Ваш прогрес не постраждає, якщо не відбудуться значні зміни.
Чи бажаєте ви продовжити оновлення?
</string>
<!-- Errors -->
<string name="error_permission">
@ -153,6 +161,7 @@
<string name="enable">Увімкнути</string>
<string name="web_search">Веб-пошук</string>
<string name="share">Поділитися</string>
<string name="proceed">Продовжити</string>
<!-- Items -->
<string name="files">файли</string>
@ -250,6 +259,9 @@
</string>
<string name="perception_expander_padding_option">Відступи лінії розширення сприйняття</string>
<string name="perception_expander_thickness_option">Товщина лінії розширення сприйняття</string>
<string name="check_for_text_update_option">Перевіряти на оновлення тексту</string>
<string name="check_for_text_update_option_desc">Автоматично перевіряти чи є оновлення тексту</string>
<string name="check_for_text_update_toast_option">Показувати \"текст актуальний\" повідомлення</string>
<!-- Browse -->
<string name="browse_files_structure_option">Структура файлів</string>
@ -340,7 +352,7 @@
<string name="history_element_deleted">Елемент історії був успішно видалений.</string>
<string name="file_not_found">Файл \"%1$s\" не знайдено.</string>
<string name="book_updated">Текст книги був успішно оновлений.</string>
<string name="nothing_changed">Текст книги актуальний.</string>
<string name="nothing_changed">Текст актуальний.</string>
<!-- DropDown properties -->
<string name="move_this_book">Перемістити</string>
@ -606,5 +618,6 @@
<string name="checkpoint_back_content_desc">Чекпоінт назад</string>
<string name="checkpoint_forward_content_desc">Чекпоінт вперед</string>
<string name="chapters_content_desc">Розділи</string>
<string name="update_text_content_desc">Оновити текст</string>
</resources>

View file

@ -32,6 +32,7 @@
<string name="delete_history">Delete reading history?</string>
<string name="confirm_update">Confirm update?</string>
<string name="enable_check_for_updates">Enable check for updates?</string>
<string name="update_book">Update book?</string>
<!-- Dialog Descriptions -->
<string name="storage_permission_description">
@ -65,7 +66,7 @@
</string>
<string name="confirm_update_description">
Update this book\'s text and chapters.
The progress should remain unchanged unless order of paragraphs was changed.
The progress should remain unchanged unless the order of paragraphs was changed.
</string>
<string name="update_app_description">
New version of Book\'s Story was found.
@ -79,6 +80,13 @@
when app starts.
Do you want to proceed and enable «Check for updates»?
</string>
<string name="update_book_description">
Update of the text has been found.
Update is needed to ensure latest parser methods are used.
It is offline process.
Your progress will not be affected unless big changes occur.
Do you want to proceed with update?
</string>
<!-- Errors -->
<string name="error_permission">
@ -156,6 +164,7 @@
<string name="enable">Enable</string>
<string name="web_search">Web search</string>
<string name="share">Share</string>
<string name="proceed">Proceed</string>
<!-- Items -->
<string name="files">files</string>
@ -253,6 +262,9 @@
</string>
<string name="perception_expander_padding_option">Perception expander line padding</string>
<string name="perception_expander_thickness_option">Perception expander line thickness</string>
<string name="check_for_text_update_option">Check for text update</string>
<string name="check_for_text_update_option_desc">Automatically check whether there is text update</string>
<string name="check_for_text_update_toast_option">Show \"text is up-to-date\" toast</string>
<!-- Browse -->
<string name="browse_files_structure_option">Files structure</string>
@ -343,7 +355,7 @@
<string name="history_element_deleted">History element was successfully deleted.</string>
<string name="file_not_found">File \"%1$s\" was not found.</string>
<string name="book_updated">Book\'s text was successfully updated.</string>
<string name="nothing_changed">Book\'s text is up to date.</string>
<string name="nothing_changed">Text is up-to-date.</string>
<!-- DropDown properties -->
<string name="move_this_book">Move</string>
@ -611,5 +623,6 @@
<string name="checkpoint_back_content_desc">Checkpoint back</string>
<string name="checkpoint_forward_content_desc">Checkpoint forward</string>
<string name="chapters_content_desc">Chapters</string>
<string name="update_text_content_desc">Update text</string>
</resources>