🚀 Add and Improve Parsers
Added new supported file extensions: ".zip", ".html" and ".htm". Improved parsers: ".epub". Partly Resolves(no ".cbz"): #48
This commit is contained in:
parent
5e2cc28c92
commit
e1fdac0f39
13 changed files with 987 additions and 536 deletions
|
|
@ -2,8 +2,11 @@ package ua.acclorite.book_story.data.parser
|
||||||
|
|
||||||
import ua.acclorite.book_story.data.parser.epub.EpubFileParser
|
import ua.acclorite.book_story.data.parser.epub.EpubFileParser
|
||||||
import ua.acclorite.book_story.data.parser.fb2.Fb2FileParser
|
import ua.acclorite.book_story.data.parser.fb2.Fb2FileParser
|
||||||
|
import ua.acclorite.book_story.data.parser.htm.HtmFileParser
|
||||||
|
import ua.acclorite.book_story.data.parser.html.HtmlFileParser
|
||||||
import ua.acclorite.book_story.data.parser.pdf.PdfFileParser
|
import ua.acclorite.book_story.data.parser.pdf.PdfFileParser
|
||||||
import ua.acclorite.book_story.data.parser.txt.TxtFileParser
|
import ua.acclorite.book_story.data.parser.txt.TxtFileParser
|
||||||
|
import ua.acclorite.book_story.data.parser.zip.ZipFileParser
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.util.CoverImage
|
import ua.acclorite.book_story.domain.util.CoverImage
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -13,32 +16,48 @@ class FileParserImpl @Inject constructor(
|
||||||
private val txtFileParser: TxtFileParser,
|
private val txtFileParser: TxtFileParser,
|
||||||
private val pdfFileParser: PdfFileParser,
|
private val pdfFileParser: PdfFileParser,
|
||||||
private val epubFileParser: EpubFileParser,
|
private val epubFileParser: EpubFileParser,
|
||||||
private val fb2FileParser: Fb2FileParser
|
private val fb2FileParser: Fb2FileParser,
|
||||||
|
private val zipFileParser: ZipFileParser,
|
||||||
|
private val htmlFileParser: HtmlFileParser,
|
||||||
|
private val htmFileParser: HtmFileParser,
|
||||||
) : FileParser {
|
) : FileParser {
|
||||||
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
|
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val fileFormat = ".${file.name.substringAfterLast(".")}".lowercase().trim()
|
val fileFormat = ".${file.extension}".lowercase().trim()
|
||||||
|
return when (fileFormat) {
|
||||||
|
".pdf" -> {
|
||||||
|
pdfFileParser.parse(file)
|
||||||
|
}
|
||||||
|
|
||||||
if (fileFormat == ".pdf") {
|
".epub" -> {
|
||||||
return pdfFileParser.parse(file)
|
epubFileParser.parse(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
".txt" -> {
|
||||||
|
txtFileParser.parse(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
".fb2" -> {
|
||||||
|
fb2FileParser.parse(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
".zip" -> {
|
||||||
|
zipFileParser.parse(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
".html" -> {
|
||||||
|
htmlFileParser.parse(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
".htm" -> {
|
||||||
|
htmFileParser.parse(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileFormat == ".epub") {
|
|
||||||
return epubFileParser.parse(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileFormat == ".txt") {
|
|
||||||
return txtFileParser.parse(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileFormat == ".fb2") {
|
|
||||||
return fb2FileParser.parse(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package ua.acclorite.book_story.data.parser
|
package ua.acclorite.book_story.data.parser
|
||||||
|
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.data.parser.epub.EpubTextParser
|
|
||||||
import ua.acclorite.book_story.data.parser.fb2.Fb2TextParser
|
import ua.acclorite.book_story.data.parser.fb2.Fb2TextParser
|
||||||
|
import ua.acclorite.book_story.data.parser.htm.HtmTextParser
|
||||||
|
import ua.acclorite.book_story.data.parser.html.HtmlTextParser
|
||||||
import ua.acclorite.book_story.data.parser.pdf.PdfTextParser
|
import ua.acclorite.book_story.data.parser.pdf.PdfTextParser
|
||||||
import ua.acclorite.book_story.data.parser.txt.TxtTextParser
|
import ua.acclorite.book_story.data.parser.txt.TxtTextParser
|
||||||
|
import ua.acclorite.book_story.data.parser.zip.ZipTextParser
|
||||||
import ua.acclorite.book_story.domain.util.Resource
|
import ua.acclorite.book_story.domain.util.Resource
|
||||||
import ua.acclorite.book_story.domain.util.UIText
|
import ua.acclorite.book_story.domain.util.UIText
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -13,8 +15,11 @@ import javax.inject.Inject
|
||||||
class TextParserImpl @Inject constructor(
|
class TextParserImpl @Inject constructor(
|
||||||
private val txtTextParser: TxtTextParser,
|
private val txtTextParser: TxtTextParser,
|
||||||
private val pdfTextParser: PdfTextParser,
|
private val pdfTextParser: PdfTextParser,
|
||||||
private val epubTextParser: EpubTextParser,
|
private val epubTextParser: HtmlTextParser,
|
||||||
private val fb2TextParser: Fb2TextParser
|
private val fb2TextParser: Fb2TextParser,
|
||||||
|
private val zipTextParser: ZipTextParser,
|
||||||
|
private val htmlTextParser: HtmlTextParser,
|
||||||
|
private val htmTextParser: HtmTextParser,
|
||||||
) : TextParser {
|
) : TextParser {
|
||||||
override suspend fun parse(file: File): Resource<List<String>> {
|
override suspend fun parse(file: File): Resource<List<String>> {
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
|
|
@ -23,25 +28,38 @@ class TextParserImpl @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val fileFormat = ".${file.name.substringAfterLast(".")}".lowercase().trim()
|
val fileFormat = ".${file.extension}".lowercase().trim()
|
||||||
|
return when (fileFormat) {
|
||||||
|
".pdf" -> {
|
||||||
|
pdfTextParser.parse(file)
|
||||||
|
}
|
||||||
|
|
||||||
if (fileFormat == ".pdf") {
|
".epub" -> {
|
||||||
return pdfTextParser.parse(file)
|
epubTextParser.parse(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
".txt" -> {
|
||||||
|
txtTextParser.parse(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
".fb2" -> {
|
||||||
|
fb2TextParser.parse(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
".zip" -> {
|
||||||
|
zipTextParser.parse(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
".html" -> {
|
||||||
|
htmlTextParser.parse(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
".htm" -> {
|
||||||
|
htmTextParser.parse(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileFormat == ".epub") {
|
|
||||||
return epubTextParser.parse(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileFormat == ".txt") {
|
|
||||||
return txtTextParser.parse(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileFormat == ".fb2") {
|
|
||||||
return fb2TextParser.parse(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ 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 ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.data.parser.FileParser
|
import ua.acclorite.book_story.data.parser.FileParser
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
|
|
@ -36,30 +37,43 @@ class EpubFileParser @Inject constructor() : FileParser {
|
||||||
.use { it.readText() }
|
.use { it.readText() }
|
||||||
val document = Jsoup.parse(opfContent)
|
val document = Jsoup.parse(opfContent)
|
||||||
|
|
||||||
val title = document.select("metadata > dc|title").text().trim()
|
val title = document.select("metadata > dc|title").text().trim().run {
|
||||||
val author = UIText.StringValue(
|
ifBlank {
|
||||||
document.select("metadata > dc|creator").text().trim()
|
file.nameWithoutExtension.trim()
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val author = document.select("metadata > dc|creator").text().trim().run {
|
||||||
|
if (isBlank()) {
|
||||||
|
UIText.StringResource(R.string.unknown_author)
|
||||||
|
} else {
|
||||||
|
UIText.StringValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val description = Jsoup.parse(
|
val description = Jsoup.parse(
|
||||||
document.select("metadata > dc|description").text()
|
document.select("metadata > dc|description").text()
|
||||||
).text()
|
).text().run {
|
||||||
|
ifBlank {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var coverImagePath: String? = null
|
val coverImage = document
|
||||||
|
|
||||||
val coverId = document
|
|
||||||
.select("metadata > meta[name=cover]")
|
.select("metadata > meta[name=cover]")
|
||||||
.attr("content")
|
.attr("content")
|
||||||
if (coverId.isNotBlank()) {
|
.run {
|
||||||
coverImagePath = document
|
if (isNotBlank()) {
|
||||||
.select("manifest > item[id=$coverId]")
|
document
|
||||||
.attr("href")
|
.select("manifest > item[id=$this]")
|
||||||
}
|
.attr("href")
|
||||||
|
.apply { if (isNotBlank()) return@run this }
|
||||||
|
}
|
||||||
|
|
||||||
if (coverImagePath.isNullOrBlank()) {
|
document
|
||||||
coverImagePath = document
|
.select("manifest > item[media-type*=image]")
|
||||||
.select("manifest > item[media-type*=image]")
|
.firstOrNull()?.attr("href")
|
||||||
.firstOrNull()?.attr("href")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
book = Book(
|
book = Book(
|
||||||
title = title,
|
title = title,
|
||||||
|
|
@ -73,7 +87,7 @@ class EpubFileParser @Inject constructor() : FileParser {
|
||||||
lastOpened = null,
|
lastOpened = null,
|
||||||
category = Category.entries[0],
|
category = Category.entries[0],
|
||||||
coverImage = null
|
coverImage = null
|
||||||
) to extractCoverImageBitmap(file, coverImagePath)
|
) to extractCoverImageBitmap(file, coverImage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return book
|
return book
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ class Fb2FileParser @Inject constructor() : FileParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
val titleFromFile = extractElementContent(document, "book-title")
|
val titleFromFile = extractElementContent(document, "book-title")
|
||||||
val title = titleFromFile ?: file.name.dropLast(4).trim()
|
val title = titleFromFile ?: file.nameWithoutExtension.trim()
|
||||||
|
|
||||||
val authorFirstName = extractElementContent(document, "first-name")
|
val authorFirstName = extractElementContent(document, "first-name")
|
||||||
val authorLastName = extractElementContent(document, "last-name")
|
val authorLastName = extractElementContent(document, "last-name")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
package ua.acclorite.book_story.data.parser.htm
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import ua.acclorite.book_story.R
|
||||||
|
import ua.acclorite.book_story.data.parser.FileParser
|
||||||
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
|
import ua.acclorite.book_story.domain.util.CoverImage
|
||||||
|
import ua.acclorite.book_story.domain.util.UIText
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class HtmFileParser @Inject constructor() : FileParser {
|
||||||
|
|
||||||
|
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
|
||||||
|
if (!file.name.endsWith(".htm", true) || !file.exists()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val document = Jsoup.parse(file)
|
||||||
|
|
||||||
|
val title = document.select("head > title").text().trim().run {
|
||||||
|
ifBlank {
|
||||||
|
file.nameWithoutExtension.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Book(
|
||||||
|
title = title,
|
||||||
|
author = UIText.StringResource(R.string.unknown_author),
|
||||||
|
description = null,
|
||||||
|
textPath = "",
|
||||||
|
scrollIndex = 0,
|
||||||
|
scrollOffset = 0,
|
||||||
|
progress = 0f,
|
||||||
|
filePath = file.path,
|
||||||
|
lastOpened = null,
|
||||||
|
category = Category.entries[0],
|
||||||
|
coverImage = null
|
||||||
|
) to null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package ua.acclorite.book_story.data.parser.htm
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import ua.acclorite.book_story.R
|
||||||
|
import ua.acclorite.book_story.data.parser.TextParser
|
||||||
|
import ua.acclorite.book_story.domain.util.Resource
|
||||||
|
import ua.acclorite.book_story.domain.util.UIText
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
|
class HtmTextParser @Inject constructor() : TextParser {
|
||||||
|
|
||||||
|
override suspend fun parse(file: File): Resource<List<String>> {
|
||||||
|
if (!file.name.endsWith(".htm", true) || !file.exists()) {
|
||||||
|
return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val lines = mutableListOf<String>()
|
||||||
|
|
||||||
|
val document = Jsoup.parse(file)
|
||||||
|
document.select("p").append("\n")
|
||||||
|
document
|
||||||
|
.wholeText()
|
||||||
|
.lines()
|
||||||
|
.forEach { line ->
|
||||||
|
if (line.isNotBlank()) {
|
||||||
|
lines.add(line.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lines.isEmpty()) {
|
||||||
|
return Resource.Error(UIText.StringResource(R.string.error_file_empty))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Resource.Success(lines)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return Resource.Error(
|
||||||
|
UIText.StringResource(
|
||||||
|
R.string.error_query,
|
||||||
|
e.message?.take(40)?.trim() ?: ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
package ua.acclorite.book_story.data.parser.html
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import ua.acclorite.book_story.R
|
||||||
|
import ua.acclorite.book_story.data.parser.FileParser
|
||||||
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
|
import ua.acclorite.book_story.domain.util.CoverImage
|
||||||
|
import ua.acclorite.book_story.domain.util.UIText
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class HtmlFileParser @Inject constructor() : FileParser {
|
||||||
|
|
||||||
|
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
|
||||||
|
if (!file.name.endsWith(".html", true) || !file.exists()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val document = Jsoup.parse(file)
|
||||||
|
|
||||||
|
val title = document.select("head > title").text().trim().run {
|
||||||
|
ifBlank {
|
||||||
|
file.nameWithoutExtension.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Book(
|
||||||
|
title = title,
|
||||||
|
author = UIText.StringResource(R.string.unknown_author),
|
||||||
|
description = null,
|
||||||
|
textPath = "",
|
||||||
|
scrollIndex = 0,
|
||||||
|
scrollOffset = 0,
|
||||||
|
progress = 0f,
|
||||||
|
filePath = file.path,
|
||||||
|
lastOpened = null,
|
||||||
|
category = Category.entries[0],
|
||||||
|
coverImage = null
|
||||||
|
) to null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package ua.acclorite.book_story.data.parser.html
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import ua.acclorite.book_story.R
|
||||||
|
import ua.acclorite.book_story.data.parser.TextParser
|
||||||
|
import ua.acclorite.book_story.domain.util.Resource
|
||||||
|
import ua.acclorite.book_story.domain.util.UIText
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
|
class HtmlTextParser @Inject constructor() : TextParser {
|
||||||
|
|
||||||
|
override suspend fun parse(file: File): Resource<List<String>> {
|
||||||
|
if (!file.name.endsWith(".html", true) || !file.exists()) {
|
||||||
|
return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val lines = mutableListOf<String>()
|
||||||
|
|
||||||
|
val document = Jsoup.parse(file)
|
||||||
|
document.select("p").append("\n")
|
||||||
|
document
|
||||||
|
.wholeText()
|
||||||
|
.lines()
|
||||||
|
.forEach { line ->
|
||||||
|
if (line.isNotBlank()) {
|
||||||
|
lines.add(line.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lines.isEmpty()) {
|
||||||
|
return Resource.Error(UIText.StringResource(R.string.error_file_empty))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Resource.Success(lines)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return Resource.Error(
|
||||||
|
UIText.StringResource(
|
||||||
|
R.string.error_query,
|
||||||
|
e.message?.take(40)?.trim() ?: ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,10 +24,11 @@ class PdfFileParser @Inject constructor(private val application: Application) :
|
||||||
|
|
||||||
val document = PDDocument.load(file)
|
val document = PDDocument.load(file)
|
||||||
|
|
||||||
val title = document.documentInformation.title ?: file.name.dropLast(4).trim()
|
val title = document.documentInformation.title ?: file.nameWithoutExtension.trim()
|
||||||
val fileAuthor = document.documentInformation.author
|
val author = document.documentInformation.author.run {
|
||||||
val author = if (fileAuthor != null) UIText.StringValue(fileAuthor)
|
if (isNullOrBlank()) UIText.StringResource(R.string.unknown_author)
|
||||||
else UIText.StringResource(R.string.unknown_author)
|
else UIText.StringValue(this)
|
||||||
|
}
|
||||||
val description = document.documentInformation.subject ?: null
|
val description = document.documentInformation.subject ?: null
|
||||||
|
|
||||||
document.close()
|
document.close()
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ class TxtFileParser @Inject constructor() : FileParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val title = file.name.trim().dropLast(4)
|
val title = file.nameWithoutExtension.trim()
|
||||||
val author = UIText.StringResource(R.string.unknown_author)
|
val author = UIText.StringResource(R.string.unknown_author)
|
||||||
|
|
||||||
return Book(
|
return Book(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
package ua.acclorite.book_story.data.parser.zip
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import ua.acclorite.book_story.R
|
||||||
|
import ua.acclorite.book_story.data.parser.FileParser
|
||||||
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
|
import ua.acclorite.book_story.domain.util.CoverImage
|
||||||
|
import ua.acclorite.book_story.domain.util.UIText
|
||||||
|
import java.io.File
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ZipFileParser @Inject constructor() : FileParser {
|
||||||
|
|
||||||
|
override suspend fun parse(file: File): Pair<Book, CoverImage?>? {
|
||||||
|
if (!file.name.endsWith(".zip", true) || !file.exists()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var book: Pair<Book, CoverImage?>? = null
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
ZipFile(file).use { zip ->
|
||||||
|
val opfEntry = zip.entries().asSequence().find { entry ->
|
||||||
|
entry.name.endsWith(".opf")
|
||||||
|
} ?: return@withContext
|
||||||
|
|
||||||
|
val opfContent = zip
|
||||||
|
.getInputStream(opfEntry)
|
||||||
|
.bufferedReader()
|
||||||
|
.use { it.readText() }
|
||||||
|
val document = Jsoup.parse(opfContent)
|
||||||
|
|
||||||
|
val title = document.select("metadata > dc|title").text().trim().run {
|
||||||
|
ifBlank {
|
||||||
|
file.nameWithoutExtension.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val author = document.select("metadata > dc|creator").text().trim().run {
|
||||||
|
if (isBlank()) {
|
||||||
|
UIText.StringResource(R.string.unknown_author)
|
||||||
|
} else {
|
||||||
|
UIText.StringValue(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val description = Jsoup.parse(
|
||||||
|
document.select("metadata > dc|description").text()
|
||||||
|
).text().run {
|
||||||
|
ifBlank {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val coverImage = document
|
||||||
|
.select("metadata > meta[name=cover]")
|
||||||
|
.attr("content")
|
||||||
|
.run {
|
||||||
|
if (isNotBlank()) {
|
||||||
|
document
|
||||||
|
.select("manifest > item[id=$this]")
|
||||||
|
.attr("href")
|
||||||
|
.apply { if (isNotBlank()) return@run this }
|
||||||
|
}
|
||||||
|
|
||||||
|
document
|
||||||
|
.select("manifest > item[media-type*=image]")
|
||||||
|
.firstOrNull()?.attr("href")
|
||||||
|
}
|
||||||
|
|
||||||
|
book = Book(
|
||||||
|
title = title,
|
||||||
|
author = author,
|
||||||
|
description = description,
|
||||||
|
textPath = "",
|
||||||
|
scrollIndex = 0,
|
||||||
|
scrollOffset = 0,
|
||||||
|
progress = 0f,
|
||||||
|
filePath = file.path,
|
||||||
|
lastOpened = null,
|
||||||
|
category = Category.entries[0],
|
||||||
|
coverImage = null
|
||||||
|
) to extractCoverImageBitmap(file, coverImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return book
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return 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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
package ua.acclorite.book_story.data.parser.zip
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import ua.acclorite.book_story.R
|
||||||
|
import ua.acclorite.book_story.data.parser.TextParser
|
||||||
|
import ua.acclorite.book_story.domain.util.Resource
|
||||||
|
import ua.acclorite.book_story.domain.util.UIText
|
||||||
|
import java.io.File
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
|
class ZipTextParser @Inject constructor() : TextParser {
|
||||||
|
|
||||||
|
override suspend fun parse(file: File): Resource<List<String>> {
|
||||||
|
if (!file.name.endsWith(".zip", true) || !file.exists()) {
|
||||||
|
return Resource.Error(UIText.StringResource(R.string.error_wrong_file_format))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val lines = mutableListOf<String>()
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
ZipFile(file).use { zip ->
|
||||||
|
zip.entries().asSequence().forEach { entry ->
|
||||||
|
if (
|
||||||
|
entry.name.endsWith(".xhtml")
|
||||||
|
|| entry.name.endsWith(".html")
|
||||||
|
|| entry.name.endsWith(".xml")
|
||||||
|
|| entry.name.endsWith(".htm")
|
||||||
|
) {
|
||||||
|
val content = zip.getInputStream(entry).bufferedReader()
|
||||||
|
.use {
|
||||||
|
it.readText()
|
||||||
|
}
|
||||||
|
|
||||||
|
val document = Jsoup.parse(content)
|
||||||
|
document.select("p").append("\n")
|
||||||
|
document
|
||||||
|
.wholeText()
|
||||||
|
.lines()
|
||||||
|
.forEach { line ->
|
||||||
|
if (line.isNotBlank()) {
|
||||||
|
lines.add(line.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lines.isEmpty()) {
|
||||||
|
return Resource.Error(UIText.StringResource(R.string.error_file_empty))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Resource.Success(lines)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return Resource.Error(
|
||||||
|
UIText.StringResource(
|
||||||
|
R.string.error_query,
|
||||||
|
e.message?.take(40)?.trim() ?: ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,497 +27,521 @@ object Constants {
|
||||||
const val MAIN_STATE = "main_state"
|
const val MAIN_STATE = "main_state"
|
||||||
|
|
||||||
// Supported file extensions
|
// Supported file extensions
|
||||||
val EXTENSIONS = listOf(".epub", ".pdf", ".fb2", ".txt")
|
val EXTENSIONS = provideSupportedExtensions()
|
||||||
|
|
||||||
// Supported languages
|
// Supported languages
|
||||||
val LANGUAGES = listOf(
|
val LANGUAGES = provideLanguages()
|
||||||
Pair("en", "English"),
|
|
||||||
Pair("uk", "Українська"),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Navigation items for NavigationBars.
|
// Navigation items for NavigationBars.
|
||||||
val NAVIGATION_ITEMS = listOf(
|
val NAVIGATION_ITEMS = provideNavigationItems()
|
||||||
NavigationItem(
|
|
||||||
screen = Screen.Library,
|
|
||||||
title = R.string.library_screen,
|
|
||||||
tooltip = R.string.library_content_desc,
|
|
||||||
selectedIcon = R.drawable.library_screen_filled,
|
|
||||||
unselectedIcon = R.drawable.library_screen_outlined
|
|
||||||
),
|
|
||||||
NavigationItem(
|
|
||||||
screen = Screen.History,
|
|
||||||
title = R.string.history_screen,
|
|
||||||
tooltip = R.string.history_content_desc,
|
|
||||||
selectedIcon = R.drawable.history_screen_filled,
|
|
||||||
unselectedIcon = R.drawable.history_screen_outlined
|
|
||||||
),
|
|
||||||
NavigationItem(
|
|
||||||
screen = Screen.Browse,
|
|
||||||
title = R.string.browse_screen,
|
|
||||||
tooltip = R.string.browse_content_desc,
|
|
||||||
selectedIcon = R.drawable.browse_screen_filled,
|
|
||||||
unselectedIcon = R.drawable.browse_screen_outlined
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Supported themes
|
// Supported themes
|
||||||
val THEMES = listOf(
|
val THEMES = provideThemes()
|
||||||
Pair(Theme.DYNAMIC, UIText.StringResource(R.string.dynamic_theme)),
|
|
||||||
Pair(Theme.BLUE, UIText.StringResource(R.string.blue_theme)),
|
|
||||||
Pair(Theme.GREEN, UIText.StringResource(R.string.green_theme)),
|
|
||||||
Pair(Theme.RED, UIText.StringResource(R.string.red_theme)),
|
|
||||||
Pair(Theme.PURPLE, UIText.StringResource(R.string.purple_theme)),
|
|
||||||
Pair(Theme.PINK, UIText.StringResource(R.string.pink_theme)),
|
|
||||||
Pair(Theme.YELLOW, UIText.StringResource(R.string.yellow_theme)),
|
|
||||||
Pair(Theme.AQUA, UIText.StringResource(R.string.aqua_theme)),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Credits
|
// Credits
|
||||||
val CREDITS = listOf(
|
val CREDITS = provideCredits()
|
||||||
Credit(
|
|
||||||
name = "Tachiyomi (Mihon)",
|
|
||||||
source = "GitHub",
|
|
||||||
credits = listOf(
|
|
||||||
UIText.StringResource(R.string.credits_design),
|
|
||||||
UIText.StringValue("Readme")
|
|
||||||
),
|
|
||||||
website = "https://www.github.com/mihonapp/mihon"
|
|
||||||
),
|
|
||||||
Credit(
|
|
||||||
name = "Kitsune",
|
|
||||||
source = "GitHub",
|
|
||||||
credits = listOf(
|
|
||||||
UIText.StringResource(R.string.credits_updates)
|
|
||||||
),
|
|
||||||
website = "https://www.github.com/Drumber/Kitsune"
|
|
||||||
),
|
|
||||||
Credit(
|
|
||||||
name = "Material Design Icons",
|
|
||||||
source = "Google Fonts",
|
|
||||||
credits = listOf(
|
|
||||||
UIText.StringResource(R.string.credits_icon)
|
|
||||||
),
|
|
||||||
website = "https://fonts.google.com/icons"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Help Tips
|
// Help Tips
|
||||||
val HELP_TIPS = listOf(
|
val HELP_TIPS = provideHelpTips()
|
||||||
HelpTip(
|
|
||||||
title = R.string.help_title_how_to_find_books,
|
|
||||||
description = { _, _ ->
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_find_books))
|
|
||||||
},
|
|
||||||
customContent = { onHelpEvent ->
|
|
||||||
HelpFindBooksContent(onEvent = onHelpEvent)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
|
|
||||||
HelpTip(
|
|
||||||
title = R.string.help_title_how_to_add_books,
|
|
||||||
description = { onNavigate, fromStart ->
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_add_books_1) + " ")
|
|
||||||
|
|
||||||
HelpAnnotation(
|
|
||||||
onClick = {
|
|
||||||
if (!fromStart) {
|
|
||||||
onNavigate {
|
|
||||||
navigate(Screen.Browse, useBackAnimation = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_add_books_2))
|
|
||||||
}
|
|
||||||
append(". ")
|
|
||||||
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_add_books_3) + " ")
|
|
||||||
|
|
||||||
HelpAnnotation(
|
|
||||||
onClick = {
|
|
||||||
if (!fromStart) {
|
|
||||||
onNavigate {
|
|
||||||
navigate(Screen.Library, useBackAnimation = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_add_books_4))
|
|
||||||
}
|
|
||||||
append(".")
|
|
||||||
}
|
|
||||||
),
|
|
||||||
|
|
||||||
HelpTip(
|
|
||||||
title = R.string.help_title_how_to_customize_app,
|
|
||||||
description = { onNavigate, fromStart ->
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_customize_app_1) + " ")
|
|
||||||
|
|
||||||
HelpAnnotation(
|
|
||||||
onClick = {
|
|
||||||
if (!fromStart) {
|
|
||||||
onNavigate {
|
|
||||||
navigate(Screen.Settings, useBackAnimation = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_customize_app_2))
|
|
||||||
}
|
|
||||||
append(". ")
|
|
||||||
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_customize_app_3))
|
|
||||||
}
|
|
||||||
),
|
|
||||||
|
|
||||||
HelpTip(
|
|
||||||
title = R.string.help_title_how_to_move_or_delete_books,
|
|
||||||
description = { onNavigate, fromStart ->
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_move_or_delete_books_1) + " ")
|
|
||||||
|
|
||||||
HelpAnnotation(
|
|
||||||
onClick = {
|
|
||||||
if (!fromStart) {
|
|
||||||
onNavigate {
|
|
||||||
navigate(Screen.Library, useBackAnimation = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_move_or_delete_books_2))
|
|
||||||
}
|
|
||||||
append(". ")
|
|
||||||
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_move_or_delete_books_3))
|
|
||||||
}
|
|
||||||
),
|
|
||||||
|
|
||||||
HelpTip(
|
|
||||||
title = R.string.help_title_how_to_edit_book,
|
|
||||||
description = { onNavigate, fromStart ->
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_edit_book_1) + " ")
|
|
||||||
|
|
||||||
HelpAnnotation(
|
|
||||||
onClick = {
|
|
||||||
if (!fromStart) {
|
|
||||||
onNavigate {
|
|
||||||
navigate(Screen.Library, useBackAnimation = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_edit_book_2))
|
|
||||||
}
|
|
||||||
append(" ")
|
|
||||||
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_edit_book_3))
|
|
||||||
}
|
|
||||||
),
|
|
||||||
|
|
||||||
HelpTip(
|
|
||||||
title = R.string.help_title_how_to_read_book,
|
|
||||||
description = { onNavigate, fromStart ->
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_read_book_1) + " ")
|
|
||||||
|
|
||||||
HelpAnnotation(
|
|
||||||
onClick = {
|
|
||||||
if (!fromStart) {
|
|
||||||
onNavigate {
|
|
||||||
navigate(Screen.Library, useBackAnimation = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_read_book_2))
|
|
||||||
}
|
|
||||||
append(". ")
|
|
||||||
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_read_book_3))
|
|
||||||
}
|
|
||||||
),
|
|
||||||
|
|
||||||
HelpTip(
|
|
||||||
title = R.string.help_title_how_to_customize_reader,
|
|
||||||
description = { onNavigate, fromStart ->
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_customize_reader_1) + " ")
|
|
||||||
|
|
||||||
HelpAnnotation(
|
|
||||||
onClick = {
|
|
||||||
if (!fromStart) {
|
|
||||||
onNavigate {
|
|
||||||
navigate(Screen.Settings, useBackAnimation = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_customize_reader_2))
|
|
||||||
}
|
|
||||||
append(" ")
|
|
||||||
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_customize_reader_3))
|
|
||||||
}
|
|
||||||
),
|
|
||||||
|
|
||||||
HelpTip(
|
|
||||||
title = R.string.help_title_how_to_update_book,
|
|
||||||
description = { onNavigate, fromStart ->
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_update_book_1) + " ")
|
|
||||||
|
|
||||||
HelpAnnotation(
|
|
||||||
onClick = {
|
|
||||||
if (!fromStart) {
|
|
||||||
onNavigate {
|
|
||||||
navigate(Screen.Library, useBackAnimation = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_update_book_2))
|
|
||||||
}
|
|
||||||
append(" ")
|
|
||||||
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_update_book_3))
|
|
||||||
}
|
|
||||||
),
|
|
||||||
|
|
||||||
HelpTip(
|
|
||||||
title = R.string.help_title_how_to_manage_history,
|
|
||||||
description = { onNavigate, fromStart ->
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_manage_history_1) + " ")
|
|
||||||
|
|
||||||
HelpAnnotation(
|
|
||||||
onClick = {
|
|
||||||
if (!fromStart) {
|
|
||||||
onNavigate {
|
|
||||||
navigate(Screen.History, useBackAnimation = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_manage_history_2))
|
|
||||||
}
|
|
||||||
append(". ")
|
|
||||||
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_manage_history_3))
|
|
||||||
}
|
|
||||||
),
|
|
||||||
|
|
||||||
HelpTip(
|
|
||||||
title = R.string.help_title_how_to_use_tooltip,
|
|
||||||
description = { _, _ ->
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_use_tooltip_1))
|
|
||||||
}
|
|
||||||
),
|
|
||||||
|
|
||||||
HelpTip(
|
|
||||||
title = R.string.help_title_how_to_use_double_click_translation,
|
|
||||||
description = { _, _ ->
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_use_double_click_translation_1))
|
|
||||||
}
|
|
||||||
),
|
|
||||||
|
|
||||||
HelpTip(
|
|
||||||
title = R.string.help_title_how_to_create_color_presets,
|
|
||||||
description = { _, _ ->
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_create_color_presets_1))
|
|
||||||
}
|
|
||||||
),
|
|
||||||
|
|
||||||
HelpTip(
|
|
||||||
title = R.string.help_title_how_to_use_color_presets,
|
|
||||||
description = { _, _ ->
|
|
||||||
append(stringResource(id = R.string.help_desc_how_to_use_color_presets_1))
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
// About Badges
|
// About Badges
|
||||||
val ABOUT_BADGES = listOf(
|
val ABOUT_BADGES = provideAboutBadges()
|
||||||
Badge(
|
|
||||||
id = "x",
|
|
||||||
drawable = R.drawable.x_logo,
|
|
||||||
imageVector = null,
|
|
||||||
contentDescription = R.string.x_content_desc,
|
|
||||||
url = "https://www.x.com/acclorite"
|
|
||||||
),
|
|
||||||
Badge(
|
|
||||||
id = "reddit",
|
|
||||||
drawable = R.drawable.reddit,
|
|
||||||
imageVector = null,
|
|
||||||
contentDescription = R.string.reddit_content_desc,
|
|
||||||
url = "https://www.reddit.com/user/Acclorite/"
|
|
||||||
),
|
|
||||||
Badge(
|
|
||||||
id = "tryzub",
|
|
||||||
drawable = R.drawable.tryzub,
|
|
||||||
imageVector = null,
|
|
||||||
contentDescription = R.string.tryzub_content_desc,
|
|
||||||
url = null
|
|
||||||
),
|
|
||||||
Badge(
|
|
||||||
id = "github_project",
|
|
||||||
drawable = R.drawable.github,
|
|
||||||
imageVector = null,
|
|
||||||
contentDescription = R.string.github_project_content_desc,
|
|
||||||
url = "https://www.github.com/Acclorite/book-story"
|
|
||||||
),
|
|
||||||
Badge(
|
|
||||||
id = "github_profile",
|
|
||||||
drawable = null,
|
|
||||||
imageVector = Icons.Default.Person,
|
|
||||||
contentDescription = R.string.github_profile_content_desc,
|
|
||||||
url = "https://www.github.com/Acclorite"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Fonts for Reader
|
// Fonts for Reader
|
||||||
val FONTS = listOf(
|
val FONTS = provideFonts()
|
||||||
FontWithName(
|
|
||||||
"default",
|
// Empty Book (Used for Default Screen values)
|
||||||
UIText.StringResource(R.string.default_font),
|
val EMPTY_BOOK = provideEmptyBook()
|
||||||
FontFamily.Default
|
|
||||||
|
// Default Color Preset (Used when creating new color presets)
|
||||||
|
val DEFAULT_COLOR_PRESET = provideDefaultColorPreset()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun provideSupportedExtensions() = listOf(
|
||||||
|
".epub", ".pdf", ".fb2", ".txt", ".zip", ".html", ".htm"
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun provideLanguages() = listOf(
|
||||||
|
Pair("en", "English"),
|
||||||
|
Pair("uk", "Українська"),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun provideNavigationItems() = listOf(
|
||||||
|
NavigationItem(
|
||||||
|
screen = Screen.Library,
|
||||||
|
title = R.string.library_screen,
|
||||||
|
tooltip = R.string.library_content_desc,
|
||||||
|
selectedIcon = R.drawable.library_screen_filled,
|
||||||
|
unselectedIcon = R.drawable.library_screen_outlined
|
||||||
|
),
|
||||||
|
NavigationItem(
|
||||||
|
screen = Screen.History,
|
||||||
|
title = R.string.history_screen,
|
||||||
|
tooltip = R.string.history_content_desc,
|
||||||
|
selectedIcon = R.drawable.history_screen_filled,
|
||||||
|
unselectedIcon = R.drawable.history_screen_outlined
|
||||||
|
),
|
||||||
|
NavigationItem(
|
||||||
|
screen = Screen.Browse,
|
||||||
|
title = R.string.browse_screen,
|
||||||
|
tooltip = R.string.browse_content_desc,
|
||||||
|
selectedIcon = R.drawable.browse_screen_filled,
|
||||||
|
unselectedIcon = R.drawable.browse_screen_outlined
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun provideThemes() = listOf(
|
||||||
|
Pair(Theme.DYNAMIC, UIText.StringResource(R.string.dynamic_theme)),
|
||||||
|
Pair(Theme.BLUE, UIText.StringResource(R.string.blue_theme)),
|
||||||
|
Pair(Theme.GREEN, UIText.StringResource(R.string.green_theme)),
|
||||||
|
Pair(Theme.RED, UIText.StringResource(R.string.red_theme)),
|
||||||
|
Pair(Theme.PURPLE, UIText.StringResource(R.string.purple_theme)),
|
||||||
|
Pair(Theme.PINK, UIText.StringResource(R.string.pink_theme)),
|
||||||
|
Pair(Theme.YELLOW, UIText.StringResource(R.string.yellow_theme)),
|
||||||
|
Pair(Theme.AQUA, UIText.StringResource(R.string.aqua_theme)),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun provideCredits() = listOf(
|
||||||
|
Credit(
|
||||||
|
name = "Tachiyomi (Mihon)",
|
||||||
|
source = "GitHub",
|
||||||
|
credits = listOf(
|
||||||
|
UIText.StringResource(R.string.credits_design),
|
||||||
|
UIText.StringValue("Readme")
|
||||||
),
|
),
|
||||||
FontWithName(
|
website = "https://www.github.com/mihonapp/mihon"
|
||||||
"raleway",
|
),
|
||||||
UIText.StringValue("Raleway"),
|
Credit(
|
||||||
FontFamily(
|
name = "Kitsune",
|
||||||
Font(R.font.raleway_regular),
|
source = "GitHub",
|
||||||
Font(R.font.raleway_regular_italic, style = FontStyle.Italic)
|
credits = listOf(
|
||||||
)
|
UIText.StringResource(R.string.credits_updates)
|
||||||
),
|
),
|
||||||
FontWithName(
|
website = "https://www.github.com/Drumber/Kitsune"
|
||||||
"open_sans",
|
),
|
||||||
UIText.StringValue("Open Sans"),
|
Credit(
|
||||||
FontFamily(
|
name = "Material Design Icons",
|
||||||
Font(R.font.opensans_regular),
|
source = "Google Fonts",
|
||||||
Font(R.font.opensans_regular_italic, style = FontStyle.Italic)
|
credits = listOf(
|
||||||
)
|
UIText.StringResource(R.string.credits_icon)
|
||||||
),
|
),
|
||||||
FontWithName(
|
website = "https://fonts.google.com/icons"
|
||||||
"mulish",
|
),
|
||||||
UIText.StringValue("Mulish"),
|
)
|
||||||
FontFamily(
|
|
||||||
Font(R.font.mulish_regular),
|
private fun provideHelpTips() = listOf(
|
||||||
Font(R.font.mulish_regular_italic, style = FontStyle.Italic)
|
HelpTip(
|
||||||
)
|
title = R.string.help_title_how_to_find_books,
|
||||||
),
|
description = { _, _ ->
|
||||||
FontWithName(
|
append(stringResource(id = R.string.help_desc_how_to_find_books))
|
||||||
"arimo",
|
},
|
||||||
UIText.StringValue("Arimo"),
|
customContent = { onHelpEvent ->
|
||||||
FontFamily(
|
HelpFindBooksContent(onEvent = onHelpEvent)
|
||||||
Font(R.font.arimo_regular),
|
}
|
||||||
Font(R.font.arimo_regular_italic, style = FontStyle.Italic)
|
),
|
||||||
)
|
|
||||||
),
|
HelpTip(
|
||||||
FontWithName(
|
title = R.string.help_title_how_to_add_books,
|
||||||
"garamond",
|
description = { onNavigate, fromStart ->
|
||||||
UIText.StringValue("Garamond"),
|
append(stringResource(id = R.string.help_desc_how_to_add_books_1) + " ")
|
||||||
FontFamily(
|
|
||||||
Font(R.font.garamond_regular),
|
HelpAnnotation(
|
||||||
Font(R.font.garamond_regular_italic, style = FontStyle.Italic)
|
onClick = {
|
||||||
)
|
if (!fromStart) {
|
||||||
),
|
onNavigate {
|
||||||
FontWithName(
|
navigate(Screen.Browse, useBackAnimation = true)
|
||||||
"roboto_serif",
|
}
|
||||||
UIText.StringValue("Roboto Serif"),
|
}
|
||||||
FontFamily(
|
}
|
||||||
Font(R.font.robotoserif_regular),
|
) {
|
||||||
Font(R.font.robotoserif_regular_italic, style = FontStyle.Italic)
|
append(stringResource(id = R.string.help_desc_how_to_add_books_2))
|
||||||
)
|
}
|
||||||
),
|
append(". ")
|
||||||
FontWithName(
|
|
||||||
"noto_serif",
|
append(stringResource(id = R.string.help_desc_how_to_add_books_3) + " ")
|
||||||
UIText.StringValue("Noto Serif"),
|
|
||||||
FontFamily(
|
HelpAnnotation(
|
||||||
Font(R.font.notoserif_regular),
|
onClick = {
|
||||||
Font(R.font.notoserif_regular_italic, style = FontStyle.Italic)
|
if (!fromStart) {
|
||||||
)
|
onNavigate {
|
||||||
),
|
navigate(Screen.Library, useBackAnimation = true)
|
||||||
FontWithName(
|
}
|
||||||
"noto_sans",
|
}
|
||||||
UIText.StringValue("Noto Sans"),
|
}
|
||||||
FontFamily(
|
) {
|
||||||
Font(R.font.notosans_regular),
|
append(stringResource(id = R.string.help_desc_how_to_add_books_4))
|
||||||
Font(R.font.notosans_regular_italic, style = FontStyle.Italic)
|
}
|
||||||
)
|
append(".")
|
||||||
),
|
}
|
||||||
FontWithName(
|
),
|
||||||
"roboto",
|
|
||||||
UIText.StringValue("Roboto"),
|
HelpTip(
|
||||||
FontFamily(
|
title = R.string.help_title_how_to_customize_app,
|
||||||
Font(R.font.roboto_regular),
|
description = { onNavigate, fromStart ->
|
||||||
Font(R.font.roboto_regular_italic, style = FontStyle.Italic)
|
append(stringResource(id = R.string.help_desc_how_to_customize_app_1) + " ")
|
||||||
)
|
|
||||||
),
|
HelpAnnotation(
|
||||||
FontWithName(
|
onClick = {
|
||||||
"jost",
|
if (!fromStart) {
|
||||||
UIText.StringValue("Jost"),
|
onNavigate {
|
||||||
FontFamily(
|
navigate(Screen.Settings, useBackAnimation = true)
|
||||||
Font(R.font.jost_regular),
|
}
|
||||||
Font(R.font.jost_regular_italic, style = FontStyle.Italic)
|
}
|
||||||
)
|
}
|
||||||
),
|
) {
|
||||||
FontWithName(
|
append(stringResource(id = R.string.help_desc_how_to_customize_app_2))
|
||||||
"merriweather",
|
}
|
||||||
UIText.StringValue("Merriweather"),
|
append(". ")
|
||||||
FontFamily(
|
|
||||||
Font(R.font.merriweather_regular),
|
append(stringResource(id = R.string.help_desc_how_to_customize_app_3))
|
||||||
Font(R.font.merriweather_regular_italic, style = FontStyle.Italic)
|
}
|
||||||
)
|
),
|
||||||
),
|
|
||||||
FontWithName(
|
HelpTip(
|
||||||
"montserrat",
|
title = R.string.help_title_how_to_move_or_delete_books,
|
||||||
UIText.StringValue("Montserrat"),
|
description = { onNavigate, fromStart ->
|
||||||
FontFamily(
|
append(stringResource(id = R.string.help_desc_how_to_move_or_delete_books_1) + " ")
|
||||||
Font(R.font.montserrat_regular),
|
|
||||||
Font(R.font.montserrat_regular_italic, style = FontStyle.Italic)
|
HelpAnnotation(
|
||||||
)
|
onClick = {
|
||||||
),
|
if (!fromStart) {
|
||||||
FontWithName(
|
onNavigate {
|
||||||
"nunito",
|
navigate(Screen.Library, useBackAnimation = true)
|
||||||
UIText.StringValue("Nunito"),
|
}
|
||||||
FontFamily(
|
}
|
||||||
Font(R.font.nunito_regular),
|
}
|
||||||
Font(R.font.nunito_regular_italic, style = FontStyle.Italic)
|
) {
|
||||||
)
|
append(stringResource(id = R.string.help_desc_how_to_move_or_delete_books_2))
|
||||||
),
|
}
|
||||||
FontWithName(
|
append(". ")
|
||||||
"roboto_slab",
|
|
||||||
UIText.StringValue("Roboto Slab"),
|
append(stringResource(id = R.string.help_desc_how_to_move_or_delete_books_3))
|
||||||
FontFamily(
|
}
|
||||||
Font(R.font.robotoslab_regular)
|
),
|
||||||
)
|
|
||||||
),
|
HelpTip(
|
||||||
FontWithName(
|
title = R.string.help_title_how_to_edit_book,
|
||||||
"lora",
|
description = { onNavigate, fromStart ->
|
||||||
UIText.StringValue("Lora"),
|
append(stringResource(id = R.string.help_desc_how_to_edit_book_1) + " ")
|
||||||
FontFamily(
|
|
||||||
Font(R.font.lora_regular),
|
HelpAnnotation(
|
||||||
Font(R.font.lora_regular_italic, style = FontStyle.Italic)
|
onClick = {
|
||||||
)
|
if (!fromStart) {
|
||||||
|
onNavigate {
|
||||||
|
navigate(Screen.Library, useBackAnimation = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
append(stringResource(id = R.string.help_desc_how_to_edit_book_2))
|
||||||
|
}
|
||||||
|
append(" ")
|
||||||
|
|
||||||
|
append(stringResource(id = R.string.help_desc_how_to_edit_book_3))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
HelpTip(
|
||||||
|
title = R.string.help_title_how_to_read_book,
|
||||||
|
description = { onNavigate, fromStart ->
|
||||||
|
append(stringResource(id = R.string.help_desc_how_to_read_book_1) + " ")
|
||||||
|
|
||||||
|
HelpAnnotation(
|
||||||
|
onClick = {
|
||||||
|
if (!fromStart) {
|
||||||
|
onNavigate {
|
||||||
|
navigate(Screen.Library, useBackAnimation = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
append(stringResource(id = R.string.help_desc_how_to_read_book_2))
|
||||||
|
}
|
||||||
|
append(". ")
|
||||||
|
|
||||||
|
append(stringResource(id = R.string.help_desc_how_to_read_book_3))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
HelpTip(
|
||||||
|
title = R.string.help_title_how_to_customize_reader,
|
||||||
|
description = { onNavigate, fromStart ->
|
||||||
|
append(stringResource(id = R.string.help_desc_how_to_customize_reader_1) + " ")
|
||||||
|
|
||||||
|
HelpAnnotation(
|
||||||
|
onClick = {
|
||||||
|
if (!fromStart) {
|
||||||
|
onNavigate {
|
||||||
|
navigate(Screen.Settings, useBackAnimation = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
append(stringResource(id = R.string.help_desc_how_to_customize_reader_2))
|
||||||
|
}
|
||||||
|
append(" ")
|
||||||
|
|
||||||
|
append(stringResource(id = R.string.help_desc_how_to_customize_reader_3))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
HelpTip(
|
||||||
|
title = R.string.help_title_how_to_update_book,
|
||||||
|
description = { onNavigate, fromStart ->
|
||||||
|
append(stringResource(id = R.string.help_desc_how_to_update_book_1) + " ")
|
||||||
|
|
||||||
|
HelpAnnotation(
|
||||||
|
onClick = {
|
||||||
|
if (!fromStart) {
|
||||||
|
onNavigate {
|
||||||
|
navigate(Screen.Library, useBackAnimation = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
append(stringResource(id = R.string.help_desc_how_to_update_book_2))
|
||||||
|
}
|
||||||
|
append(" ")
|
||||||
|
|
||||||
|
append(stringResource(id = R.string.help_desc_how_to_update_book_3))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
HelpTip(
|
||||||
|
title = R.string.help_title_how_to_manage_history,
|
||||||
|
description = { onNavigate, fromStart ->
|
||||||
|
append(stringResource(id = R.string.help_desc_how_to_manage_history_1) + " ")
|
||||||
|
|
||||||
|
HelpAnnotation(
|
||||||
|
onClick = {
|
||||||
|
if (!fromStart) {
|
||||||
|
onNavigate {
|
||||||
|
navigate(Screen.History, useBackAnimation = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
append(stringResource(id = R.string.help_desc_how_to_manage_history_2))
|
||||||
|
}
|
||||||
|
append(". ")
|
||||||
|
|
||||||
|
append(stringResource(id = R.string.help_desc_how_to_manage_history_3))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
HelpTip(
|
||||||
|
title = R.string.help_title_how_to_use_tooltip,
|
||||||
|
description = { _, _ ->
|
||||||
|
append(stringResource(id = R.string.help_desc_how_to_use_tooltip_1))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
HelpTip(
|
||||||
|
title = R.string.help_title_how_to_use_double_click_translation,
|
||||||
|
description = { _, _ ->
|
||||||
|
append(stringResource(id = R.string.help_desc_how_to_use_double_click_translation_1))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
HelpTip(
|
||||||
|
title = R.string.help_title_how_to_create_color_presets,
|
||||||
|
description = { _, _ ->
|
||||||
|
append(stringResource(id = R.string.help_desc_how_to_create_color_presets_1))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
HelpTip(
|
||||||
|
title = R.string.help_title_how_to_use_color_presets,
|
||||||
|
description = { _, _ ->
|
||||||
|
append(stringResource(id = R.string.help_desc_how_to_use_color_presets_1))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun provideAboutBadges() = listOf(
|
||||||
|
Badge(
|
||||||
|
id = "x",
|
||||||
|
drawable = R.drawable.x_logo,
|
||||||
|
imageVector = null,
|
||||||
|
contentDescription = R.string.x_content_desc,
|
||||||
|
url = "https://www.x.com/acclorite"
|
||||||
|
),
|
||||||
|
Badge(
|
||||||
|
id = "reddit",
|
||||||
|
drawable = R.drawable.reddit,
|
||||||
|
imageVector = null,
|
||||||
|
contentDescription = R.string.reddit_content_desc,
|
||||||
|
url = "https://www.reddit.com/user/Acclorite/"
|
||||||
|
),
|
||||||
|
Badge(
|
||||||
|
id = "tryzub",
|
||||||
|
drawable = R.drawable.tryzub,
|
||||||
|
imageVector = null,
|
||||||
|
contentDescription = R.string.tryzub_content_desc,
|
||||||
|
url = null
|
||||||
|
),
|
||||||
|
Badge(
|
||||||
|
id = "github_project",
|
||||||
|
drawable = R.drawable.github,
|
||||||
|
imageVector = null,
|
||||||
|
contentDescription = R.string.github_project_content_desc,
|
||||||
|
url = "https://www.github.com/Acclorite/book-story"
|
||||||
|
),
|
||||||
|
Badge(
|
||||||
|
id = "github_profile",
|
||||||
|
drawable = null,
|
||||||
|
imageVector = Icons.Default.Person,
|
||||||
|
contentDescription = R.string.github_profile_content_desc,
|
||||||
|
url = "https://www.github.com/Acclorite"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun provideFonts() = listOf(
|
||||||
|
FontWithName(
|
||||||
|
"default",
|
||||||
|
UIText.StringResource(R.string.default_font),
|
||||||
|
FontFamily.Default
|
||||||
|
),
|
||||||
|
FontWithName(
|
||||||
|
"raleway",
|
||||||
|
UIText.StringValue("Raleway"),
|
||||||
|
FontFamily(
|
||||||
|
Font(R.font.raleway_regular),
|
||||||
|
Font(R.font.raleway_regular_italic, style = FontStyle.Italic)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
FontWithName(
|
||||||
|
"open_sans",
|
||||||
|
UIText.StringValue("Open Sans"),
|
||||||
|
FontFamily(
|
||||||
|
Font(R.font.opensans_regular),
|
||||||
|
Font(R.font.opensans_regular_italic, style = FontStyle.Italic)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
FontWithName(
|
||||||
|
"mulish",
|
||||||
|
UIText.StringValue("Mulish"),
|
||||||
|
FontFamily(
|
||||||
|
Font(R.font.mulish_regular),
|
||||||
|
Font(R.font.mulish_regular_italic, style = FontStyle.Italic)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
FontWithName(
|
||||||
|
"arimo",
|
||||||
|
UIText.StringValue("Arimo"),
|
||||||
|
FontFamily(
|
||||||
|
Font(R.font.arimo_regular),
|
||||||
|
Font(R.font.arimo_regular_italic, style = FontStyle.Italic)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
FontWithName(
|
||||||
|
"garamond",
|
||||||
|
UIText.StringValue("Garamond"),
|
||||||
|
FontFamily(
|
||||||
|
Font(R.font.garamond_regular),
|
||||||
|
Font(R.font.garamond_regular_italic, style = FontStyle.Italic)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
FontWithName(
|
||||||
|
"roboto_serif",
|
||||||
|
UIText.StringValue("Roboto Serif"),
|
||||||
|
FontFamily(
|
||||||
|
Font(R.font.robotoserif_regular),
|
||||||
|
Font(R.font.robotoserif_regular_italic, style = FontStyle.Italic)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
FontWithName(
|
||||||
|
"noto_serif",
|
||||||
|
UIText.StringValue("Noto Serif"),
|
||||||
|
FontFamily(
|
||||||
|
Font(R.font.notoserif_regular),
|
||||||
|
Font(R.font.notoserif_regular_italic, style = FontStyle.Italic)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
FontWithName(
|
||||||
|
"noto_sans",
|
||||||
|
UIText.StringValue("Noto Sans"),
|
||||||
|
FontFamily(
|
||||||
|
Font(R.font.notosans_regular),
|
||||||
|
Font(R.font.notosans_regular_italic, style = FontStyle.Italic)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
FontWithName(
|
||||||
|
"roboto",
|
||||||
|
UIText.StringValue("Roboto"),
|
||||||
|
FontFamily(
|
||||||
|
Font(R.font.roboto_regular),
|
||||||
|
Font(R.font.roboto_regular_italic, style = FontStyle.Italic)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
FontWithName(
|
||||||
|
"jost",
|
||||||
|
UIText.StringValue("Jost"),
|
||||||
|
FontFamily(
|
||||||
|
Font(R.font.jost_regular),
|
||||||
|
Font(R.font.jost_regular_italic, style = FontStyle.Italic)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
FontWithName(
|
||||||
|
"merriweather",
|
||||||
|
UIText.StringValue("Merriweather"),
|
||||||
|
FontFamily(
|
||||||
|
Font(R.font.merriweather_regular),
|
||||||
|
Font(R.font.merriweather_regular_italic, style = FontStyle.Italic)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
FontWithName(
|
||||||
|
"montserrat",
|
||||||
|
UIText.StringValue("Montserrat"),
|
||||||
|
FontFamily(
|
||||||
|
Font(R.font.montserrat_regular),
|
||||||
|
Font(R.font.montserrat_regular_italic, style = FontStyle.Italic)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
FontWithName(
|
||||||
|
"nunito",
|
||||||
|
UIText.StringValue("Nunito"),
|
||||||
|
FontFamily(
|
||||||
|
Font(R.font.nunito_regular),
|
||||||
|
Font(R.font.nunito_regular_italic, style = FontStyle.Italic)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
FontWithName(
|
||||||
|
"roboto_slab",
|
||||||
|
UIText.StringValue("Roboto Slab"),
|
||||||
|
FontFamily(
|
||||||
|
Font(R.font.robotoslab_regular)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
FontWithName(
|
||||||
|
"lora",
|
||||||
|
UIText.StringValue("Lora"),
|
||||||
|
FontFamily(
|
||||||
|
Font(R.font.lora_regular),
|
||||||
|
Font(R.font.lora_regular_italic, style = FontStyle.Italic)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
val EMPTY_BOOK = Book(
|
private fun provideEmptyBook() = Book(
|
||||||
id = -1,
|
id = -1,
|
||||||
title = "",
|
title = "",
|
||||||
author = UIText.StringValue(""),
|
author = UIText.StringValue(""),
|
||||||
description = null,
|
description = null,
|
||||||
textPath = "",
|
textPath = "",
|
||||||
filePath = "",
|
filePath = "",
|
||||||
coverImage = null,
|
coverImage = null,
|
||||||
scrollIndex = 0,
|
scrollIndex = 0,
|
||||||
scrollOffset = 0,
|
scrollOffset = 0,
|
||||||
progress = 0f,
|
progress = 0f,
|
||||||
lastOpened = null,
|
lastOpened = null,
|
||||||
category = Category.READING
|
category = Category.READING
|
||||||
)
|
)
|
||||||
|
|
||||||
val DEFAULT_COLOR_PRESET = ColorPreset(
|
private fun provideDefaultColorPreset() = ColorPreset(
|
||||||
id = -1,
|
id = -1,
|
||||||
name = null,
|
name = null,
|
||||||
backgroundColor = Color.DarkGray,
|
backgroundColor = Color.DarkGray,
|
||||||
fontColor = Color.LightGray,
|
fontColor = Color.LightGray,
|
||||||
isSelected = false
|
isSelected = false
|
||||||
)
|
)
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue