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 private val fb2FileParser: Fb2FileParser
) : FileParser { ) : FileParser {
override suspend fun parse(file: File): Pair<Book, CoverImage?>? { override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
if (!file.exists()) {
return null
}
val fileFormat = ".${file.name.substringAfterLast(".")}".lowercase().trim() val fileFormat = ".${file.name.substringAfterLast(".")}".lowercase().trim()
if (fileFormat == ".pdf") { if (fileFormat == ".pdf") {

View file

@ -17,6 +17,12 @@ class TextParserImpl @Inject constructor(
private val fb2TextParser: Fb2TextParser private val fb2TextParser: Fb2TextParser
) : TextParser { ) : TextParser {
override suspend fun parse(file: File): Resource<List<String>> { 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() val fileFormat = ".${file.name.substringAfterLast(".")}".lowercase().trim()
if (fileFormat == ".pdf") { if (fileFormat == ".pdf") {

View file

@ -2,8 +2,10 @@ package ua.acclorite.book_story.data.repository
import android.app.Application import android.app.Application
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import com.google.mlkit.common.model.DownloadConditions 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.Resource
import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.domain.util.UIText
import ua.acclorite.book_story.presentation.data.MainState import ua.acclorite.book_story.presentation.data.MainState
import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.Locale 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( override suspend fun <T> retrieveDataFromDataStore(
key: Preferences.Key<T>, key: Preferences.Key<T>,
defaultValue: T defaultValue: T

View file

@ -52,6 +52,12 @@ interface BookRepository {
books: List<Book> books: List<Book>
) )
suspend fun canResetCover(
bookId: Int
): Boolean
suspend fun resetCoverImage(bookId: Int): Boolean
suspend fun <T> retrieveDataFromDataStore( suspend fun <T> retrieveDataFromDataStore(
key: Preferences.Key<T>, 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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HideImage import androidx.compose.material.icons.filled.HideImage
import androidx.compose.material.icons.filled.ImageSearch import androidx.compose.material.icons.filled.ImageSearch
import androidx.compose.material.icons.filled.Restore
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -62,12 +65,44 @@ fun BookInfoChangeCoverBottomSheet(
} }
) )
LaunchedEffect(Unit) {
onEvent(BookInfoEvent.OnCheckCoverReset)
}
CustomBottomSheet( CustomBottomSheet(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
onDismissRequest = { onDismissRequest = {
onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet) onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet)
} }
) { padding -> ) { padding ->
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()
}
)
)
}
}
}
item {
BookInfoChangeCoverBottomSheetItem( BookInfoChangeCoverBottomSheetItem(
icon = Icons.Default.ImageSearch, icon = Icons.Default.ImageSearch,
text = stringResource(id = R.string.change_cover), text = stringResource(id = R.string.change_cover),
@ -77,8 +112,10 @@ fun BookInfoChangeCoverBottomSheet(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
) )
} }
}
if (state.value.book.coverImage != null) { if (state.value.book.coverImage != null) {
item {
BookInfoChangeCoverBottomSheetItem( BookInfoChangeCoverBottomSheetItem(
icon = Icons.Default.HideImage, icon = Icons.Default.HideImage,
text = stringResource(id = R.string.delete_cover), text = stringResource(id = R.string.delete_cover),
@ -99,11 +136,15 @@ fun BookInfoChangeCoverBottomSheet(
).show() ).show()
} }
} }
}
item {
Spacer( Spacer(
modifier = Modifier.height( modifier = Modifier.height(
8.dp + padding 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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -22,9 +23,15 @@ import androidx.compose.ui.unit.sp
/** /**
* Change cover bottom sheet item. * 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 @Composable
fun BookInfoChangeCoverBottomSheetItem( fun LazyItemScope.BookInfoChangeCoverBottomSheetItem(
icon: ImageVector, icon: ImageVector,
text: String, text: String,
description: String, description: String,
@ -33,6 +40,7 @@ fun BookInfoChangeCoverBottomSheetItem(
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.animateItem()
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
onClick() 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.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.util.OnNavigate import ua.acclorite.book_story.domain.util.OnNavigate
import ua.acclorite.book_story.domain.util.UIText
@Immutable @Immutable
sealed class BookInfoEvent { sealed class BookInfoEvent {
@ -17,6 +18,11 @@ sealed class BookInfoEvent {
data class OnChangeCover(val uri: Uri, val context: Context, val refreshList: (Book) -> Unit) : data class OnChangeCover(val uri: Uri, val context: Context, val refreshList: (Book) -> Unit) :
BookInfoEvent() BookInfoEvent()
data object OnCheckCoverReset : BookInfoEvent()
data class OnResetCoverImage(
val refreshList: (Book) -> Unit,
val showResult: (UIText) -> Unit
) : BookInfoEvent()
data object OnShowHideEditTitle : BookInfoEvent() data object OnShowHideEditTitle : BookInfoEvent()
data class OnRequestFocus(val focusRequester: FocusRequester) : BookInfoEvent() data class OnRequestFocus(val focusRequester: FocusRequester) : BookInfoEvent()
data class OnTitleValueChange(val value: String) : BookInfoEvent() data class OnTitleValueChange(val value: String) : BookInfoEvent()

View file

@ -22,6 +22,8 @@ data class BookInfoState(
val titleValue: String = "", val titleValue: String = "",
val showChangeCoverBottomSheet: Boolean = false, val showChangeCoverBottomSheet: Boolean = false,
val canResetCover: Boolean = false,
val showDetailsBottomSheet: Boolean = false, val showDetailsBottomSheet: Boolean = false,
val showDeleteDialog: 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.Category
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook 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.DeleteBooks
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.GetBookFromFile 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.GetText
import ua.acclorite.book_story.domain.use_case.InsertHistory 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.UpdateBookWithText
import ua.acclorite.book_story.domain.use_case.UpdateBooks import ua.acclorite.book_story.domain.use_case.UpdateBooks
import ua.acclorite.book_story.domain.use_case.UpdateCoverImageOfBook import ua.acclorite.book_story.domain.use_case.UpdateCoverImageOfBook
import ua.acclorite.book_story.domain.util.OnNavigate 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 ua.acclorite.book_story.presentation.data.Screen
import java.io.File import java.io.File
import java.util.Date import java.util.Date
@ -45,7 +48,9 @@ class BookInfoViewModel @Inject constructor(
private val deleteBooks: DeleteBooks, private val deleteBooks: DeleteBooks,
private val getBookFromFile: GetBookFromFile, private val getBookFromFile: GetBookFromFile,
private val getBookById: GetBookById, private val getBookById: GetBookById,
private val getText: GetText private val getText: GetText,
private val canResetCover: CanResetCover,
private val resetCoverImage: ResetCoverImage,
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow(BookInfoState()) private val _state = MutableStateFlow(BookInfoState())
@ -83,7 +88,8 @@ class BookInfoViewModel @Inject constructor(
book = it.book.copy( book = it.book.copy(
coverImage = newCoverImage coverImage = newCoverImage
), ),
showChangeCoverBottomSheet = false showChangeCoverBottomSheet = false,
canResetCover = canResetCover.execute(bookId = it.book.id)
) )
} }
event.refreshList(_state.value.book) event.refreshList(_state.value.book)
@ -105,13 +111,52 @@ class BookInfoViewModel @Inject constructor(
book = it.book.copy( book = it.book.copy(
coverImage = null coverImage = null
), ),
showChangeCoverBottomSheet = false showChangeCoverBottomSheet = false,
canResetCover = canResetCover.execute(bookId = it.book.id)
) )
} }
event.refreshList(_state.value.book) 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 -> { is BookInfoEvent.OnShowHideEditTitle -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val shouldHide = _state.value.editTitle val shouldHide = _state.value.editTitle
@ -662,7 +707,10 @@ class BookInfoViewModel @Inject constructor(
} }
_state.update { _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_wrong_language">Не можна вибрати цю мову.</string>
<string name="error_could_not_download_language">Не вдалося завантажити мову.</string> <string name="error_could_not_download_language">Не вдалося завантажити мову.</string>
<string name="error_could_not_download_languages">Не вдалося завантажити мови.</string> <string name="error_could_not_download_languages">Не вдалося завантажити мови.</string>
<string name="error_could_not_reset_cover">Не вдалося скинути обкладинку.</string>
<!-- Is Empty messages --> <!-- Is Empty messages -->
<string name="browse_empty"> <string name="browse_empty">
@ -186,6 +187,8 @@
<string name="read_more">Вам потрібно читати більше!</string> <string name="read_more">Вам потрібно читати більше!</string>
<string name="read_keep">Так тримати!</string> <string name="read_keep">Так тримати!</string>
<string name="read_done">Браво, ви повністю прочитали цю книгу!</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">Змінити обкладинку</string>
<string name="change_cover_desc">Виберіть нову обкладинку з ваших фото</string> <string name="change_cover_desc">Виберіть нову обкладинку з ваших фото</string>
<string name="delete_cover">Видалити обкладинку</string> <string name="delete_cover">Видалити обкладинку</string>
@ -294,6 +297,7 @@
<string name="translator_canceling">Переклад відміняється. Будь ласка, зачекайте.</string> <string name="translator_canceling">Переклад відміняється. Будь ласка, зачекайте.</string>
<string name="translator_text_canceling">Перекладений текст відміняється. Будь ласка, зачекайте.</string> <string name="translator_text_canceling">Перекладений текст відміняється. Будь ласка, зачекайте.</string>
<string name="translator_starting">Переклад скоро почнеться. Будь ласка, зачекайте.</string> <string name="translator_starting">Переклад скоро почнеться. Будь ласка, зачекайте.</string>
<string name="cover_reset">Обкладинка була успішно скинута.</string>
<!-- Snackbar messages --> <!-- Snackbar messages -->
<string name="history_element_deleted">Елемент історії був успішно видалений.</string> <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_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_language">Could not download language.</string>
<string name="error_could_not_download_languages">Could not download languages.</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 --> <!-- Is Empty messages -->
<string name="browse_empty"> <string name="browse_empty">
@ -204,6 +205,8 @@
<string name="read_more">You need to read more!</string> <string name="read_more">You need to read more!</string>
<string name="read_keep">Keep it up!</string> <string name="read_keep">Keep it up!</string>
<string name="read_done">Well done, you have read the whole book!</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">Change cover image</string>
<string name="change_cover_desc">Choose new cover from your photos</string> <string name="change_cover_desc">Choose new cover from your photos</string>
<string name="delete_cover">Delete cover image</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_canceling">Translation is canceling. Please wait.</string>
<string name="translator_text_canceling">Translated text 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="translator_starting">Translation will start soon. Please wait.</string>
<string name="cover_reset">Cover image was successfully reset.</string>
<!-- Snackbar messages --> <!-- Snackbar messages -->
<string name="history_element_deleted">History element was successfully deleted.</string> <string name="history_element_deleted">History element was successfully deleted.</string>