🚀 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 = booleanPreferencesKey("perception_expander")
val PERCEPTION_EXPANDER_PADDING = intPreferencesKey("perception_expander_padding") val PERCEPTION_EXPANDER_PADDING = intPreferencesKey("perception_expander_padding")
val PERCEPTION_EXPANDER_THICKNESS = intPreferencesKey("perception_expander_thickness") 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 // Browse settings
val BROWSE_FILES_STRUCTURE = stringPreferencesKey("browse_files_structure") val BROWSE_FILES_STRUCTURE = stringPreferencesKey("browse_files_structure")

View file

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

View file

@ -40,4 +40,6 @@ sealed class MainEvent {
data class OnChangePerceptionExpander(val bool: Boolean) : MainEvent() data class OnChangePerceptionExpander(val bool: Boolean) : MainEvent()
data class OnChangePerceptionExpanderPadding(val padding: Int) : MainEvent() data class OnChangePerceptionExpanderPadding(val padding: Int) : MainEvent()
data class OnChangePerceptionExpanderThickness(val thickness: 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 perceptionExpander: Boolean = provideDefaultValue { false },
val perceptionExpanderPadding: Int = provideDefaultValue { 5 }, val perceptionExpanderPadding: Int = provideDefaultValue { 5 },
val perceptionExpanderThickness: Int = provideDefaultValue { 4 }, val perceptionExpanderThickness: Int = provideDefaultValue { 4 },
val checkForTextUpdate: Boolean = provideDefaultValue { true },
val checkForTextUpdateToast: Boolean = provideDefaultValue { true },
// Browse Settings // Browse Settings
val browseFilesStructure: BrowseFilesStructure = provideDefaultValue { val browseFilesStructure: BrowseFilesStructure = provideDefaultValue {
@ -254,6 +256,14 @@ data class MainState(
perceptionExpanderThickness = provideValue( perceptionExpanderThickness = provideValue(
PERCEPTION_EXPANDER_THICKNESS PERCEPTION_EXPANDER_THICKNESS
) { perceptionExpanderThickness }, ) { 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.BookInfoStatisticSection
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoTopBar 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.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.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.BookInfoDeleteDialog
import ua.acclorite.book_story.presentation.screens.book_info.components.dialog.BookInfoMoveDialog import ua.acclorite.book_story.presentation.screens.book_info.components.dialog.BookInfoMoveDialog
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent 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) { fun BookInfoScreenRoot(screen: Screen.BookInfo) {
val viewModel = LocalBookInfoViewModel.current.viewModel val viewModel = LocalBookInfoViewModel.current.viewModel
val onNavigate = LocalOnNavigate.current val onNavigate = LocalOnNavigate.current
val context = LocalContext.current
val snackbarState = remember { SnackbarHostState() }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.init( viewModel.init(
screen = screen, screen = screen,
onNavigate = onNavigate snackbarState = snackbarState,
onNavigate = onNavigate,
context = context
) )
} }
BookInfoScreen() BookInfoScreen(
snackbarState = snackbarState
)
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
@ -84,19 +91,18 @@ fun BookInfoScreenRoot(screen: Screen.BookInfo) {
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
private fun BookInfoScreen() { private fun BookInfoScreen(snackbarState: SnackbarHostState) {
val context = LocalContext.current val context = LocalContext.current
val state = LocalBookInfoViewModel.current.state val state = LocalBookInfoViewModel.current.state
val onEvent = LocalBookInfoViewModel.current.onEvent val onEvent = LocalBookInfoViewModel.current.onEvent
val onNavigate = LocalOnNavigate.current val onNavigate = LocalOnNavigate.current
val listState = rememberLazyListState() val listState = rememberLazyListState()
val snackbarState = remember { SnackbarHostState() }
val refreshState = rememberPullRefreshState( val refreshState = rememberPullRefreshState(
refreshing = state.value.isRefreshing || state.value.isLoadingUpdate, refreshing = state.value.updating || state.value.checkingForUpdate,
onRefresh = { onRefresh = {
onEvent( onEvent(
BookInfoEvent.OnLoadUpdate( BookInfoEvent.OnCheckForUpdate(
snackbarState, snackbarState,
context context
) )
@ -181,7 +187,7 @@ private fun BookInfoScreen() {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
if (!state.value.isRefreshing) { if (!state.value.updating) {
onEvent(BookInfoEvent.OnNavigateToReaderScreen(onNavigate = onNavigate)) onEvent(BookInfoEvent.OnNavigateToReaderScreen(onNavigate = onNavigate))
} }
}, },
@ -221,7 +227,7 @@ private fun BookInfoScreen() {
) )
PullRefreshIndicator( PullRefreshIndicator(
state.value.isRefreshing || state.value.isLoadingUpdate, state.value.updating || state.value.checkingForUpdate,
refreshState, refreshState,
Modifier Modifier
.align(Alignment.TopCenter) .align(Alignment.TopCenter)
@ -254,7 +260,7 @@ private fun BookInfoScreen() {
return@BackHandler return@BackHandler
} }
if (!state.value.isRefreshing) { if (!state.value.updating) {
onEvent(BookInfoEvent.OnCancelUpdate) onEvent(BookInfoEvent.OnCancelUpdate)
onNavigate { onNavigate {
navigateBack() navigateBack()

View file

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

View file

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

View file

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

View file

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

View file

@ -214,7 +214,10 @@ private fun HistoryScreen() {
onBodyClick = { onBodyClick = {
onNavigate { onNavigate {
navigate( 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)) onEvent(LibraryEvent.OnSelectBook(it))
} else { } else {
onNavigate { 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.ReaderChaptersDrawer
import ua.acclorite.book_story.presentation.screens.reader.components.ReaderPerceptionExpander 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.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.ReaderBottomBar
import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.ReaderTopBar import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.ReaderTopBar
import ua.acclorite.book_story.presentation.screens.reader.components.readerFastColorPresetChange import ua.acclorite.book_story.presentation.screens.reader.components.readerFastColorPresetChange
@ -117,6 +118,15 @@ fun ReaderScreenRoot(screen: Screen.Reader) {
screen = screen, screen = screen,
onNavigate = onNavigate, onNavigate = onNavigate,
fullscreenMode = mainState.value.fullscreen, 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, activity = context,
refreshList = { refreshList = {
onLibraryEvent(LibraryEvent.OnUpdateBook(it)) onLibraryEvent(LibraryEvent.OnUpdateBook(it))
@ -350,6 +360,9 @@ private fun ReaderScreen(lazyListState: LazyListState) {
if (state.value.showSettingsBottomSheet) { if (state.value.showSettingsBottomSheet) {
ReaderSettingsBottomSheet() ReaderSettingsBottomSheet()
} }
if (state.value.showUpdateDialog) {
ReaderUpdateDialog()
}
Scaffold( Scaffold(
Modifier 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.activity.ComponentActivity
import androidx.compose.animation.core.animateFloatAsState 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.background
import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth 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.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.rounded.Menu 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.ExperimentalMaterial3Api
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -18,14 +26,17 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import ua.acclorite.book_story.R 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.CustomIconButton
import ua.acclorite.book_story.presentation.core.components.LocalHistoryViewModel import ua.acclorite.book_story.presentation.core.components.LocalHistoryViewModel
import ua.acclorite.book_story.presentation.core.components.LocalLibraryViewModel import ua.acclorite.book_story.presentation.core.components.LocalLibraryViewModel
@ -92,8 +103,10 @@ fun ReaderTopBar() {
fontSize = 20.sp, fontSize = 20.sp,
lineHeight = 20.sp, lineHeight = 20.sp,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
overflow = TextOverflow.Ellipsis,
maxLines = 1, maxLines = 1,
modifier = Modifier modifier = Modifier
.padding(end = 8.dp)
.noRippleClickable( .noRippleClickable(
enabled = !state.value.lockMenu, enabled = !state.value.lockMenu,
onClick = { onClick = {
@ -107,7 +120,10 @@ fun ReaderTopBar() {
navigate = { navigate = {
onNavigate { onNavigate {
navigate( navigate(
Screen.BookInfo(state.value.book.id), Screen.BookInfo(
bookId = state.value.book.id,
startUpdate = false
),
useBackAnimation = true, useBackAnimation = true,
saveInBackStack = false saveInBackStack = false
) )
@ -117,8 +133,9 @@ fun ReaderTopBar() {
) )
} }
) )
.basicMarquee( .then(
iterations = Int.MAX_VALUE if (state.value.checkingForUpdate || state.value.updateFound) Modifier
else Modifier.basicMarquee(iterations = Int.MAX_VALUE)
) )
) )
Text( Text(
@ -132,6 +149,40 @@ fun ReaderTopBar() {
} }
}, },
actions = { 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) { if (state.value.currentChapter != null) {
CustomIconButton( CustomIconButton(
icon = Icons.Rounded.Menu, icon = Icons.Rounded.Menu,

View file

@ -11,6 +11,7 @@ sealed class ReaderEvent {
data object OnTextIsEmpty : ReaderEvent() data object OnTextIsEmpty : ReaderEvent()
data class OnLoadText( data class OnLoadText(
val checkForUpdate: () -> Unit,
val refreshList: (Book) -> Unit, val refreshList: (Book) -> Unit,
val onError: (UIText) -> Unit, val onError: (UIText) -> Unit,
val onTextIsEmpty: () -> Unit val onTextIsEmpty: () -> Unit
@ -86,4 +87,14 @@ sealed class ReaderEvent {
val context: ComponentActivity, val context: ComponentActivity,
val noAppsFound: () -> Unit val noAppsFound: () -> Unit
) : ReaderEvent() ) : 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 checkpoint: Pair<Int, Int> = 0 to 0,
val lockMenu: Boolean = false, val lockMenu: Boolean = false,
val checkingForUpdate: Boolean = false,
val updateFound: Boolean = false,
val showUpdateDialog: Boolean = false,
val showSettingsBottomSheet: Boolean = false, val showSettingsBottomSheet: Boolean = false,
val currentPage: Int = 0, val currentPage: Int = 0,
) )

View file

@ -22,18 +22,22 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book 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.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.ParseText
import ua.acclorite.book_story.domain.use_case.UpdateBook import ua.acclorite.book_story.domain.use_case.UpdateBook
import ua.acclorite.book_story.domain.util.OnNavigate 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.domain.util.UIText
import ua.acclorite.book_story.presentation.core.navigation.Screen 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.BaseViewModel
import ua.acclorite.book_story.presentation.core.util.launchActivity import ua.acclorite.book_story.presentation.core.util.launchActivity
import java.io.File
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -43,7 +47,8 @@ class ReaderViewModel @Inject constructor(
private val updateBook: UpdateBook, private val updateBook: UpdateBook,
private val getText: GetText, private val getText: GetText,
private val getLatestHistory: GetLatestHistory, private val getLatestHistory: GetLatestHistory,
private val getBookById: GetBookById private val getBookById: GetBookById,
private val parseText: ParseText
) : BaseViewModel<ReaderState, ReaderEvent>() { ) : BaseViewModel<ReaderState, ReaderEvent>() {
private val _state = MutableStateFlow(ReaderState()) private val _state = MutableStateFlow(ReaderState())
@ -51,6 +56,7 @@ class ReaderViewModel @Inject constructor(
private var eventJob = SupervisorJob() private var eventJob = SupervisorJob()
private var scrollJob: Job? = null private var scrollJob: Job? = null
private var updateJob: Job? = null
override fun onEvent(event: ReaderEvent) { override fun onEvent(event: ReaderEvent) {
viewModelScope.launch(eventJob + Dispatchers.Main) { viewModelScope.launch(eventJob + Dispatchers.Main) {
@ -95,6 +101,8 @@ class ReaderViewModel @Inject constructor(
_state.value.book.scrollOffset _state.value.book.scrollOffset
) )
event.checkForUpdate()
delay(100) delay(100)
_state.update { _state.update {
it.copy( it.copy(
@ -399,6 +407,97 @@ class ReaderViewModel @Inject constructor(
event.noAppsFound() 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, screen: Screen.Reader,
onNavigate: OnNavigate, onNavigate: OnNavigate,
fullscreenMode: Boolean, fullscreenMode: Boolean,
checkForTextUpdate: Boolean,
checkForTextUpdateToast: () -> Unit,
activity: ComponentActivity, activity: ComponentActivity,
refreshList: (Book) -> Unit, refreshList: (Book) -> Unit,
onError: (UIText) -> Unit onError: (UIText) -> Unit
@ -437,6 +538,15 @@ class ReaderViewModel @Inject constructor(
) )
onEvent( onEvent(
ReaderEvent.OnLoadText( ReaderEvent.OnLoadText(
checkForUpdate = {
if (checkForTextUpdate) {
onEvent(
ReaderEvent.OnCheckTextForUpdate {
checkForTextUpdateToast()
}
)
}
},
refreshList = { refreshList(it) }, refreshList = { refreshList(it) },
onError = { onError = {
onError(it) 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 androidx.compose.ui.unit.Dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.screens.settings.components.SettingsSubcategory 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.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.HideBarsOnFastScrollSetting
import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.KeepScreenOnSetting import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.settings.KeepScreenOnSetting
@ -34,6 +36,14 @@ fun LazyListScope.MiscSubcategory(
topPadding = topPadding, topPadding = topPadding,
bottomPadding = bottomPadding bottomPadding = bottomPadding
) { ) {
item {
CheckForTextUpdateSetting()
}
item {
CheckForTextUpdateToastSetting()
}
item { item {
FullscreenSetting() FullscreenSetting()
} }

View file

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

View file

@ -32,6 +32,7 @@
<string name="delete_history">Delete reading history?</string> <string name="delete_history">Delete reading history?</string>
<string name="confirm_update">Confirm update?</string> <string name="confirm_update">Confirm update?</string>
<string name="enable_check_for_updates">Enable check for updates?</string> <string name="enable_check_for_updates">Enable check for updates?</string>
<string name="update_book">Update book?</string>
<!-- Dialog Descriptions --> <!-- Dialog Descriptions -->
<string name="storage_permission_description"> <string name="storage_permission_description">
@ -65,7 +66,7 @@
</string> </string>
<string name="confirm_update_description"> <string name="confirm_update_description">
Update this book\'s text and chapters. 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>
<string name="update_app_description"> <string name="update_app_description">
New version of Book\'s Story was found. New version of Book\'s Story was found.
@ -79,6 +80,13 @@
when app starts. when app starts.
Do you want to proceed and enable «Check for updates»? Do you want to proceed and enable «Check for updates»?
</string> </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 --> <!-- Errors -->
<string name="error_permission"> <string name="error_permission">
@ -156,6 +164,7 @@
<string name="enable">Enable</string> <string name="enable">Enable</string>
<string name="web_search">Web search</string> <string name="web_search">Web search</string>
<string name="share">Share</string> <string name="share">Share</string>
<string name="proceed">Proceed</string>
<!-- Items --> <!-- Items -->
<string name="files">files</string> <string name="files">files</string>
@ -253,6 +262,9 @@
</string> </string>
<string name="perception_expander_padding_option">Perception expander line padding</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="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 --> <!-- Browse -->
<string name="browse_files_structure_option">Files structure</string> <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="history_element_deleted">History element was successfully deleted.</string>
<string name="file_not_found">File \"%1$s\" was not found.</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="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 --> <!-- DropDown properties -->
<string name="move_this_book">Move</string> <string name="move_this_book">Move</string>
@ -611,5 +623,6 @@
<string name="checkpoint_back_content_desc">Checkpoint back</string> <string name="checkpoint_back_content_desc">Checkpoint back</string>
<string name="checkpoint_forward_content_desc">Checkpoint forward</string> <string name="checkpoint_forward_content_desc">Checkpoint forward</string>
<string name="chapters_content_desc">Chapters</string> <string name="chapters_content_desc">Chapters</string>
<string name="update_text_content_desc">Update text</string>
</resources> </resources>