BookInfoScreen: Cover Reset feature.

This commit is contained in:
acclorite 2024-07-03 12:31:19 +03:00
parent 809ecb38c7
commit 45c1bd2b80
13 changed files with 235 additions and 37 deletions

View file

@ -16,6 +16,10 @@ class FileParserImpl @Inject constructor(
private val fb2FileParser: Fb2FileParser
) : FileParser {
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
if (!file.exists()) {
return null
}
val fileFormat = ".${file.name.substringAfterLast(".")}".lowercase().trim()
if (fileFormat == ".pdf") {

View file

@ -17,6 +17,12 @@ class TextParserImpl @Inject constructor(
private val fb2TextParser: Fb2TextParser
) : TextParser {
override suspend fun parse(file: File): Resource<List<String>> {
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") {

View file

@ -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 <T> retrieveDataFromDataStore(
key: Preferences.Key<T>,
defaultValue: T

View file

@ -52,6 +52,12 @@ interface BookRepository {
books: List<Book>
)
suspend fun canResetCover(
bookId: Int
): Boolean
suspend fun resetCoverImage(bookId: Int): Boolean
suspend fun <T> retrieveDataFromDataStore(
key: Preferences.Key<T>,

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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
)
)
}
}

View file

@ -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()

View file

@ -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()

View file

@ -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,

View file

@ -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)
)
}
}
}

View file

@ -123,6 +123,7 @@
<string name="error_wrong_language">Не можна вибрати цю мову.</string>
<string name="error_could_not_download_language">Не вдалося завантажити мову.</string>
<string name="error_could_not_download_languages">Не вдалося завантажити мови.</string>
<string name="error_could_not_reset_cover">Не вдалося скинути обкладинку.</string>
<!-- Is Empty messages -->
<string name="browse_empty">
@ -186,6 +187,8 @@
<string name="read_more">Вам потрібно читати більше!</string>
<string name="read_keep">Так тримати!</string>
<string name="read_done">Браво, ви повністю прочитали цю книгу!</string>
<string name="reset_cover">Скинути обкладинку</string>
<string name="reset_cover_desc">Скиніть обкладинку книги до стандартної</string>
<string name="change_cover">Змінити обкладинку</string>
<string name="change_cover_desc">Виберіть нову обкладинку з ваших фото</string>
<string name="delete_cover">Видалити обкладинку</string>
@ -294,6 +297,7 @@
<string name="translator_canceling">Переклад відміняється. Будь ласка, зачекайте.</string>
<string name="translator_text_canceling">Перекладений текст відміняється. Будь ласка, зачекайте.</string>
<string name="translator_starting">Переклад скоро почнеться. Будь ласка, зачекайте.</string>
<string name="cover_reset">Обкладинка була успішно скинута.</string>
<!-- Snackbar messages -->
<string name="history_element_deleted">Елемент історії був успішно видалений.</string>

View file

@ -142,6 +142,7 @@
<string name="error_wrong_language">Cannot select this language.</string>
<string name="error_could_not_download_language">Could not download language.</string>
<string name="error_could_not_download_languages">Could not download languages.</string>
<string name="error_could_not_reset_cover">Could not reset cover image.</string>
<!-- Is Empty messages -->
<string name="browse_empty">
@ -204,6 +205,8 @@
<string name="read_more">You need to read more!</string>
<string name="read_keep">Keep it up!</string>
<string name="read_done">Well done, you have read the whole book!</string>
<string name="reset_cover">Reset cover image</string>
<string name="reset_cover_desc">Reset cover image of the book to the default</string>
<string name="change_cover">Change cover image</string>
<string name="change_cover_desc">Choose new cover from your photos</string>
<string name="delete_cover">Delete cover image</string>
@ -314,6 +317,7 @@
<string name="translator_canceling">Translation is canceling. Please wait.</string>
<string name="translator_text_canceling">Translated text is canceling. Please wait.</string>
<string name="translator_starting">Translation will start soon. Please wait.</string>
<string name="cover_reset">Cover image was successfully reset.</string>
<!-- Snackbar messages -->
<string name="history_element_deleted">History element was successfully deleted.</string>