diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/DataStoreConstants.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/DataStoreConstants.kt index eafd2f31..1d9ba3a3 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/DataStoreConstants.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/constants/DataStoreConstants.kt @@ -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") diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/navigation/Navigation.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/navigation/Navigation.kt index 5212b898..d824d0cb 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/navigation/Navigation.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/navigation/Navigation.kt @@ -88,7 +88,8 @@ sealed class Screen : Parcelable { @Parcelize @Keep data class BookInfo( - val bookId: Int + val bookId: Int, + val startUpdate: Boolean ) : Screen() @Parcelize diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt index 8f843f09..343d306d 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainState.kt index 9c5537da..5e7b001a 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainState.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainState.kt @@ -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 }, ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt index 766cbf27..a3930e94 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt @@ -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 + ) + } + } + } } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt index c7f3a0c5..76d8a6c4 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt @@ -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() diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt index 2d4204e1..1f42134e 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt @@ -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 } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoTopBar.kt index 6a757335..d3f8f4eb 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoTopBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoTopBar.kt @@ -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()) || diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/confirm_update_dialog/BookInfoConfirmUpateDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoConfirmUpdateDialog.kt similarity index 98% rename from app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/confirm_update_dialog/BookInfoConfirmUpateDialog.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoConfirmUpdateDialog.kt index 584ae621..e24b4d95 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/confirm_update_dialog/BookInfoConfirmUpateDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoConfirmUpdateDialog.kt @@ -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 diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt index 16f95535..50400528 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt @@ -101,7 +101,7 @@ sealed class BookInfoEvent { val snackbarState: SnackbarHostState ) : BookInfoEvent() - data class OnLoadUpdate( + data class OnCheckForUpdate( val snackbarState: SnackbarHostState, val context: Context ) : BookInfoEvent() diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt index 6736195a..01014743 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt @@ -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? = null, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt index f8bd329c..ef07ee1b 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt @@ -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) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt index 772d3648..23ec058e 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt @@ -214,7 +214,10 @@ private fun HistoryScreen() { onBodyClick = { onNavigate { navigate( - Screen.BookInfo(it.bookId) + Screen.BookInfo( + bookId = it.bookId, + startUpdate = false + ) ) } }, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt index cf19e3a4..9317c68a 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt @@ -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 + ) + ) } } }, 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 5cda8900..89423194 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 @@ -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 diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderUpdateDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderUpdateDialog.kt new file mode 100644 index 00000000..921766e5 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderUpdateDialog.kt @@ -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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt index ec83f114..9501ff21 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt @@ -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, 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 766d5533..0f1458c1 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 @@ -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() } \ No newline at end of file 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 063f0d33..66b4d670 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 @@ -24,6 +24,10 @@ data class ReaderState( val checkpoint: Pair = 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, ) \ No newline at end of file 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 1b41bda4..a9c2e57f 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 @@ -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() { 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) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/CheckForTextUpdateSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/CheckForTextUpdateSetting.kt new file mode 100644 index 00000000..8cdc7923 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/CheckForTextUpdateSetting.kt @@ -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 + ) + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/CheckForTextUpdateToastSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/CheckForTextUpdateToastSetting.kt new file mode 100644 index 00000000..5b80b021 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/CheckForTextUpdateToastSetting.kt @@ -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 + ) + ) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/MiscSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/MiscSubcategory.kt index 985b0376..fd26a534 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/MiscSubcategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/subcategories/MiscSubcategory.kt @@ -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() } diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index b80fbec5..8bb2dea0 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -29,6 +29,7 @@ Видалити історію читання? Підтвердити оновлення? Увімнкути перевірку на оновлення? + Оновити книгу? @@ -75,6 +76,13 @@ з «api.github.com» кожен раз, коли застосунок запускається. Хочете продовжити та увімкнути «Перевіряти на оновлення»? + + Знайдено оновлення тексту книги. + Оновлення необхідне для того, щоб забезпечити використання найновіших методів парсингу. + Це автономний процес і в більшості випадків пов\'язаний з оновленням парсеру текста. + Ваш прогрес не постраждає, якщо не відбудуться значні зміни. + Чи бажаєте ви продовжити оновлення? + @@ -153,6 +161,7 @@ Увімкнути Веб-пошук Поділитися + Продовжити файли @@ -250,6 +259,9 @@ Відступи лінії розширення сприйняття Товщина лінії розширення сприйняття + Перевіряти на оновлення тексту + Автоматично перевіряти чи є оновлення тексту + Показувати \"текст актуальний\" повідомлення Структура файлів @@ -340,7 +352,7 @@ Елемент історії був успішно видалений. Файл \"%1$s\" не знайдено. Текст книги був успішно оновлений. - Текст книги актуальний. + Текст актуальний. Перемістити @@ -606,5 +618,6 @@ Чекпоінт назад Чекпоінт вперед Розділи + Оновити текст \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dbd1af43..829544d0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,6 +32,7 @@ Delete reading history? Confirm update? Enable check for updates? + Update book? @@ -65,7 +66,7 @@ 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. 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»? + + 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? + @@ -156,6 +164,7 @@ Enable Web search Share + Proceed files @@ -253,6 +262,9 @@ Perception expander line padding Perception expander line thickness + Check for text update + Automatically check whether there is text update + Show \"text is up-to-date\" toast Files structure @@ -343,7 +355,7 @@ History element was successfully deleted. File \"%1$s\" was not found. Book\'s text was successfully updated. - Book\'s text is up to date. + Text is up-to-date. Move @@ -611,5 +623,6 @@ Checkpoint back Checkpoint forward Chapters + Update text \ No newline at end of file