refactor: cover parser
This commit is contained in:
parent
c76f7f38d5
commit
9e17d86588
33 changed files with 477 additions and 305 deletions
|
|
@ -22,6 +22,8 @@ import ua.acclorite.book_story.data.mapper.file.FileMapper
|
||||||
import ua.acclorite.book_story.data.mapper.file.FileMapperImpl
|
import ua.acclorite.book_story.data.mapper.file.FileMapperImpl
|
||||||
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
|
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
|
||||||
import ua.acclorite.book_story.data.mapper.history.HistoryMapperImpl
|
import ua.acclorite.book_story.data.mapper.history.HistoryMapperImpl
|
||||||
|
import ua.acclorite.book_story.data.parser.cover.CoverParser
|
||||||
|
import ua.acclorite.book_story.data.parser.cover.CoverParserImpl
|
||||||
import ua.acclorite.book_story.data.parser.file.FileParser
|
import ua.acclorite.book_story.data.parser.file.FileParser
|
||||||
import ua.acclorite.book_story.data.parser.file.FileParserImpl
|
import ua.acclorite.book_story.data.parser.file.FileParserImpl
|
||||||
import ua.acclorite.book_story.data.parser.text.TextParser
|
import ua.acclorite.book_story.data.parser.text.TextParser
|
||||||
|
|
@ -121,6 +123,12 @@ abstract class RepositoryModule {
|
||||||
fileParserImpl: FileParserImpl
|
fileParserImpl: FileParserImpl
|
||||||
): FileParser
|
): FileParser
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindCoverParser(
|
||||||
|
coverParserImpl: CoverParserImpl
|
||||||
|
): CoverParser
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun bindTextParser(
|
abstract fun bindTextParser(
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.data.model.common
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import ua.acclorite.book_story.core.CoverImage
|
|
||||||
import ua.acclorite.book_story.domain.model.library.Book
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class BookWithCover(
|
|
||||||
val book: Book,
|
|
||||||
val coverImage: CoverImage?
|
|
||||||
)
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.data.model.common
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import ua.acclorite.book_story.core.ui.UIText
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
sealed class NullableBook(
|
|
||||||
val bookWithCover: BookWithCover?,
|
|
||||||
val fileName: String?,
|
|
||||||
val message: UIText?
|
|
||||||
) {
|
|
||||||
class NotNull(
|
|
||||||
bookWithCover: BookWithCover
|
|
||||||
) : NullableBook(
|
|
||||||
bookWithCover = bookWithCover,
|
|
||||||
fileName = null,
|
|
||||||
message = null
|
|
||||||
)
|
|
||||||
|
|
||||||
class Null(
|
|
||||||
fileName: String,
|
|
||||||
message: UIText?
|
|
||||||
) : NullableBook(
|
|
||||||
bookWithCover = null,
|
|
||||||
fileName = fileName,
|
|
||||||
message = message
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* 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.data.parser.cover
|
||||||
|
|
||||||
|
import ua.acclorite.book_story.core.CoverImage
|
||||||
|
import ua.acclorite.book_story.data.model.file.CachedFile
|
||||||
|
|
||||||
|
interface CoverParser {
|
||||||
|
suspend fun parse(cachedFile: CachedFile): CoverImage?
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* 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.data.parser.cover
|
||||||
|
|
||||||
|
import ua.acclorite.book_story.core.CoverImage
|
||||||
|
import ua.acclorite.book_story.core.log.logE
|
||||||
|
import ua.acclorite.book_story.data.model.file.CachedFile
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class CoverParserImpl @Inject constructor(
|
||||||
|
private val epubCoverParser: EpubCoverParser,
|
||||||
|
private val txtCoverParser: TxtCoverParser,
|
||||||
|
private val pdfCoverParser: PdfCoverParser,
|
||||||
|
private val fb2CoverParser: Fb2CoverParser,
|
||||||
|
private val htmlCoverParser: HtmlCoverParser
|
||||||
|
) : CoverParser {
|
||||||
|
|
||||||
|
override suspend fun parse(cachedFile: CachedFile): CoverImage? {
|
||||||
|
if (!cachedFile.canAccess()) {
|
||||||
|
logE("File does not exist or no read access is granted.")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileFormat = ".${cachedFile.name.substringAfterLast(".")}".lowercase().trim()
|
||||||
|
return when (fileFormat) {
|
||||||
|
".pdf" -> {
|
||||||
|
pdfCoverParser.parse(cachedFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
".epub" -> {
|
||||||
|
epubCoverParser.parse(cachedFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
".txt" -> {
|
||||||
|
txtCoverParser.parse(cachedFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
".fb2" -> {
|
||||||
|
fb2CoverParser.parse(cachedFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
".html" -> {
|
||||||
|
htmlCoverParser.parse(cachedFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
".htm" -> {
|
||||||
|
htmlCoverParser.parse(cachedFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
".md" -> {
|
||||||
|
txtCoverParser.parse(cachedFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
logE("Wrong file format, could not find supported extension.")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* 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.data.parser.cover
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.parser.Parser
|
||||||
|
import ua.acclorite.book_story.core.CoverImage
|
||||||
|
import ua.acclorite.book_story.core.log.logE
|
||||||
|
import ua.acclorite.book_story.data.model.file.CachedFile
|
||||||
|
import java.net.URLDecoder
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class EpubCoverParser @Inject constructor() : CoverParser {
|
||||||
|
|
||||||
|
override suspend fun parse(cachedFile: CachedFile): CoverImage? {
|
||||||
|
return try {
|
||||||
|
var coverImage: CoverImage? = null
|
||||||
|
|
||||||
|
val rawFile = cachedFile.rawFile
|
||||||
|
if (rawFile == null || !rawFile.exists() || !rawFile.canRead()) return null
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
ZipFile(rawFile).use { zip ->
|
||||||
|
val opfEntry = zip.entries().asSequence().find { entry ->
|
||||||
|
entry.name.endsWith(".opf", ignoreCase = true)
|
||||||
|
} ?: return@withContext
|
||||||
|
|
||||||
|
val opfContent = zip
|
||||||
|
.getInputStream(opfEntry)
|
||||||
|
.bufferedReader()
|
||||||
|
.use { it.readText() }
|
||||||
|
val document = Jsoup.parse(opfContent, Parser.xmlParser())
|
||||||
|
|
||||||
|
val coverImagePath = document
|
||||||
|
.select("metadata > meta[name=cover]")
|
||||||
|
.attr("content")
|
||||||
|
.let { coverId ->
|
||||||
|
if (coverId.isNotBlank()) {
|
||||||
|
document
|
||||||
|
.select("manifest > item[id=$coverId]")
|
||||||
|
.attr("href")
|
||||||
|
.also { src ->
|
||||||
|
if (src.isBlank()) return@also
|
||||||
|
return@let src
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document
|
||||||
|
.select("manifest > item[media-type*=image]")
|
||||||
|
.firstOrNull()
|
||||||
|
?.attr("href")
|
||||||
|
?.also { src -> if (src.isBlank()) return@let null }
|
||||||
|
}
|
||||||
|
?.let { src -> URLDecoder.decode(src, StandardCharsets.UTF_8.name()) }
|
||||||
|
?: return@use
|
||||||
|
|
||||||
|
zip.entries().asSequence().first { entry ->
|
||||||
|
if (entry.name.endsWith(coverImagePath)) {
|
||||||
|
val imageBytes = zip.getInputStream(entry).readBytes()
|
||||||
|
coverImage = BitmapFactory.decodeByteArray(
|
||||||
|
imageBytes, 0, imageBytes.size
|
||||||
|
)
|
||||||
|
return@first true
|
||||||
|
} else return@first false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
coverImage
|
||||||
|
} catch (_: Exception) {
|
||||||
|
logE("Could not parse cover image.")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* 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.data.parser.cover
|
||||||
|
|
||||||
|
import ua.acclorite.book_story.core.CoverImage
|
||||||
|
import ua.acclorite.book_story.data.model.file.CachedFile
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class Fb2CoverParser @Inject constructor() : CoverParser {
|
||||||
|
|
||||||
|
override suspend fun parse(cachedFile: CachedFile): CoverImage? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* 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.data.parser.cover
|
||||||
|
|
||||||
|
import ua.acclorite.book_story.core.CoverImage
|
||||||
|
import ua.acclorite.book_story.data.model.file.CachedFile
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class HtmlCoverParser @Inject constructor() : CoverParser {
|
||||||
|
|
||||||
|
override suspend fun parse(cachedFile: CachedFile): CoverImage? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* 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.data.parser.cover
|
||||||
|
|
||||||
|
import ua.acclorite.book_story.core.CoverImage
|
||||||
|
import ua.acclorite.book_story.data.model.file.CachedFile
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class PdfCoverParser @Inject constructor() : CoverParser {
|
||||||
|
|
||||||
|
override suspend fun parse(cachedFile: CachedFile): CoverImage? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* 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.data.parser.cover
|
||||||
|
|
||||||
|
import ua.acclorite.book_story.core.CoverImage
|
||||||
|
import ua.acclorite.book_story.data.model.file.CachedFile
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class TxtCoverParser @Inject constructor() : CoverParser {
|
||||||
|
|
||||||
|
override suspend fun parse(cachedFile: CachedFile): CoverImage? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,28 +6,22 @@
|
||||||
|
|
||||||
package ua.acclorite.book_story.data.parser.file
|
package ua.acclorite.book_story.data.parser.file
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.parser.Parser
|
import org.jsoup.parser.Parser
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.core.ui.UIText
|
import ua.acclorite.book_story.core.ui.UIText
|
||||||
import ua.acclorite.book_story.data.model.common.BookWithCover
|
|
||||||
import ua.acclorite.book_story.data.model.file.CachedFile
|
import ua.acclorite.book_story.data.model.file.CachedFile
|
||||||
import ua.acclorite.book_story.domain.model.library.Book
|
import ua.acclorite.book_story.domain.model.library.Book
|
||||||
import java.io.File
|
|
||||||
import java.net.URLDecoder
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class EpubFileParser @Inject constructor() : FileParser {
|
class EpubFileParser @Inject constructor() : FileParser {
|
||||||
|
|
||||||
override suspend fun parse(cachedFile: CachedFile): BookWithCover? {
|
override suspend fun parse(cachedFile: CachedFile): Book? {
|
||||||
return try {
|
return try {
|
||||||
var book: BookWithCover? = null
|
var book: Book? = null
|
||||||
|
|
||||||
val rawFile = cachedFile.rawFile
|
val rawFile = cachedFile.rawFile
|
||||||
if (rawFile == null || !rawFile.exists() || !rawFile.canRead()) return null
|
if (rawFile == null || !rawFile.exists() || !rawFile.canRead()) return null
|
||||||
|
|
@ -66,42 +60,17 @@ class EpubFileParser @Inject constructor() : FileParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val coverImage = document
|
book = Book(
|
||||||
.select("metadata > meta[name=cover]")
|
title = title,
|
||||||
.attr("content")
|
author = author,
|
||||||
.let { coverId ->
|
description = description,
|
||||||
if (coverId.isNotBlank()) {
|
scrollIndex = 0,
|
||||||
document
|
scrollOffset = 0,
|
||||||
.select("manifest > item[id=$coverId]")
|
progress = 0f,
|
||||||
.attr("href")
|
filePath = cachedFile.path,
|
||||||
.also { src ->
|
lastOpened = null,
|
||||||
if (src.isBlank()) return@also
|
categories = emptyList(),
|
||||||
return@let src
|
coverImage = null
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document
|
|
||||||
.select("manifest > item[media-type*=image]")
|
|
||||||
.firstOrNull()
|
|
||||||
?.attr("href")
|
|
||||||
?.also { src -> if (src.isBlank()) return@let null }
|
|
||||||
}
|
|
||||||
?.let { src -> URLDecoder.decode(src, StandardCharsets.UTF_8.name()) }
|
|
||||||
|
|
||||||
book = BookWithCover(
|
|
||||||
book = Book(
|
|
||||||
title = title,
|
|
||||||
author = author,
|
|
||||||
description = description,
|
|
||||||
scrollIndex = 0,
|
|
||||||
scrollOffset = 0,
|
|
||||||
progress = 0f,
|
|
||||||
filePath = cachedFile.path,
|
|
||||||
lastOpened = null,
|
|
||||||
categories = emptyList(),
|
|
||||||
coverImage = null
|
|
||||||
),
|
|
||||||
coverImage = extractCoverImageBitmap(rawFile, coverImage)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,21 +80,4 @@ class EpubFileParser @Inject constructor() : FileParser {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractCoverImageBitmap(file: File, coverImagePath: String?): Bitmap? {
|
|
||||||
if (coverImagePath.isNullOrBlank()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
ZipFile(file).use { zip ->
|
|
||||||
zip.entries().asSequence().forEach { entry ->
|
|
||||||
if (entry.name.endsWith(coverImagePath)) {
|
|
||||||
val imageBytes = zip.getInputStream(entry).readBytes()
|
|
||||||
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -10,14 +10,13 @@ import org.jsoup.Jsoup
|
||||||
import org.jsoup.parser.Parser
|
import org.jsoup.parser.Parser
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.core.ui.UIText
|
import ua.acclorite.book_story.core.ui.UIText
|
||||||
import ua.acclorite.book_story.data.model.common.BookWithCover
|
|
||||||
import ua.acclorite.book_story.data.model.file.CachedFile
|
import ua.acclorite.book_story.data.model.file.CachedFile
|
||||||
import ua.acclorite.book_story.domain.model.library.Book
|
import ua.acclorite.book_story.domain.model.library.Book
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class Fb2FileParser @Inject constructor() : FileParser {
|
class Fb2FileParser @Inject constructor() : FileParser {
|
||||||
|
|
||||||
override suspend fun parse(cachedFile: CachedFile): BookWithCover? {
|
override suspend fun parse(cachedFile: CachedFile): Book? {
|
||||||
return try {
|
return try {
|
||||||
val document = cachedFile.openInputStream()?.use {
|
val document = cachedFile.openInputStream()?.use {
|
||||||
Jsoup.parse(it, null, "", Parser.xmlParser())
|
Jsoup.parse(it, null, "", Parser.xmlParser())
|
||||||
|
|
@ -44,19 +43,16 @@ class Fb2FileParser @Inject constructor() : FileParser {
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
BookWithCover(
|
Book(
|
||||||
book = Book(
|
title = title,
|
||||||
title = title,
|
author = author,
|
||||||
author = author,
|
description = description,
|
||||||
description = description,
|
scrollIndex = 0,
|
||||||
scrollIndex = 0,
|
scrollOffset = 0,
|
||||||
scrollOffset = 0,
|
progress = 0f,
|
||||||
progress = 0f,
|
filePath = cachedFile.path,
|
||||||
filePath = cachedFile.path,
|
lastOpened = null,
|
||||||
lastOpened = null,
|
categories = emptyList(),
|
||||||
categories = emptyList(),
|
|
||||||
coverImage = null
|
|
||||||
),
|
|
||||||
coverImage = null
|
coverImage = null
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@
|
||||||
|
|
||||||
package ua.acclorite.book_story.data.parser.file
|
package ua.acclorite.book_story.data.parser.file
|
||||||
|
|
||||||
import ua.acclorite.book_story.data.model.common.BookWithCover
|
|
||||||
import ua.acclorite.book_story.data.model.file.CachedFile
|
import ua.acclorite.book_story.data.model.file.CachedFile
|
||||||
|
import ua.acclorite.book_story.domain.model.library.Book
|
||||||
|
|
||||||
interface FileParser {
|
interface FileParser {
|
||||||
suspend fun parse(cachedFile: CachedFile): BookWithCover?
|
suspend fun parse(cachedFile: CachedFile): Book?
|
||||||
}
|
}
|
||||||
|
|
@ -7,8 +7,8 @@
|
||||||
package ua.acclorite.book_story.data.parser.file
|
package ua.acclorite.book_story.data.parser.file
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import ua.acclorite.book_story.data.model.common.BookWithCover
|
|
||||||
import ua.acclorite.book_story.data.model.file.CachedFile
|
import ua.acclorite.book_story.data.model.file.CachedFile
|
||||||
|
import ua.acclorite.book_story.domain.model.library.Book
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val FILE_PARSER = "File Parser"
|
private const val FILE_PARSER = "File Parser"
|
||||||
|
|
@ -21,7 +21,7 @@ class FileParserImpl @Inject constructor(
|
||||||
private val htmlFileParser: HtmlFileParser,
|
private val htmlFileParser: HtmlFileParser,
|
||||||
) : FileParser {
|
) : FileParser {
|
||||||
|
|
||||||
override suspend fun parse(cachedFile: CachedFile): BookWithCover? {
|
override suspend fun parse(cachedFile: CachedFile): Book? {
|
||||||
if (!cachedFile.canAccess()) {
|
if (!cachedFile.canAccess()) {
|
||||||
Log.e(FILE_PARSER, "File does not exist or no read access is granted.")
|
Log.e(FILE_PARSER, "File does not exist or no read access is granted.")
|
||||||
return null
|
return null
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,13 @@ import org.jsoup.Jsoup
|
||||||
import org.jsoup.parser.Parser
|
import org.jsoup.parser.Parser
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.core.ui.UIText
|
import ua.acclorite.book_story.core.ui.UIText
|
||||||
import ua.acclorite.book_story.data.model.common.BookWithCover
|
|
||||||
import ua.acclorite.book_story.data.model.file.CachedFile
|
import ua.acclorite.book_story.data.model.file.CachedFile
|
||||||
import ua.acclorite.book_story.domain.model.library.Book
|
import ua.acclorite.book_story.domain.model.library.Book
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class HtmlFileParser @Inject constructor() : FileParser {
|
class HtmlFileParser @Inject constructor() : FileParser {
|
||||||
|
|
||||||
override suspend fun parse(cachedFile: CachedFile): BookWithCover? {
|
override suspend fun parse(cachedFile: CachedFile): Book? {
|
||||||
return try {
|
return try {
|
||||||
val document = cachedFile.openInputStream()?.use {
|
val document = cachedFile.openInputStream()?.use {
|
||||||
Jsoup.parse(it, null, "", Parser.htmlParser())
|
Jsoup.parse(it, null, "", Parser.htmlParser())
|
||||||
|
|
@ -30,19 +29,16 @@ class HtmlFileParser @Inject constructor() : FileParser {
|
||||||
return@run this
|
return@run this
|
||||||
}
|
}
|
||||||
|
|
||||||
BookWithCover(
|
Book(
|
||||||
book = Book(
|
title = title,
|
||||||
title = title,
|
author = UIText.StringResource(R.string.unknown_author),
|
||||||
author = UIText.StringResource(R.string.unknown_author),
|
description = null,
|
||||||
description = null,
|
scrollIndex = 0,
|
||||||
scrollIndex = 0,
|
scrollOffset = 0,
|
||||||
scrollOffset = 0,
|
progress = 0f,
|
||||||
progress = 0f,
|
filePath = cachedFile.path,
|
||||||
filePath = cachedFile.path,
|
lastOpened = null,
|
||||||
lastOpened = null,
|
categories = emptyList(),
|
||||||
categories = emptyList(),
|
|
||||||
coverImage = null
|
|
||||||
),
|
|
||||||
coverImage = null
|
coverImage = null
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import com.tom_roush.pdfbox.android.PDFBoxResourceLoader
|
||||||
import com.tom_roush.pdfbox.pdmodel.PDDocument
|
import com.tom_roush.pdfbox.pdmodel.PDDocument
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.core.ui.UIText
|
import ua.acclorite.book_story.core.ui.UIText
|
||||||
import ua.acclorite.book_story.data.model.common.BookWithCover
|
|
||||||
import ua.acclorite.book_story.data.model.file.CachedFile
|
import ua.acclorite.book_story.data.model.file.CachedFile
|
||||||
import ua.acclorite.book_story.domain.model.library.Book
|
import ua.acclorite.book_story.domain.model.library.Book
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -20,7 +19,7 @@ class PdfFileParser @Inject constructor(
|
||||||
private val application: Application
|
private val application: Application
|
||||||
) : FileParser {
|
) : FileParser {
|
||||||
|
|
||||||
override suspend fun parse(cachedFile: CachedFile): BookWithCover? {
|
override suspend fun parse(cachedFile: CachedFile): Book? {
|
||||||
return try {
|
return try {
|
||||||
PDFBoxResourceLoader.init(application)
|
PDFBoxResourceLoader.init(application)
|
||||||
val document = PDDocument.load(cachedFile.openInputStream())
|
val document = PDDocument.load(cachedFile.openInputStream())
|
||||||
|
|
@ -35,19 +34,16 @@ class PdfFileParser @Inject constructor(
|
||||||
|
|
||||||
document.close()
|
document.close()
|
||||||
|
|
||||||
BookWithCover(
|
Book(
|
||||||
book = Book(
|
title = title,
|
||||||
title = title,
|
author = author,
|
||||||
author = author,
|
description = description,
|
||||||
description = description,
|
scrollIndex = 0,
|
||||||
scrollIndex = 0,
|
scrollOffset = 0,
|
||||||
scrollOffset = 0,
|
progress = 0f,
|
||||||
progress = 0f,
|
filePath = cachedFile.path,
|
||||||
filePath = cachedFile.path,
|
lastOpened = null,
|
||||||
lastOpened = null,
|
categories = emptyList(),
|
||||||
categories = emptyList(),
|
|
||||||
coverImage = null
|
|
||||||
),
|
|
||||||
coverImage = null
|
coverImage = null
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
||||||
|
|
@ -8,31 +8,27 @@ package ua.acclorite.book_story.data.parser.file
|
||||||
|
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.core.ui.UIText
|
import ua.acclorite.book_story.core.ui.UIText
|
||||||
import ua.acclorite.book_story.data.model.common.BookWithCover
|
|
||||||
import ua.acclorite.book_story.data.model.file.CachedFile
|
import ua.acclorite.book_story.data.model.file.CachedFile
|
||||||
import ua.acclorite.book_story.domain.model.library.Book
|
import ua.acclorite.book_story.domain.model.library.Book
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TxtFileParser @Inject constructor() : FileParser {
|
class TxtFileParser @Inject constructor() : FileParser {
|
||||||
|
|
||||||
override suspend fun parse(cachedFile: CachedFile): BookWithCover? {
|
override suspend fun parse(cachedFile: CachedFile): Book? {
|
||||||
return try {
|
return try {
|
||||||
val title = cachedFile.name.substringBeforeLast(".").trim()
|
val title = cachedFile.name.substringBeforeLast(".").trim()
|
||||||
val author = UIText.StringResource(R.string.unknown_author)
|
val author = UIText.StringResource(R.string.unknown_author)
|
||||||
|
|
||||||
BookWithCover(
|
Book(
|
||||||
book = Book(
|
title = title,
|
||||||
title = title,
|
author = author,
|
||||||
author = author,
|
description = null,
|
||||||
description = null,
|
scrollIndex = 0,
|
||||||
scrollIndex = 0,
|
scrollOffset = 0,
|
||||||
scrollOffset = 0,
|
progress = 0f,
|
||||||
progress = 0f,
|
filePath = cachedFile.path,
|
||||||
filePath = cachedFile.path,
|
lastOpened = null,
|
||||||
lastOpened = null,
|
categories = emptyList(),
|
||||||
categories = emptyList(),
|
|
||||||
coverImage = null
|
|
||||||
),
|
|
||||||
coverImage = null
|
coverImage = null
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import ua.acclorite.book_story.core.CoverImage
|
||||||
import ua.acclorite.book_story.data.local.room.BookDatabase
|
import ua.acclorite.book_story.data.local.room.BookDatabase
|
||||||
import ua.acclorite.book_story.data.mapper.book.BookMapper
|
import ua.acclorite.book_story.data.mapper.book.BookMapper
|
||||||
import ua.acclorite.book_story.data.mapper.file.FileMapper
|
import ua.acclorite.book_story.data.mapper.file.FileMapper
|
||||||
import ua.acclorite.book_story.data.parser.file.FileParser
|
import ua.acclorite.book_story.data.parser.cover.CoverParser
|
||||||
import ua.acclorite.book_story.data.parser.text.TextParser
|
import ua.acclorite.book_story.data.parser.text.TextParser
|
||||||
import ua.acclorite.book_story.domain.model.file.File
|
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.Book
|
||||||
|
|
@ -27,7 +27,7 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
private val database: BookDatabase,
|
private val database: BookDatabase,
|
||||||
private val bookMapper: BookMapper,
|
private val bookMapper: BookMapper,
|
||||||
private val fileMapper: FileMapper,
|
private val fileMapper: FileMapper,
|
||||||
private val fileParser: FileParser,
|
private val coverParser: CoverParser,
|
||||||
private val textParser: TextParser,
|
private val textParser: TextParser,
|
||||||
private val fileProvider: FileProvider
|
private val fileProvider: FileProvider
|
||||||
) : BookRepository {
|
) : BookRepository {
|
||||||
|
|
@ -87,8 +87,9 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
override suspend fun getDefaultCover(book: Book): Result<CoverImage?> = runCatching {
|
override suspend fun getDefaultCover(book: Book): Result<CoverImage?> = runCatching {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
fileProvider.getFileFromBook(book)
|
fileProvider.getFileFromBook(book).mapCatching {
|
||||||
.mapCatching { fileParser.parse(it)?.coverImage }
|
coverParser.parse(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,13 +8,15 @@ package ua.acclorite.book_story.data.repository
|
||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import ua.acclorite.book_story.core.CoverImage
|
||||||
import ua.acclorite.book_story.core.data.ExtensionsData
|
import ua.acclorite.book_story.core.data.ExtensionsData
|
||||||
import ua.acclorite.book_story.data.local.room.BookDatabase
|
import ua.acclorite.book_story.data.local.room.BookDatabase
|
||||||
import ua.acclorite.book_story.data.mapper.file.FileMapper
|
import ua.acclorite.book_story.data.mapper.file.FileMapper
|
||||||
import ua.acclorite.book_story.data.model.common.BookWithCover
|
|
||||||
import ua.acclorite.book_story.data.model.file.CachedFile
|
import ua.acclorite.book_story.data.model.file.CachedFile
|
||||||
|
import ua.acclorite.book_story.data.parser.cover.CoverParser
|
||||||
import ua.acclorite.book_story.data.parser.file.FileParser
|
import ua.acclorite.book_story.data.parser.file.FileParser
|
||||||
import ua.acclorite.book_story.domain.model.file.File
|
import ua.acclorite.book_story.domain.model.file.File
|
||||||
|
import ua.acclorite.book_story.domain.model.library.Book
|
||||||
import ua.acclorite.book_story.domain.repository.FileSystemRepository
|
import ua.acclorite.book_story.domain.repository.FileSystemRepository
|
||||||
import ua.acclorite.book_story.domain.service.FileProvider
|
import ua.acclorite.book_story.domain.service.FileProvider
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -25,6 +27,7 @@ class FileSystemRepositoryImpl @Inject constructor(
|
||||||
private val database: BookDatabase,
|
private val database: BookDatabase,
|
||||||
private val fileMapper: FileMapper,
|
private val fileMapper: FileMapper,
|
||||||
private val fileParser: FileParser,
|
private val fileParser: FileParser,
|
||||||
|
private val coverParser: CoverParser,
|
||||||
private val fileProvider: FileProvider
|
private val fileProvider: FileProvider
|
||||||
) : FileSystemRepository {
|
) : FileSystemRepository {
|
||||||
|
|
||||||
|
|
@ -73,10 +76,17 @@ class FileSystemRepositoryImpl @Inject constructor(
|
||||||
return files
|
return files
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getBookFromFile(file: File): Result<BookWithCover> = runCatching {
|
override suspend fun getBookFromFile(file: File): Result<Pair<Book, CoverImage?>> =
|
||||||
withContext(Dispatchers.IO) {
|
runCatching {
|
||||||
fileParser.parse(fileMapper.toCachedFile(file))
|
withContext(Dispatchers.IO) {
|
||||||
?: throw Exception("Could not parse ${file.name}.")
|
val cachedFile = fileMapper.toCachedFile(file)
|
||||||
|
|
||||||
|
val book = fileParser.parse(
|
||||||
|
cachedFile = cachedFile
|
||||||
|
) ?: throw Exception("Could not parse ${file.name}.")
|
||||||
|
val coverImage = coverParser.parse(cachedFile = cachedFile)
|
||||||
|
|
||||||
|
return@withContext book to coverImage
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -6,8 +6,9 @@
|
||||||
|
|
||||||
package ua.acclorite.book_story.domain.repository
|
package ua.acclorite.book_story.domain.repository
|
||||||
|
|
||||||
import ua.acclorite.book_story.data.model.common.BookWithCover
|
import ua.acclorite.book_story.core.CoverImage
|
||||||
import ua.acclorite.book_story.domain.model.file.File
|
import ua.acclorite.book_story.domain.model.file.File
|
||||||
|
import ua.acclorite.book_story.domain.model.library.Book
|
||||||
|
|
||||||
interface FileSystemRepository {
|
interface FileSystemRepository {
|
||||||
suspend fun searchFiles(
|
suspend fun searchFiles(
|
||||||
|
|
@ -16,5 +17,5 @@ interface FileSystemRepository {
|
||||||
|
|
||||||
suspend fun getBookFromFile(
|
suspend fun getBookFromFile(
|
||||||
file: File
|
file: File
|
||||||
): Result<BookWithCover>
|
): Result<Pair<Book, CoverImage?>>
|
||||||
}
|
}
|
||||||
|
|
@ -24,8 +24,8 @@ class AddBookUseCase @Inject constructor(
|
||||||
suspend operator fun invoke(book: Book, coverImage: CoverImage?) {
|
suspend operator fun invoke(book: Book, coverImage: CoverImage?) {
|
||||||
logI("Inserting [${book.title}].")
|
logI("Inserting [${book.title}].")
|
||||||
|
|
||||||
val coverImageUri = coverImage?.let {
|
val coverImageUri = coverImage?.let { coverImage ->
|
||||||
coverImageHandler.saveCover(it).fold(
|
coverImageHandler.saveCover(coverImage).fold(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
logI("Successfully saved cover image of [${book.title}].")
|
logI("Successfully saved cover image of [${book.title}].")
|
||||||
it.toUri()
|
it.toUri()
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,7 @@ class CanResetCoverImageUseCase @Inject constructor(
|
||||||
bookRepository.getBook(bookId).mapCatching { book ->
|
bookRepository.getBook(bookId).mapCatching { book ->
|
||||||
// Getting default cover image
|
// Getting default cover image
|
||||||
val defaultCoverImage = bookRepository.getDefaultCover(book).getOrThrow()
|
val defaultCoverImage = bookRepository.getDefaultCover(book).getOrThrow()
|
||||||
if (defaultCoverImage == null) {
|
?: return@mapCatching false
|
||||||
return@mapCatching false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return true if current cover is null (and default is not)
|
// Return true if current cover is null (and default is not)
|
||||||
if (book.coverImage == null) {
|
if (book.coverImage == null) {
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,7 @@ class ResetCoverImageUseCase @Inject constructor(
|
||||||
bookRepository.getBook(bookId).mapCatching { book ->
|
bookRepository.getBook(bookId).mapCatching { book ->
|
||||||
// Getting default cover image
|
// Getting default cover image
|
||||||
val defaultCoverImage = bookRepository.getDefaultCover(book).getOrThrow()
|
val defaultCoverImage = bookRepository.getDefaultCover(book).getOrThrow()
|
||||||
if (defaultCoverImage == null) {
|
?: throw NoSuchElementException("Could not find default cover image")
|
||||||
throw NoSuchElementException("Could not find default cover image")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deleting old cover
|
// Deleting old cover
|
||||||
book.coverImage?.let { coverImageHandler.deleteCover(it) }?.onFailure {
|
book.coverImage?.let { coverImageHandler.deleteCover(it) }?.onFailure {
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,11 @@
|
||||||
|
|
||||||
package ua.acclorite.book_story.domain.use_case.file_system
|
package ua.acclorite.book_story.domain.use_case.file_system
|
||||||
|
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.core.CoverImage
|
||||||
import ua.acclorite.book_story.core.log.logE
|
import ua.acclorite.book_story.core.log.logE
|
||||||
import ua.acclorite.book_story.core.log.logI
|
import ua.acclorite.book_story.core.log.logI
|
||||||
import ua.acclorite.book_story.core.ui.UIText
|
|
||||||
import ua.acclorite.book_story.data.model.common.NullableBook
|
|
||||||
import ua.acclorite.book_story.data.model.common.NullableBook.NotNull
|
|
||||||
import ua.acclorite.book_story.data.model.common.NullableBook.Null
|
|
||||||
import ua.acclorite.book_story.domain.model.file.File
|
import ua.acclorite.book_story.domain.model.file.File
|
||||||
|
import ua.acclorite.book_story.domain.model.library.Book
|
||||||
import ua.acclorite.book_story.domain.repository.FileSystemRepository
|
import ua.acclorite.book_story.domain.repository.FileSystemRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -21,20 +18,17 @@ class GetBookFromFileUseCase @Inject constructor(
|
||||||
private val fileSystemRepository: FileSystemRepository
|
private val fileSystemRepository: FileSystemRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend operator fun invoke(file: File): NullableBook {
|
suspend operator fun invoke(file: File): Pair<Book, CoverImage?>? {
|
||||||
logI("Getting book from [${file.name}] file.")
|
logI("Getting book from [${file.name}] file.")
|
||||||
|
|
||||||
return fileSystemRepository.getBookFromFile(file).fold(
|
return fileSystemRepository.getBookFromFile(file).fold(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
logI("Successfully got [${it.book.title}] book from file.")
|
logI("Successfully got [${it.first.title}] book from file.")
|
||||||
NotNull(it)
|
it
|
||||||
},
|
},
|
||||||
onFailure = {
|
onFailure = {
|
||||||
logE("Could not get book from file with error: ${it.message}")
|
logE("Could not get book from file with error: ${it.message}")
|
||||||
Null(
|
null
|
||||||
file.name,
|
|
||||||
UIText.StringResource(R.string.error_something_went_wrong)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ package ua.acclorite.book_story.presentation.browse
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import ua.acclorite.book_story.presentation.browse.model.SelectableFile
|
import ua.acclorite.book_story.presentation.browse.model.SelectableFile
|
||||||
import ua.acclorite.book_story.presentation.library.model.SelectableNullableBook
|
import ua.acclorite.book_story.presentation.browse.model.SelectableNullableBook
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed class BrowseEvent {
|
sealed class BrowseEvent {
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,16 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import ua.acclorite.book_story.data.model.common.NullableBook
|
import ua.acclorite.book_story.R
|
||||||
|
import ua.acclorite.book_story.core.ui.UIText
|
||||||
import ua.acclorite.book_story.domain.model.file.File
|
import ua.acclorite.book_story.domain.model.file.File
|
||||||
import ua.acclorite.book_story.domain.use_case.book.AddBookUseCase
|
import ua.acclorite.book_story.domain.use_case.book.AddBookUseCase
|
||||||
import ua.acclorite.book_story.domain.use_case.file_system.GetBookFromFileUseCase
|
import ua.acclorite.book_story.domain.use_case.file_system.GetBookFromFileUseCase
|
||||||
import ua.acclorite.book_story.domain.use_case.file_system.GetFilesUseCase
|
import ua.acclorite.book_story.domain.use_case.file_system.GetFilesUseCase
|
||||||
|
import ua.acclorite.book_story.presentation.browse.model.NullableBook
|
||||||
import ua.acclorite.book_story.presentation.browse.model.SelectableFile
|
import ua.acclorite.book_story.presentation.browse.model.SelectableFile
|
||||||
|
import ua.acclorite.book_story.presentation.browse.model.SelectableNullableBook
|
||||||
import ua.acclorite.book_story.presentation.library.LibraryScreen
|
import ua.acclorite.book_story.presentation.library.LibraryScreen
|
||||||
import ua.acclorite.book_story.presentation.library.model.SelectableNullableBook
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.coroutines.coroutineContext
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
|
|
@ -248,17 +250,29 @@ class BrowseModel @Inject constructor(
|
||||||
val books = _state.value.files.mapNotNull { file ->
|
val books = _state.value.files.mapNotNull { file ->
|
||||||
ensureActive()
|
ensureActive()
|
||||||
if (!file.selected) return@mapNotNull null
|
if (!file.selected) return@mapNotNull null
|
||||||
SelectableNullableBook(
|
|
||||||
data = getBookFromFileUseCase(
|
val nullableBook = getBookFromFileUseCase(
|
||||||
File(
|
File(
|
||||||
name = file.name,
|
name = file.name,
|
||||||
uri = file.uri.toString(),
|
uri = file.uri.toString(),
|
||||||
path = file.path,
|
path = file.path,
|
||||||
size = file.size,
|
size = file.size,
|
||||||
lastModified = file.lastModified,
|
lastModified = file.lastModified,
|
||||||
isDirectory = file.isDirectory
|
isDirectory = file.isDirectory
|
||||||
|
)
|
||||||
|
).let {
|
||||||
|
if (it != null) {
|
||||||
|
NullableBook.NotNull(it.first, it.second)
|
||||||
|
} else {
|
||||||
|
NullableBook.Null(
|
||||||
|
fileName = file.name,
|
||||||
|
UIText.StringResource(resId = R.string.error_something_went_wrong)
|
||||||
)
|
)
|
||||||
),
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectableNullableBook(
|
||||||
|
data = nullableBook,
|
||||||
selected = true
|
selected = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -294,12 +308,12 @@ class BrowseModel @Inject constructor(
|
||||||
is BrowseEvent.OnActionAddDialog -> {
|
is BrowseEvent.OnActionAddDialog -> {
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
_state.value.selectedBooksAddDialog.mapNotNull {
|
_state.value.selectedBooksAddDialog.mapNotNull {
|
||||||
if (it.data is NullableBook.NotNull && it.selected) return@mapNotNull it.data.bookWithCover
|
if (it.data is NullableBook.NotNull && it.selected) return@mapNotNull it.data
|
||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
}.ifEmpty { return@withContext }.forEach { bookWithCover ->
|
}.ifEmpty { return@withContext }.forEach { nullableBook ->
|
||||||
addBookUseCase(
|
addBookUseCase(
|
||||||
bookWithCover.book,
|
nullableBook.book,
|
||||||
bookWithCover.coverImage
|
nullableBook.coverImage
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import androidx.compose.runtime.Immutable
|
||||||
import ua.acclorite.book_story.core.BottomSheet
|
import ua.acclorite.book_story.core.BottomSheet
|
||||||
import ua.acclorite.book_story.core.Dialog
|
import ua.acclorite.book_story.core.Dialog
|
||||||
import ua.acclorite.book_story.presentation.browse.model.SelectableFile
|
import ua.acclorite.book_story.presentation.browse.model.SelectableFile
|
||||||
import ua.acclorite.book_story.presentation.library.model.SelectableNullableBook
|
import ua.acclorite.book_story.presentation.browse.model.SelectableNullableBook
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class BrowseState(
|
data class BrowseState(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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.presentation.browse.model
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import ua.acclorite.book_story.core.CoverImage
|
||||||
|
import ua.acclorite.book_story.core.ui.UIText
|
||||||
|
import ua.acclorite.book_story.domain.model.library.Book
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed class NullableBook {
|
||||||
|
data class NotNull(
|
||||||
|
val book: Book,
|
||||||
|
val coverImage: CoverImage?
|
||||||
|
) : NullableBook()
|
||||||
|
|
||||||
|
data class Null(
|
||||||
|
val fileName: String,
|
||||||
|
val message: UIText?
|
||||||
|
) : NullableBook()
|
||||||
|
}
|
||||||
|
|
@ -4,10 +4,9 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-only
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package ua.acclorite.book_story.presentation.library.model
|
package ua.acclorite.book_story.presentation.browse.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import ua.acclorite.book_story.data.model.common.NullableBook
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class SelectableNullableBook(
|
data class SelectableNullableBook(
|
||||||
|
|
@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.lazy.items
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.AddChart
|
import androidx.compose.material.icons.filled.AddChart
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -20,13 +20,12 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.data.model.common.NullableBook
|
|
||||||
import ua.acclorite.book_story.presentation.browse.BrowseEvent
|
import ua.acclorite.book_story.presentation.browse.BrowseEvent
|
||||||
import ua.acclorite.book_story.presentation.library.model.SelectableNullableBook
|
import ua.acclorite.book_story.presentation.browse.model.NullableBook
|
||||||
|
import ua.acclorite.book_story.presentation.browse.model.SelectableNullableBook
|
||||||
import ua.acclorite.book_story.ui.common.components.dialog.Dialog
|
import ua.acclorite.book_story.ui.common.components.dialog.Dialog
|
||||||
import ua.acclorite.book_story.ui.common.components.progress_indicator.CircularProgressIndicator
|
import ua.acclorite.book_story.ui.common.components.progress_indicator.CircularProgressIndicator
|
||||||
import ua.acclorite.book_story.ui.common.helpers.showToast
|
import ua.acclorite.book_story.ui.common.helpers.showToast
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseAddDialog(
|
fun BrowseAddDialog(
|
||||||
|
|
@ -63,22 +62,25 @@ fun BrowseAddDialog(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
items(
|
itemsIndexed(
|
||||||
selectedBooksAddDialog,
|
selectedBooksAddDialog,
|
||||||
key = { it.data.fileName ?: UUID.randomUUID() }
|
key = { index, _ -> index }
|
||||||
) { book ->
|
) { _, book ->
|
||||||
BrowseAddDialogItem(
|
BrowseAddDialogItem(
|
||||||
result = book
|
result = book
|
||||||
) {
|
) {
|
||||||
if (it) {
|
when (book.data) {
|
||||||
selectAddDialog(
|
is NullableBook.NotNull -> {
|
||||||
BrowseEvent.OnSelectAddDialog(
|
selectAddDialog(
|
||||||
book = book
|
BrowseEvent.OnSelectAddDialog(
|
||||||
|
book = book
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
} else {
|
|
||||||
book.data.message?.asString(context)
|
is NullableBook.Null -> {
|
||||||
?.showToast(context = context)
|
book.data.message?.asString(context)?.showToast(context = context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,73 +24,77 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.data.model.common.NullableBook
|
import ua.acclorite.book_story.presentation.browse.model.NullableBook
|
||||||
import ua.acclorite.book_story.presentation.library.model.SelectableNullableBook
|
import ua.acclorite.book_story.presentation.browse.model.SelectableNullableBook
|
||||||
import ua.acclorite.book_story.ui.common.components.common.CircularCheckbox
|
import ua.acclorite.book_story.ui.common.components.common.CircularCheckbox
|
||||||
import ua.acclorite.book_story.ui.common.components.common.StyledText
|
import ua.acclorite.book_story.ui.common.components.common.StyledText
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseAddDialogItem(result: SelectableNullableBook, onClick: (Boolean) -> Unit) {
|
fun BrowseAddDialogItem(result: SelectableNullableBook, onClick: () -> Unit) {
|
||||||
if (result.data is NullableBook.NotNull) {
|
when (result.data) {
|
||||||
Row(
|
is NullableBook.NotNull -> {
|
||||||
Modifier
|
Row(
|
||||||
.fillMaxWidth()
|
Modifier
|
||||||
.clickable {
|
.fillMaxWidth()
|
||||||
onClick(true)
|
.clickable {
|
||||||
}
|
onClick()
|
||||||
.padding(vertical = 12.dp, horizontal = 24.dp),
|
}
|
||||||
verticalAlignment = Alignment.CenterVertically
|
.padding(vertical = 12.dp, horizontal = 24.dp),
|
||||||
) {
|
verticalAlignment = Alignment.CenterVertically
|
||||||
Column(
|
|
||||||
Modifier.weight(1f)
|
|
||||||
) {
|
) {
|
||||||
StyledText(
|
Column(
|
||||||
text = result.data.bookWithCover!!.book.title,
|
Modifier.weight(1f)
|
||||||
style = MaterialTheme.typography.bodyLarge.copy(
|
) {
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
StyledText(
|
||||||
),
|
text = result.data.book.title,
|
||||||
maxLines = 1
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
)
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
StyledText(
|
),
|
||||||
text = result.data.bookWithCover.book.author.asString(),
|
maxLines = 1
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(
|
)
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
StyledText(
|
||||||
),
|
text = result.data.book.author.asString(),
|
||||||
maxLines = 1
|
style = MaterialTheme.typography.bodyMedium.copy(
|
||||||
)
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
}
|
),
|
||||||
Row {
|
maxLines = 1
|
||||||
Spacer(modifier = Modifier.width(24.dp))
|
)
|
||||||
CircularCheckbox(
|
}
|
||||||
selected = result.selected,
|
Row {
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
Spacer(modifier = Modifier.width(24.dp))
|
||||||
)
|
CircularCheckbox(
|
||||||
|
selected = result.selected,
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Row(
|
is NullableBook.Null -> {
|
||||||
Modifier
|
Row(
|
||||||
.fillMaxWidth()
|
Modifier
|
||||||
.clickable {
|
.fillMaxWidth()
|
||||||
onClick(false)
|
.clickable {
|
||||||
}
|
onClick()
|
||||||
.padding(vertical = 12.dp, horizontal = 24.dp),
|
}
|
||||||
verticalAlignment = Alignment.CenterVertically
|
.padding(vertical = 12.dp, horizontal = 24.dp),
|
||||||
) {
|
verticalAlignment = Alignment.CenterVertically
|
||||||
Icon(
|
) {
|
||||||
imageVector = Icons.Default.Error,
|
Icon(
|
||||||
contentDescription = stringResource(id = R.string.error_content_desc),
|
imageVector = Icons.Default.Error,
|
||||||
modifier = Modifier.size(26.dp),
|
contentDescription = stringResource(id = R.string.error_content_desc),
|
||||||
tint = MaterialTheme.colorScheme.error
|
modifier = Modifier.size(26.dp),
|
||||||
)
|
tint = MaterialTheme.colorScheme.error
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
)
|
||||||
StyledText(
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
text = result.data.fileName!!,
|
StyledText(
|
||||||
style = MaterialTheme.typography.bodyLarge.copy(
|
text = result.data.fileName,
|
||||||
color = MaterialTheme.colorScheme.error
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
),
|
color = MaterialTheme.colorScheme.error
|
||||||
maxLines = 2
|
),
|
||||||
)
|
maxLines = 2
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -17,7 +17,7 @@ import ua.acclorite.book_story.core.Dialog
|
||||||
import ua.acclorite.book_story.presentation.browse.BrowseEvent
|
import ua.acclorite.book_story.presentation.browse.BrowseEvent
|
||||||
import ua.acclorite.book_story.presentation.browse.model.BrowseLayout
|
import ua.acclorite.book_story.presentation.browse.model.BrowseLayout
|
||||||
import ua.acclorite.book_story.presentation.browse.model.SelectableFile
|
import ua.acclorite.book_story.presentation.browse.model.SelectableFile
|
||||||
import ua.acclorite.book_story.presentation.library.model.SelectableNullableBook
|
import ua.acclorite.book_story.presentation.browse.model.SelectableNullableBook
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import androidx.compose.runtime.Composable
|
||||||
import ua.acclorite.book_story.core.Dialog
|
import ua.acclorite.book_story.core.Dialog
|
||||||
import ua.acclorite.book_story.presentation.browse.BrowseEvent
|
import ua.acclorite.book_story.presentation.browse.BrowseEvent
|
||||||
import ua.acclorite.book_story.presentation.browse.BrowseScreen
|
import ua.acclorite.book_story.presentation.browse.BrowseScreen
|
||||||
import ua.acclorite.book_story.presentation.library.model.SelectableNullableBook
|
import ua.acclorite.book_story.presentation.browse.model.SelectableNullableBook
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseDialog(
|
fun BrowseDialog(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue