BookInfoScreen: Cover Reset feature.
This commit is contained in:
parent
809ecb38c7
commit
45c1bd2b80
13 changed files with 235 additions and 37 deletions
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,12 +65,44 @@ fun BookInfoChangeCoverBottomSheet(
|
|||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
onEvent(BookInfoEvent.OnCheckCoverReset)
|
||||
}
|
||||
|
||||
CustomBottomSheet(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onDismissRequest = {
|
||||
onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet)
|
||||
}
|
||||
) { 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(
|
||||
icon = Icons.Default.ImageSearch,
|
||||
text = stringResource(id = R.string.change_cover),
|
||||
|
|
@ -77,8 +112,10 @@ fun BookInfoChangeCoverBottomSheet(
|
|||
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.value.book.coverImage != null) {
|
||||
item {
|
||||
BookInfoChangeCoverBottomSheetItem(
|
||||
icon = Icons.Default.HideImage,
|
||||
text = stringResource(id = R.string.delete_cover),
|
||||
|
|
@ -99,11 +136,15 @@ fun BookInfoChangeCoverBottomSheet(
|
|||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(
|
||||
modifier = Modifier.height(
|
||||
8.dp + padding
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue