From 45c1bd2b809b1b667c48885147439b85a6de206e Mon Sep 17 00:00:00 2001 From: acclorite Date: Wed, 3 Jul 2024 12:31:19 +0300 Subject: [PATCH] BookInfoScreen: Cover Reset feature. --- .../book_story/data/parser/FileParserImpl.kt | 4 + .../book_story/data/parser/TextParserImpl.kt | 6 + .../data/repository/BookRepositoryImpl.kt | 43 +++++++ .../domain/repository/BookRepository.kt | 6 + .../domain/use_case/CanResetCover.kt | 13 +++ .../domain/use_case/ResetCoverImage.kt | 13 +++ .../BookInfoChangeCoverBottomSheet.kt | 105 ++++++++++++------ .../BookInfoChangeCoverBottomSheetItem.kt | 10 +- .../screens/book_info/data/BookInfoEvent.kt | 6 + .../screens/book_info/data/BookInfoState.kt | 2 + .../book_info/data/BookInfoViewModel.kt | 56 +++++++++- app/src/main/res/values-uk/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + 13 files changed, 235 insertions(+), 37 deletions(-) create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/use_case/CanResetCover.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/use_case/ResetCoverImage.kt diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt index f497ab85..f8547aca 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt @@ -16,6 +16,10 @@ class FileParserImpl @Inject constructor( private val fb2FileParser: Fb2FileParser ) : FileParser { override suspend fun parse(file: File): Pair? { + if (!file.exists()) { + return null + } + val fileFormat = ".${file.name.substringAfterLast(".")}".lowercase().trim() if (fileFormat == ".pdf") { diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt index 30a8ffee..ef7d0ad2 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt @@ -17,6 +17,12 @@ class TextParserImpl @Inject constructor( private val fb2TextParser: Fb2TextParser ) : TextParser { override suspend fun parse(file: File): Resource> { + if (!file.exists()) { + return Resource.Error( + UIText.StringResource(R.string.error_something_went_wrong_with_file) + ) + } + val fileFormat = ".${file.name.substringAfterLast(".")}".lowercase().trim() if (fileFormat == ".pdf") { diff --git a/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt index 8e894166..cc296656 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt @@ -2,8 +2,10 @@ package ua.acclorite.book_story.data.repository import android.app.Application import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.net.Uri import android.os.Environment +import android.provider.MediaStore import android.util.Log import androidx.datastore.preferences.core.Preferences import com.google.mlkit.common.model.DownloadConditions @@ -46,6 +48,7 @@ import ua.acclorite.book_story.domain.util.LanguageCode import ua.acclorite.book_story.domain.util.Resource import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.presentation.data.MainState +import java.io.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream import java.util.Locale @@ -417,6 +420,46 @@ class BookRepositoryImpl @Inject constructor( ) } + override suspend fun canResetCover(bookId: Int): Boolean { + val book = database.findBookById(bookId) + + val defaultCoverUncompressed = fileParser.parse(File(book.filePath))?.second + ?: return false + + if (book.image == null) { + return true + } + + val stream = ByteArrayOutputStream() + defaultCoverUncompressed.copy(Bitmap.Config.RGB_565, false).compress( + Bitmap.CompressFormat.WEBP, + 20, + stream + ) + val byteArray = stream.toByteArray() + val defaultCover = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) + + val currentCover = MediaStore.Images.Media.getBitmap( + application.contentResolver, + Uri.parse(book.image) + ) + + return !defaultCover.sameAs(currentCover) + } + + override suspend fun resetCoverImage(bookId: Int): Boolean { + if (!canResetCover(bookId)) { + return false + } + + val book = database.findBookById(bookId) + val defaultCover = fileParser.parse(File(book.filePath))?.second + ?: return false + updateCoverImageOfBook(bookMapper.toBook(book), defaultCover) + + return true + } + override suspend fun retrieveDataFromDataStore( key: Preferences.Key, defaultValue: T diff --git a/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt b/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt index 2cd1e389..7f83f74c 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt @@ -52,6 +52,12 @@ interface BookRepository { books: List ) + suspend fun canResetCover( + bookId: Int + ): Boolean + + suspend fun resetCoverImage(bookId: Int): Boolean + suspend fun retrieveDataFromDataStore( key: Preferences.Key, diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/CanResetCover.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/CanResetCover.kt new file mode 100644 index 00000000..d3b6b516 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/CanResetCover.kt @@ -0,0 +1,13 @@ +package ua.acclorite.book_story.domain.use_case + +import ua.acclorite.book_story.domain.repository.BookRepository +import javax.inject.Inject + +class CanResetCover @Inject constructor( + private val repository: BookRepository +) { + + suspend fun execute(bookId: Int): Boolean { + return repository.canResetCover(bookId) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/ResetCoverImage.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/ResetCoverImage.kt new file mode 100644 index 00000000..52fba9d1 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/ResetCoverImage.kt @@ -0,0 +1,13 @@ +package ua.acclorite.book_story.domain.use_case + +import ua.acclorite.book_story.domain.repository.BookRepository +import javax.inject.Inject + +class ResetCoverImage @Inject constructor( + private val repository: BookRepository +) { + + suspend fun execute(bookId: Int): Boolean { + return repository.resetCoverImage(bookId) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt index 7c9251f8..f8e79a38 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt @@ -7,10 +7,13 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.HideImage import androidx.compose.material.icons.filled.ImageSearch +import androidx.compose.material.icons.filled.Restore import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -62,48 +65,86 @@ fun BookInfoChangeCoverBottomSheet( } ) + LaunchedEffect(Unit) { + onEvent(BookInfoEvent.OnCheckCoverReset) + } + CustomBottomSheet( modifier = Modifier.fillMaxWidth(), onDismissRequest = { onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet) } ) { padding -> - BookInfoChangeCoverBottomSheetItem( - icon = Icons.Default.ImageSearch, - text = stringResource(id = R.string.change_cover), - description = stringResource(id = R.string.change_cover_desc) - ) { - photoPicker.launch( - PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) - ) - } + LazyColumn(Modifier.fillMaxWidth()) { + if (state.value.canResetCover) { + item { + BookInfoChangeCoverBottomSheetItem( + icon = Icons.Default.Restore, + text = stringResource(id = R.string.reset_cover), + description = stringResource(id = R.string.reset_cover_desc) + ) { + onEvent( + BookInfoEvent.OnResetCoverImage( + refreshList = { + onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it)) + onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it)) + }, + showResult = { + Toast.makeText( + context, + it.asString(context), + Toast.LENGTH_LONG + ).show() + } + ) + ) + } + } + } - if (state.value.book.coverImage != null) { - BookInfoChangeCoverBottomSheetItem( - icon = Icons.Default.HideImage, - text = stringResource(id = R.string.delete_cover), - description = stringResource(id = R.string.delete_cover_desc) - ) { - onEvent( - BookInfoEvent.OnDeleteCover( - refreshList = { - onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it)) - onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it)) - } + item { + BookInfoChangeCoverBottomSheetItem( + icon = Icons.Default.ImageSearch, + text = stringResource(id = R.string.change_cover), + description = stringResource(id = R.string.change_cover_desc) + ) { + photoPicker.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) + ) + } + } + + if (state.value.book.coverImage != null) { + item { + BookInfoChangeCoverBottomSheetItem( + icon = Icons.Default.HideImage, + text = stringResource(id = R.string.delete_cover), + description = stringResource(id = R.string.delete_cover_desc) + ) { + onEvent( + BookInfoEvent.OnDeleteCover( + refreshList = { + onLibraryUpdateEvent(LibraryEvent.OnUpdateBook(it)) + onHistoryUpdateEvent(HistoryEvent.OnUpdateBook(it)) + } + ) + ) + Toast.makeText( + context, + context.getString(R.string.cover_image_deleted), + Toast.LENGTH_LONG + ).show() + } + } + } + + item { + Spacer( + modifier = Modifier.height( + 8.dp + padding ) ) - Toast.makeText( - context, - context.getString(R.string.cover_image_deleted), - Toast.LENGTH_LONG - ).show() } } - - Spacer( - modifier = Modifier.height( - 8.dp + padding - ) - ) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheetItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheetItem.kt index fc8edcf4..de2b5cdb 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheetItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheetItem.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -22,9 +23,15 @@ import androidx.compose.ui.unit.sp /** * Change cover bottom sheet item. + * + * @param icon Icon. + * @param text Text. + * @param description Description of the item. + * @param verticalPadding Vertical item padding. + * @param onClick OnClick callback. */ @Composable -fun BookInfoChangeCoverBottomSheetItem( +fun LazyItemScope.BookInfoChangeCoverBottomSheetItem( icon: ImageVector, text: String, description: String, @@ -33,6 +40,7 @@ fun BookInfoChangeCoverBottomSheetItem( ) { Row( modifier = Modifier + .animateItem() .fillMaxWidth() .clickable { onClick() 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 a6d06d00..8363ff54 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 @@ -8,6 +8,7 @@ import androidx.compose.ui.focus.FocusRequester import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.util.OnNavigate +import ua.acclorite.book_story.domain.util.UIText @Immutable sealed class BookInfoEvent { @@ -17,6 +18,11 @@ sealed class BookInfoEvent { data class OnChangeCover(val uri: Uri, val context: Context, val refreshList: (Book) -> Unit) : BookInfoEvent() + data object OnCheckCoverReset : BookInfoEvent() + data class OnResetCoverImage( + val refreshList: (Book) -> Unit, + val showResult: (UIText) -> Unit + ) : BookInfoEvent() data object OnShowHideEditTitle : BookInfoEvent() data class OnRequestFocus(val focusRequester: FocusRequester) : BookInfoEvent() data class OnTitleValueChange(val value: String) : 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 9a96949e..1bba16b1 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 @@ -22,6 +22,8 @@ data class BookInfoState( val titleValue: String = "", val showChangeCoverBottomSheet: Boolean = false, + val canResetCover: Boolean = false, + val showDetailsBottomSheet: Boolean = false, val showDeleteDialog: Boolean = false, 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 72a52b92..753c7523 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 @@ -21,15 +21,18 @@ import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.NullableBook +import ua.acclorite.book_story.domain.use_case.CanResetCover import ua.acclorite.book_story.domain.use_case.DeleteBooks import ua.acclorite.book_story.domain.use_case.GetBookById import ua.acclorite.book_story.domain.use_case.GetBookFromFile import ua.acclorite.book_story.domain.use_case.GetText import ua.acclorite.book_story.domain.use_case.InsertHistory +import ua.acclorite.book_story.domain.use_case.ResetCoverImage import ua.acclorite.book_story.domain.use_case.UpdateBookWithText import ua.acclorite.book_story.domain.use_case.UpdateBooks import ua.acclorite.book_story.domain.use_case.UpdateCoverImageOfBook import ua.acclorite.book_story.domain.util.OnNavigate +import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.presentation.data.Screen import java.io.File import java.util.Date @@ -45,7 +48,9 @@ class BookInfoViewModel @Inject constructor( private val deleteBooks: DeleteBooks, private val getBookFromFile: GetBookFromFile, private val getBookById: GetBookById, - private val getText: GetText + private val getText: GetText, + private val canResetCover: CanResetCover, + private val resetCoverImage: ResetCoverImage, ) : ViewModel() { private val _state = MutableStateFlow(BookInfoState()) @@ -83,7 +88,8 @@ class BookInfoViewModel @Inject constructor( book = it.book.copy( coverImage = newCoverImage ), - showChangeCoverBottomSheet = false + showChangeCoverBottomSheet = false, + canResetCover = canResetCover.execute(bookId = it.book.id) ) } event.refreshList(_state.value.book) @@ -105,13 +111,52 @@ class BookInfoViewModel @Inject constructor( book = it.book.copy( coverImage = null ), - showChangeCoverBottomSheet = false + showChangeCoverBottomSheet = false, + canResetCover = canResetCover.execute(bookId = it.book.id) ) } event.refreshList(_state.value.book) } } + is BookInfoEvent.OnCheckCoverReset -> { + viewModelScope.launch(Dispatchers.IO) { + _state.update { + it.copy( + canResetCover = canResetCover.execute(_state.value.book.id) + ) + } + } + } + + is BookInfoEvent.OnResetCoverImage -> { + viewModelScope.launch { + val result = resetCoverImage.execute(_state.value.book.id) + + if (!result) { + event.showResult(UIText.StringResource(R.string.error_could_not_reset_cover)) + return@launch + } + + val book = getBookById.execute(_state.value.book.id) + + if (book == null) { + event.showResult(UIText.StringResource(R.string.error_something_went_wrong)) + return@launch + } + + _state.update { + it.copy( + book = book, + showChangeCoverBottomSheet = false, + canResetCover = false + ) + } + event.showResult(UIText.StringResource(R.string.cover_reset)) + event.refreshList(_state.value.book) + } + } + is BookInfoEvent.OnShowHideEditTitle -> { viewModelScope.launch(Dispatchers.IO) { val shouldHide = _state.value.editTitle @@ -662,7 +707,10 @@ class BookInfoViewModel @Inject constructor( } _state.update { - BookInfoState(book = book) + BookInfoState( + book = book, + canResetCover = canResetCover.execute(screen.bookId) + ) } } } diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index ed8ccd7e..b046aacd 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -123,6 +123,7 @@ Не можна вибрати цю мову. Не вдалося завантажити мову. Не вдалося завантажити мови. + Не вдалося скинути обкладинку. @@ -186,6 +187,8 @@ Вам потрібно читати більше! Так тримати! Браво, ви повністю прочитали цю книгу! + Скинути обкладинку + Скиніть обкладинку книги до стандартної Змінити обкладинку Виберіть нову обкладинку з ваших фото Видалити обкладинку @@ -294,6 +297,7 @@ Переклад відміняється. Будь ласка, зачекайте. Перекладений текст відміняється. Будь ласка, зачекайте. Переклад скоро почнеться. Будь ласка, зачекайте. + Обкладинка була успішно скинута. Елемент історії був успішно видалений. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b3e141b5..b6b6e9d7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -142,6 +142,7 @@ Cannot select this language. Could not download language. Could not download languages. + Could not reset cover image. @@ -204,6 +205,8 @@ You need to read more! Keep it up! Well done, you have read the whole book! + Reset cover image + Reset cover image of the book to the default Change cover image Choose new cover from your photos Delete cover image @@ -314,6 +317,7 @@ Translation is canceling. Please wait. Translated text is canceling. Please wait. Translation will start soon. Please wait. + Cover image was successfully reset. History element was successfully deleted.