merge(fix): 221-cannot-open-books into develop
This commit is contained in:
commit
798958c56b
12 changed files with 148 additions and 121 deletions
|
|
@ -9,8 +9,6 @@ package ua.acclorite.book_story.data.model.file
|
|||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import com.anggrayudi.storage.file.DocumentFileCompat
|
||||
import com.anggrayudi.storage.file.getAbsolutePath
|
||||
|
||||
object CachedFileCompat {
|
||||
fun fromUri(context: Context, uri: Uri, builder: CachedFileBuilder? = null): CachedFile {
|
||||
|
|
@ -32,61 +30,6 @@ object CachedFileCompat {
|
|||
)
|
||||
}
|
||||
|
||||
fun fromFullPath(
|
||||
context: Context,
|
||||
path: String,
|
||||
builder: CachedFileBuilder? = null
|
||||
): CachedFile? {
|
||||
val uri = try {
|
||||
val storageId = DocumentFileCompat.getStorageId(context, path)
|
||||
if (storageId.isBlank()) throw NullPointerException("Could not get storageId.")
|
||||
|
||||
val basePath = DocumentFileCompat.getBasePath(context, path)
|
||||
if (basePath.isBlank()) throw NullPointerException("Could not get basePath.")
|
||||
|
||||
val parentUri = context.contentResolver.persistedUriPermissions.find {
|
||||
try {
|
||||
val persistedUri = DocumentFileCompat.fromUri(context, it.uri)
|
||||
val persistedUriPath = persistedUri?.getAbsolutePath(context)
|
||||
|
||||
return@find !(persistedUri == null ||
|
||||
!persistedUri.canRead() ||
|
||||
persistedUriPath.isNullOrBlank() ||
|
||||
!path.startsWith(persistedUriPath, ignoreCase = true))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return@find false
|
||||
}
|
||||
}?.uri
|
||||
if (parentUri == null) throw NullPointerException("Could not get parentUri.")
|
||||
|
||||
DocumentsContract.buildDocumentUriUsingTree(parentUri, "$storageId:$basePath")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
|
||||
val cachedFile = CachedFile(
|
||||
context = context,
|
||||
uri = when {
|
||||
DocumentsContract.isDocumentUri(context, uri) -> uri
|
||||
|
||||
DocumentsContract.isTreeUri(uri) -> {
|
||||
DocumentsContract.buildDocumentUriUsingTree(
|
||||
uri,
|
||||
DocumentsContract.getTreeDocumentId(uri)
|
||||
)
|
||||
}
|
||||
|
||||
else -> uri
|
||||
},
|
||||
builder = builder
|
||||
)
|
||||
|
||||
if (!cachedFile.canAccess()) return null
|
||||
return cachedFile
|
||||
}
|
||||
|
||||
fun build(
|
||||
name: String? = null,
|
||||
path: String? = null,
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ package ua.acclorite.book_story.data.repository
|
|||
import ua.acclorite.book_story.core.CoverImage
|
||||
import ua.acclorite.book_story.data.local.room.BookDao
|
||||
import ua.acclorite.book_story.data.mapper.book.BookMapper
|
||||
import ua.acclorite.book_story.data.mapper.file.FileMapper
|
||||
import ua.acclorite.book_story.data.parser.FileParser
|
||||
import ua.acclorite.book_story.data.parser.TextParser
|
||||
import ua.acclorite.book_story.domain.model.file.File
|
||||
import ua.acclorite.book_story.domain.model.library.Book
|
||||
import ua.acclorite.book_story.domain.model.reader.ReaderText
|
||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||
|
|
@ -22,6 +24,7 @@ import javax.inject.Singleton
|
|||
class BookRepositoryImpl @Inject constructor(
|
||||
private val database: BookDao,
|
||||
private val bookMapper: BookMapper,
|
||||
private val fileMapper: FileMapper,
|
||||
private val fileParser: FileParser,
|
||||
private val textParser: TextParser,
|
||||
private val fileProvider: FileProvider
|
||||
|
|
@ -44,6 +47,12 @@ class BookRepositoryImpl @Inject constructor(
|
|||
.mapCatching { textParser.parse(it) }
|
||||
}
|
||||
|
||||
override suspend fun getFileFromBook(bookId: Int): Result<File> {
|
||||
return getBook(bookId)
|
||||
.mapCatching { fileProvider.getFileFromBook(it).getOrThrow() }
|
||||
.mapCatching { fileMapper.toFile(it) }
|
||||
}
|
||||
|
||||
override suspend fun addBook(book: Book): Result<Unit> = runCatching {
|
||||
database.insertBook(bookMapper.toBookEntity(book))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,21 +18,23 @@ class FileProviderImpl @Inject constructor(
|
|||
) : FileProvider {
|
||||
|
||||
override fun getFileFromBook(book: Book): Result<CachedFile> = runCatching {
|
||||
CachedFileCompat.fromFullPath(
|
||||
context = application,
|
||||
path = book.filePath,
|
||||
builder = CachedFileCompat.build(
|
||||
name = book.filePath.substringAfterLast(java.io.File.separator),
|
||||
path = book.filePath,
|
||||
isDirectory = false
|
||||
application.contentResolver.persistedUriPermissions.forEach { storage ->
|
||||
val storageFile = CachedFileCompat.fromUri(
|
||||
application,
|
||||
storage.uri
|
||||
)
|
||||
).let { file ->
|
||||
if (file == null || !file.canAccess()) {
|
||||
throw NoSuchElementException("Could not load CachedFile.")
|
||||
|
||||
if (!storageFile.isDirectory) return@forEach
|
||||
if (!book.filePath.startsWith(storageFile.path, ignoreCase = true)) return@forEach
|
||||
|
||||
storageFile.walk().forEach { file ->
|
||||
if (book.filePath.equals(file.path, ignoreCase = true)) {
|
||||
return@runCatching file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file
|
||||
}
|
||||
throw NoSuchElementException("Could not find file from book.")
|
||||
}
|
||||
|
||||
override fun getStorageFiles(): Result<List<CachedFile>> = runCatching {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
package ua.acclorite.book_story.domain.repository
|
||||
|
||||
import ua.acclorite.book_story.core.CoverImage
|
||||
import ua.acclorite.book_story.domain.model.file.File
|
||||
import ua.acclorite.book_story.domain.model.library.Book
|
||||
import ua.acclorite.book_story.domain.model.reader.ReaderText
|
||||
|
||||
|
|
@ -23,6 +24,10 @@ interface BookRepository {
|
|||
bookId: Int
|
||||
): Result<List<ReaderText>>
|
||||
|
||||
suspend fun getFileFromBook(
|
||||
bookId: Int
|
||||
): Result<File>
|
||||
|
||||
suspend fun addBook(
|
||||
book: Book
|
||||
): Result<Unit>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Book's Story — free and open-source Material You eBook reader.
|
||||
* Copyright (C) 2024-2025 Acclorite
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
package ua.acclorite.book_story.domain.use_case.book
|
||||
|
||||
import ua.acclorite.book_story.core.log.logE
|
||||
import ua.acclorite.book_story.core.log.logI
|
||||
import ua.acclorite.book_story.domain.model.file.File
|
||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetFileFromBookUseCase @Inject constructor(
|
||||
private val bookRepository: BookRepository
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(bookId: Int): File? {
|
||||
logI("Getting file from book [$bookId].")
|
||||
|
||||
return bookRepository.getFileFromBook(bookId).fold(
|
||||
onSuccess = {
|
||||
logI("Successfully got file from book [$bookId].")
|
||||
it
|
||||
},
|
||||
onFailure = {
|
||||
logE("Could not get file from book [$bookId] with error: ${it.message}")
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import ua.acclorite.book_story.R
|
|||
import ua.acclorite.book_story.domain.use_case.book.CanResetCoverImageUseCase
|
||||
import ua.acclorite.book_story.domain.use_case.book.DeleteBookUseCase
|
||||
import ua.acclorite.book_story.domain.use_case.book.GetBookUseCase
|
||||
import ua.acclorite.book_story.domain.use_case.book.GetFileFromBookUseCase
|
||||
import ua.acclorite.book_story.domain.use_case.book.ResetCoverImageUseCase
|
||||
import ua.acclorite.book_story.domain.use_case.book.UpdateBookUseCase
|
||||
import ua.acclorite.book_story.domain.use_case.book.UpdateCoverImageUseCase
|
||||
|
|
@ -38,6 +39,7 @@ class BookInfoModel @Inject constructor(
|
|||
private val updateCoverImageUseCase: UpdateCoverImageUseCase,
|
||||
private val updateBookUseCase: UpdateBookUseCase,
|
||||
private val getBookUseCase: GetBookUseCase,
|
||||
private val getFileFromBookUseCase: GetFileFromBookUseCase,
|
||||
private val deleteBookUseCase: DeleteBookUseCase,
|
||||
private val canResetCoverImageUseCase: CanResetCoverImageUseCase,
|
||||
private val resetCoverImageUseCase: ResetCoverImageUseCase
|
||||
|
|
@ -302,6 +304,13 @@ class BookInfoModel @Inject constructor(
|
|||
event.context.getString(R.string.path_changed)
|
||||
.showToast(context = event.context)
|
||||
}
|
||||
|
||||
val file = getFileFromBookUseCase(_state.value.book.id)
|
||||
_state.update {
|
||||
it.copy(
|
||||
file = file
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -408,6 +417,13 @@ class BookInfoModel @Inject constructor(
|
|||
onEvent(BookInfoEvent.OnShowPathDialog)
|
||||
}
|
||||
onEvent(BookInfoEvent.OnCheckCoverReset)
|
||||
|
||||
val file = getFileFromBookUseCase(bookId)
|
||||
_state.update {
|
||||
it.copy(
|
||||
file = file
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ data class BookInfoScreen(val bookId: Int) : Screen, Parcelable {
|
|||
if (state.value.book.id == bookId) {
|
||||
BookInfoContent(
|
||||
book = state.value.book,
|
||||
file = state.value.file,
|
||||
categories = settingsState.value.categories,
|
||||
bottomSheet = state.value.bottomSheet,
|
||||
dialog = state.value.dialog,
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ package ua.acclorite.book_story.presentation.book_info
|
|||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.core.BottomSheet
|
||||
import ua.acclorite.book_story.core.Dialog
|
||||
import ua.acclorite.book_story.domain.model.file.File
|
||||
import ua.acclorite.book_story.domain.model.library.Book
|
||||
|
||||
@Immutable
|
||||
data class BookInfoState(
|
||||
val book: Book = Book.default,
|
||||
val file: File? = null,
|
||||
|
||||
val canResetCover: Boolean = false,
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ package ua.acclorite.book_story.ui.book_info
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import ua.acclorite.book_story.core.BottomSheet
|
||||
import ua.acclorite.book_story.domain.model.file.File
|
||||
import ua.acclorite.book_story.domain.model.library.Book
|
||||
import ua.acclorite.book_story.presentation.book_info.BookInfoEvent
|
||||
import ua.acclorite.book_story.presentation.book_info.BookInfoScreen
|
||||
|
|
@ -16,6 +17,7 @@ import ua.acclorite.book_story.presentation.book_info.BookInfoScreen
|
|||
fun BookInfoBottomSheet(
|
||||
bottomSheet: BottomSheet?,
|
||||
book: Book,
|
||||
file: File?,
|
||||
canResetCover: Boolean,
|
||||
showPathDialog: (BookInfoEvent.OnShowPathDialog) -> Unit,
|
||||
changeCover: (BookInfoEvent.OnChangeCover) -> Unit,
|
||||
|
|
@ -40,6 +42,7 @@ fun BookInfoBottomSheet(
|
|||
BookInfoScreen.DETAILS_BOTTOM_SHEET -> {
|
||||
BookInfoDetailsBottomSheet(
|
||||
book = book,
|
||||
file = file,
|
||||
showPathDialog = showPathDialog,
|
||||
dismissBottomSheet = dismissBottomSheet
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.lazy.LazyListState
|
|||
import androidx.compose.runtime.Composable
|
||||
import ua.acclorite.book_story.core.BottomSheet
|
||||
import ua.acclorite.book_story.core.Dialog
|
||||
import ua.acclorite.book_story.domain.model.file.File
|
||||
import ua.acclorite.book_story.domain.model.library.Book
|
||||
import ua.acclorite.book_story.domain.model.library.Category
|
||||
import ua.acclorite.book_story.presentation.book_info.BookInfoEvent
|
||||
|
|
@ -17,6 +18,7 @@ import ua.acclorite.book_story.presentation.book_info.BookInfoEvent
|
|||
@Composable
|
||||
fun BookInfoContent(
|
||||
book: Book,
|
||||
file: File?,
|
||||
categories: List<Category>,
|
||||
bottomSheet: BottomSheet?,
|
||||
dialog: Dialog?,
|
||||
|
|
@ -64,6 +66,7 @@ fun BookInfoContent(
|
|||
BookInfoBottomSheet(
|
||||
bottomSheet = bottomSheet,
|
||||
book = book,
|
||||
file = file,
|
||||
showPathDialog = showPathDialog,
|
||||
canResetCover = canResetCover,
|
||||
changeCover = changeCover,
|
||||
|
|
|
|||
|
|
@ -14,12 +14,11 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.core.data.ExtensionsData
|
||||
import ua.acclorite.book_story.data.model.file.CachedFileCompat
|
||||
import ua.acclorite.book_story.domain.model.file.File
|
||||
import ua.acclorite.book_story.domain.model.library.Book
|
||||
import ua.acclorite.book_story.presentation.book_info.BookInfoEvent
|
||||
import ua.acclorite.book_story.ui.common.components.common.LazyColumnWithScrollbar
|
||||
|
|
@ -32,20 +31,16 @@ import java.util.Locale
|
|||
@Composable
|
||||
fun BookInfoDetailsBottomSheet(
|
||||
book: Book,
|
||||
file: File?,
|
||||
showPathDialog: (BookInfoEvent.OnShowPathDialog) -> Unit,
|
||||
dismissBottomSheet: (BookInfoEvent.OnDismissBottomSheet) -> Unit
|
||||
) {
|
||||
val pattern = remember { SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault()) }
|
||||
val lastOpened = remember(book.lastOpened) { pattern.format(Date(book.lastOpened ?: 0)) }
|
||||
|
||||
val context = LocalContext.current
|
||||
val cachedFile = remember(book.filePath) {
|
||||
CachedFileCompat.fromFullPath(context, book.filePath)
|
||||
}
|
||||
|
||||
val fileSize = remember(cachedFile) {
|
||||
if (cachedFile != null && cachedFile.canAccess()) {
|
||||
val sizeBytes = cachedFile.size
|
||||
val fileSize = remember(file) {
|
||||
if (file != null) {
|
||||
val sizeBytes = file.size
|
||||
val sizeKB = sizeBytes / 1024f
|
||||
val sizeMB = sizeKB / 1024f
|
||||
when {
|
||||
|
|
@ -53,17 +48,25 @@ fun BookInfoDetailsBottomSheet(
|
|||
sizeKB > 0f -> "%.2f KB".format(sizeKB)
|
||||
else -> ""
|
||||
}
|
||||
} else {
|
||||
""
|
||||
}
|
||||
} else ""
|
||||
}
|
||||
|
||||
val fileExists = remember(cachedFile) {
|
||||
cachedFile.let {
|
||||
it != null && it.canAccess() && !it.isDirectory && ExtensionsData.fileExtensions
|
||||
.any { ext ->
|
||||
it.name.endsWith(ext, ignoreCase = true)
|
||||
val fileExists = remember(file) {
|
||||
file.let { file ->
|
||||
if (file == null) return@let false
|
||||
if (file.isDirectory) return@let false
|
||||
if (
|
||||
ExtensionsData.fileExtensions.none { ext ->
|
||||
file.name.endsWith(
|
||||
ext,
|
||||
ignoreCase = true
|
||||
)
|
||||
}
|
||||
) {
|
||||
return@let false
|
||||
}
|
||||
|
||||
return@let true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ package ua.acclorite.book_story.ui.book_info
|
|||
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.filled.EditNote
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -24,6 +24,7 @@ import androidx.compose.ui.unit.dp
|
|||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.ui.common.components.common.IconButton
|
||||
import ua.acclorite.book_story.ui.common.components.common.StyledText
|
||||
import ua.acclorite.book_story.ui.theme.ExpandingTransition
|
||||
|
||||
@Composable
|
||||
fun BookInfoDetailsBottomSheetItem(
|
||||
|
|
@ -34,10 +35,13 @@ fun BookInfoDetailsBottomSheetItem(
|
|||
errorMessage: String? = null,
|
||||
onEdit: () -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
|
@ -49,16 +53,6 @@ fun BookInfoDetailsBottomSheetItem(
|
|||
onValueChange = {},
|
||||
readOnly = true,
|
||||
isError = showError,
|
||||
supportingText = if (!showError || errorMessage.isNullOrBlank()) null else {
|
||||
{
|
||||
StyledText(
|
||||
text = errorMessage,
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
label = {
|
||||
StyledText(label)
|
||||
}
|
||||
|
|
@ -76,4 +70,17 @@ fun BookInfoDetailsBottomSheetItem(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExpandingTransition(showError && !errorMessage.isNullOrBlank()) {
|
||||
StyledText(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 4.dp),
|
||||
text = errorMessage!!,
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue