fix: cannot open books

Resolves: #221
This commit is contained in:
Acclorite 2025-06-09 12:58:54 +03:00
parent dd7998c600
commit 57a7225545
No known key found for this signature in database
GPG key ID: 6E54C611F6EE8593
12 changed files with 148 additions and 121 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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