🚀 Storage Access Framework
* Per-folder storage access * Easily revoke per-folder access * No storage permission * Privacy focus (Milestone #5) Resolves: #38
This commit is contained in:
parent
beb118f234
commit
8cbe14b0e0
82 changed files with 1424 additions and 1588 deletions
|
|
@ -1,12 +1,6 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Check device for books -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<!-- Removing DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION, which is not used -->
|
||||
<uses-permission
|
||||
android:name="ua.acclorite.book_story.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
|
||||
|
|
@ -19,11 +13,9 @@
|
|||
android:icon="@mipmap/app_icon"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/app_icon_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Splash"
|
||||
android:usesCleartextTraffic="false"
|
||||
tools:ignore="DataExtractionRules"
|
||||
tools:targetApi="tiramisu">
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ class DocumentParser @Inject constructor(
|
|||
*
|
||||
* @return Parsed text line by line with Markdown(all lines are not blank).
|
||||
*/
|
||||
suspend fun Document.parseDocument(
|
||||
suspend fun parseDocument(
|
||||
document: Document,
|
||||
zipFile: ZipFile? = null,
|
||||
imageEntries: List<ZipEntry>? = null,
|
||||
includeChapter: Boolean = true
|
||||
|
|
@ -40,148 +41,150 @@ class DocumentParser @Inject constructor(
|
|||
val readerText = mutableListOf<ReaderText>()
|
||||
var chapterAdded = false
|
||||
|
||||
body().apply {
|
||||
// Remove manual line breaks from all <p>, <a>
|
||||
select("p").forEach { element ->
|
||||
yield()
|
||||
element.html(element.html().replace(Regex("\\n+"), " "))
|
||||
element.append("\n")
|
||||
}
|
||||
select("a").forEach { element ->
|
||||
yield()
|
||||
element.html(element.html().replace(Regex("\\n+"), ""))
|
||||
}
|
||||
|
||||
// Remove <head>'s title
|
||||
select("title").remove()
|
||||
|
||||
// Markdown
|
||||
select("hr").append("\n---\n")
|
||||
select("b").append("**").prepend("**")
|
||||
select("h1").append("**").prepend("**")
|
||||
select("h2").append("**").prepend("**")
|
||||
select("h3").append("**").prepend("**")
|
||||
select("strong").append("**").prepend("**")
|
||||
select("em").append("_").prepend("_")
|
||||
select("a").forEach { element ->
|
||||
var link = element.attr("href")
|
||||
if (!link.startsWith("http") || element.wholeText().isBlank()) return@forEach
|
||||
|
||||
if (link.startsWith("http://")) {
|
||||
link = link.replace("http://", "https://")
|
||||
document.selectFirst("body")
|
||||
.run { if (this == null) document.body() else this }
|
||||
.apply {
|
||||
// Remove manual line breaks from all <p>, <a>
|
||||
select("p").forEach { element ->
|
||||
yield()
|
||||
element.html(element.html().replace(Regex("\\n+"), " "))
|
||||
element.append("\n")
|
||||
}
|
||||
select("a").forEach { element ->
|
||||
yield()
|
||||
element.html(element.html().replace(Regex("\\n+"), ""))
|
||||
}
|
||||
|
||||
element.prepend("[")
|
||||
element.append("]($link)")
|
||||
}
|
||||
// Remove <head>'s title
|
||||
select("title").remove()
|
||||
|
||||
// Image (<img>)
|
||||
select("img").forEach { element ->
|
||||
val src = element.attr("src")
|
||||
.trim()
|
||||
.substringAfterLast(File.separator)
|
||||
.lowercase()
|
||||
.takeIf {
|
||||
it.containsVisibleText() && imageEntries?.any { image ->
|
||||
it == image.name.substringAfterLast(File.separator).lowercase()
|
||||
} == true
|
||||
} ?: return@forEach
|
||||
// Markdown
|
||||
select("hr").append("\n---\n")
|
||||
select("b").append("**").prepend("**")
|
||||
select("h1").append("**").prepend("**")
|
||||
select("h2").append("**").prepend("**")
|
||||
select("h3").append("**").prepend("**")
|
||||
select("strong").append("**").prepend("**")
|
||||
select("em").append("_").prepend("_")
|
||||
select("a").forEach { element ->
|
||||
var link = element.attr("href")
|
||||
if (!link.startsWith("http") || element.wholeText().isBlank()) return@forEach
|
||||
|
||||
val alt = element.attr("alt").trim().takeIf {
|
||||
it.clearMarkdown().containsVisibleText()
|
||||
} ?: src.substringBeforeLast(".")
|
||||
|
||||
element.append("\n[[$src|$alt]]\n")
|
||||
}
|
||||
|
||||
// Image (<image>)
|
||||
select("image").forEach { element ->
|
||||
val src = element.attr("xlink:href")
|
||||
.trim()
|
||||
.substringAfterLast(File.separator)
|
||||
.lowercase()
|
||||
.takeIf {
|
||||
it.containsVisibleText() && imageEntries?.any { image ->
|
||||
it == image.name.substringAfterLast(File.separator).lowercase()
|
||||
} == true
|
||||
} ?: return@forEach
|
||||
|
||||
val alt = src.substringBeforeLast(".")
|
||||
|
||||
element.append("\n[[$src|$alt]]\n")
|
||||
}
|
||||
}.wholeText().lines().forEach { line ->
|
||||
yield()
|
||||
|
||||
val formattedLine = line.replace(
|
||||
Regex("""\*\*\*\s*(.*?)\s*\*\*\*"""), "_**$1**_"
|
||||
).replace(
|
||||
Regex("""\*\*\s*(.*?)\s*\*\*"""), "**$1**"
|
||||
).replace(
|
||||
Regex("""_\s*(.*?)\s*_"""), "_$1_"
|
||||
).trim()
|
||||
|
||||
val imageRegex = Regex("""\[\[(.*?)\|(.*?)]]""")
|
||||
|
||||
if (line.containsVisibleText()) {
|
||||
when {
|
||||
imageRegex.matches(line) -> {
|
||||
val trimmedLine = line.removeSurrounding("[[", "]]")
|
||||
val src = trimmedLine.substringBefore("|")
|
||||
val alt = "_${trimmedLine.substringAfter("|")}_"
|
||||
|
||||
val image = try {
|
||||
val imageEntry = imageEntries?.find { image ->
|
||||
src == image.name.substringAfterLast(File.separator).lowercase()
|
||||
} ?: return@forEach
|
||||
|
||||
zipFile?.getImage(imageEntry)?.asImageBitmap()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
} ?: return@forEach
|
||||
|
||||
image.prepareToDraw()
|
||||
readerText.add( // Adding image
|
||||
ReaderText.Image(
|
||||
imageBitmap = image
|
||||
)
|
||||
)
|
||||
readerText.add( // Adding alternative text (caption) for image
|
||||
ReaderText.Text(
|
||||
markdownParser.parse(alt)
|
||||
)
|
||||
)
|
||||
if (link.startsWith("http://")) {
|
||||
link = link.replace("http://", "https://")
|
||||
}
|
||||
|
||||
line == "---" || line == "***" -> readerText.add(ReaderText.Separator)
|
||||
element.prepend("[")
|
||||
element.append("]($link)")
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (
|
||||
!chapterAdded &&
|
||||
formattedLine.clearAllMarkdown().containsVisibleText() &&
|
||||
includeChapter
|
||||
) {
|
||||
readerText.add(
|
||||
0, ReaderText.Chapter(
|
||||
title = formattedLine.clearAllMarkdown(),
|
||||
nested = false
|
||||
// Image (<img>)
|
||||
select("img").forEach { element ->
|
||||
val src = element.attr("src")
|
||||
.trim()
|
||||
.substringAfterLast(File.separator)
|
||||
.lowercase()
|
||||
.takeIf {
|
||||
it.containsVisibleText() && imageEntries?.any { image ->
|
||||
it == image.name.substringAfterLast(File.separator).lowercase()
|
||||
} == true
|
||||
} ?: return@forEach
|
||||
|
||||
val alt = element.attr("alt").trim().takeIf {
|
||||
it.clearMarkdown().containsVisibleText()
|
||||
} ?: src.substringBeforeLast(".")
|
||||
|
||||
element.append("\n[[$src|$alt]]\n")
|
||||
}
|
||||
|
||||
// Image (<image>)
|
||||
select("image").forEach { element ->
|
||||
val src = element.attr("xlink:href")
|
||||
.trim()
|
||||
.substringAfterLast(File.separator)
|
||||
.lowercase()
|
||||
.takeIf {
|
||||
it.containsVisibleText() && imageEntries?.any { image ->
|
||||
it == image.name.substringAfterLast(File.separator).lowercase()
|
||||
} == true
|
||||
} ?: return@forEach
|
||||
|
||||
val alt = src.substringBeforeLast(".")
|
||||
|
||||
element.append("\n[[$src|$alt]]\n")
|
||||
}
|
||||
}.wholeText().lines().forEach { line ->
|
||||
yield()
|
||||
|
||||
val formattedLine = line.replace(
|
||||
Regex("""\*\*\*\s*(.*?)\s*\*\*\*"""), "_**$1**_"
|
||||
).replace(
|
||||
Regex("""\*\*\s*(.*?)\s*\*\*"""), "**$1**"
|
||||
).replace(
|
||||
Regex("""_\s*(.*?)\s*_"""), "_$1_"
|
||||
).trim()
|
||||
|
||||
val imageRegex = Regex("""\[\[(.*?)\|(.*?)]]""")
|
||||
|
||||
if (line.containsVisibleText()) {
|
||||
when {
|
||||
imageRegex.matches(line) -> {
|
||||
val trimmedLine = line.removeSurrounding("[[", "]]")
|
||||
val src = trimmedLine.substringBefore("|")
|
||||
val alt = "_${trimmedLine.substringAfter("|")}_"
|
||||
|
||||
val image = try {
|
||||
val imageEntry = imageEntries?.find { image ->
|
||||
src == image.name.substringAfterLast(File.separator).lowercase()
|
||||
} ?: return@forEach
|
||||
|
||||
zipFile?.getImage(imageEntry)?.asImageBitmap()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
} ?: return@forEach
|
||||
|
||||
image.prepareToDraw()
|
||||
readerText.add( // Adding image
|
||||
ReaderText.Image(
|
||||
imageBitmap = image
|
||||
)
|
||||
)
|
||||
chapterAdded = true
|
||||
} else if (
|
||||
formattedLine.clearMarkdown().containsVisibleText()
|
||||
) {
|
||||
readerText.add(
|
||||
readerText.add( // Adding alternative text (caption) for image
|
||||
ReaderText.Text(
|
||||
line = markdownParser.parse(formattedLine)
|
||||
markdownParser.parse(alt)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
line == "---" || line == "***" -> readerText.add(ReaderText.Separator)
|
||||
|
||||
else -> {
|
||||
if (
|
||||
!chapterAdded &&
|
||||
formattedLine.clearAllMarkdown().containsVisibleText() &&
|
||||
includeChapter
|
||||
) {
|
||||
readerText.add(
|
||||
0, ReaderText.Chapter(
|
||||
title = formattedLine.clearAllMarkdown(),
|
||||
nested = false
|
||||
)
|
||||
)
|
||||
chapterAdded = true
|
||||
} else if (
|
||||
formattedLine.clearMarkdown().containsVisibleText()
|
||||
) {
|
||||
readerText.add(
|
||||
ReaderText.Text(
|
||||
line = markdownParser.parse(formattedLine)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yield()
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
package ua.acclorite.book_story.data.parser
|
||||
|
||||
import ua.acclorite.book_story.domain.file.CachedFile
|
||||
import ua.acclorite.book_story.domain.library.book.BookWithCover
|
||||
import java.io.File
|
||||
|
||||
|
||||
interface FileParser {
|
||||
|
||||
suspend fun parse(file: File): BookWithCover?
|
||||
suspend fun parse(cachedFile: CachedFile): BookWithCover?
|
||||
}
|
||||
|
|
@ -12,8 +12,8 @@ import ua.acclorite.book_story.data.parser.fb2.Fb2FileParser
|
|||
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.txt.TxtFileParser
|
||||
import ua.acclorite.book_story.domain.file.CachedFile
|
||||
import ua.acclorite.book_story.domain.library.book.BookWithCover
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val FILE_PARSER = "File Parser"
|
||||
|
|
@ -25,40 +25,41 @@ class FileParserImpl @Inject constructor(
|
|||
private val fb2FileParser: Fb2FileParser,
|
||||
private val htmlFileParser: HtmlFileParser,
|
||||
) : FileParser {
|
||||
override suspend fun parse(file: File): BookWithCover? {
|
||||
if (!file.exists()) {
|
||||
Log.e(FILE_PARSER, "File does not exist.")
|
||||
|
||||
override suspend fun parse(cachedFile: CachedFile): BookWithCover? {
|
||||
if (!cachedFile.canAccess()) {
|
||||
Log.e(FILE_PARSER, "File does not exist or no read access is granted.")
|
||||
return null
|
||||
}
|
||||
|
||||
val fileFormat = ".${file.extension}".lowercase().trim()
|
||||
val fileFormat = ".${cachedFile.name.substringAfterLast(".")}".lowercase().trim()
|
||||
return when (fileFormat) {
|
||||
".pdf" -> {
|
||||
pdfFileParser.parse(file)
|
||||
pdfFileParser.parse(cachedFile)
|
||||
}
|
||||
|
||||
".epub" -> {
|
||||
epubFileParser.parse(file)
|
||||
epubFileParser.parse(cachedFile)
|
||||
}
|
||||
|
||||
".txt" -> {
|
||||
txtFileParser.parse(file)
|
||||
txtFileParser.parse(cachedFile)
|
||||
}
|
||||
|
||||
".fb2" -> {
|
||||
fb2FileParser.parse(file)
|
||||
fb2FileParser.parse(cachedFile)
|
||||
}
|
||||
|
||||
".html" -> {
|
||||
htmlFileParser.parse(file)
|
||||
htmlFileParser.parse(cachedFile)
|
||||
}
|
||||
|
||||
".htm" -> {
|
||||
htmlFileParser.parse(file)
|
||||
htmlFileParser.parse(cachedFile)
|
||||
}
|
||||
|
||||
".md" -> {
|
||||
txtFileParser.parse(file)
|
||||
txtFileParser.parse(cachedFile)
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
|
@ -67,12 +68,4 @@ class FileParserImpl @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -6,9 +6,10 @@
|
|||
|
||||
package ua.acclorite.book_story.data.parser
|
||||
|
||||
import ua.acclorite.book_story.domain.file.CachedFile
|
||||
import ua.acclorite.book_story.domain.reader.ReaderText
|
||||
import java.io.File
|
||||
|
||||
interface TextParser {
|
||||
suspend fun parse(file: File): List<ReaderText>
|
||||
|
||||
suspend fun parse(cachedFile: CachedFile): List<ReaderText>
|
||||
}
|
||||
|
|
@ -10,12 +10,11 @@ import android.util.Log
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
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.html.HtmlTextParser
|
||||
import ua.acclorite.book_story.data.parser.pdf.PdfTextParser
|
||||
import ua.acclorite.book_story.data.parser.txt.TxtTextParser
|
||||
import ua.acclorite.book_story.domain.file.CachedFile
|
||||
import ua.acclorite.book_story.domain.reader.ReaderText
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TEXT_PARSER = "Text Parser"
|
||||
|
|
@ -24,47 +23,47 @@ class TextParserImpl @Inject constructor(
|
|||
// Markdown parser (Markdown)
|
||||
private val txtTextParser: TxtTextParser,
|
||||
private val pdfTextParser: PdfTextParser,
|
||||
private val fb2TextParser: Fb2TextParser,
|
||||
|
||||
// Document parser (HTML+Markdown)
|
||||
private val epubTextParser: EpubTextParser,
|
||||
private val htmlTextParser: HtmlTextParser
|
||||
) : TextParser {
|
||||
override suspend fun parse(file: File): List<ReaderText> {
|
||||
if (!file.exists()) {
|
||||
Log.e(TEXT_PARSER, "File does not exist.")
|
||||
|
||||
override suspend fun parse(cachedFile: CachedFile): List<ReaderText> {
|
||||
if (!cachedFile.canAccess()) {
|
||||
Log.e(TEXT_PARSER, "File does not exist or no read access is granted.")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val fileFormat = ".${file.extension}".lowercase().trim()
|
||||
val fileFormat = ".${cachedFile.name.substringAfterLast(".")}".lowercase().trim()
|
||||
return withContext(Dispatchers.IO) {
|
||||
when (fileFormat) {
|
||||
".pdf" -> {
|
||||
pdfTextParser.parse(file)
|
||||
pdfTextParser.parse(cachedFile)
|
||||
}
|
||||
|
||||
".epub" -> {
|
||||
epubTextParser.parse(file)
|
||||
epubTextParser.parse(cachedFile)
|
||||
}
|
||||
|
||||
".txt" -> {
|
||||
txtTextParser.parse(file)
|
||||
txtTextParser.parse(cachedFile)
|
||||
}
|
||||
|
||||
".fb2" -> {
|
||||
fb2TextParser.parse(file)
|
||||
htmlTextParser.parse(cachedFile)
|
||||
}
|
||||
|
||||
".html" -> {
|
||||
htmlTextParser.parse(file)
|
||||
htmlTextParser.parse(cachedFile)
|
||||
}
|
||||
|
||||
".htm" -> {
|
||||
htmlTextParser.parse(file)
|
||||
htmlTextParser.parse(cachedFile)
|
||||
}
|
||||
|
||||
".md" -> {
|
||||
htmlTextParser.parse(file)
|
||||
htmlTextParser.parse(cachedFile)
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ 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.file.CachedFile
|
||||
import ua.acclorite.book_story.domain.library.book.Book
|
||||
import ua.acclorite.book_story.domain.library.book.BookWithCover
|
||||
import ua.acclorite.book_story.domain.library.category.Category
|
||||
|
|
@ -23,12 +24,15 @@ import javax.inject.Inject
|
|||
|
||||
class EpubFileParser @Inject constructor() : FileParser {
|
||||
|
||||
override suspend fun parse(file: File): BookWithCover? {
|
||||
override suspend fun parse(cachedFile: CachedFile): BookWithCover? {
|
||||
return try {
|
||||
var book: BookWithCover? = null
|
||||
|
||||
val rawFile = cachedFile.rawFile
|
||||
if (rawFile == null || !rawFile.exists() || !rawFile.canRead()) return null
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
ZipFile(file).use { zip ->
|
||||
ZipFile(rawFile).use { zip ->
|
||||
val opfEntry = zip.entries().asSequence().find { entry ->
|
||||
entry.name.endsWith(".opf", ignoreCase = true)
|
||||
} ?: return@withContext
|
||||
|
|
@ -41,7 +45,7 @@ class EpubFileParser @Inject constructor() : FileParser {
|
|||
|
||||
val title = document.select("metadata > dc|title").text().trim().run {
|
||||
ifBlank {
|
||||
file.nameWithoutExtension.trim()
|
||||
cachedFile.name.substringBeforeLast(".").trim()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,12 +89,12 @@ class EpubFileParser @Inject constructor() : FileParser {
|
|||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
filePath = cachedFile.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
),
|
||||
coverImage = extractCoverImageBitmap(file, coverImage)
|
||||
coverImage = extractCoverImageBitmap(rawFile, coverImage)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import kotlinx.coroutines.yield
|
|||
import org.jsoup.Jsoup
|
||||
import ua.acclorite.book_story.data.parser.DocumentParser
|
||||
import ua.acclorite.book_story.data.parser.TextParser
|
||||
import ua.acclorite.book_story.domain.file.CachedFile
|
||||
import ua.acclorite.book_story.domain.reader.ReaderText
|
||||
import ua.acclorite.book_story.presentation.core.constants.Constants
|
||||
import ua.acclorite.book_story.presentation.core.constants.provideImageExtensions
|
||||
|
|
@ -40,15 +41,18 @@ class EpubTextParser @Inject constructor(
|
|||
private val documentParser: DocumentParser
|
||||
) : TextParser {
|
||||
|
||||
override suspend fun parse(file: File): List<ReaderText> {
|
||||
Log.i(EPUB_TAG, "Started EPUB parsing: ${file.name}.")
|
||||
override suspend fun parse(cachedFile: CachedFile): List<ReaderText> {
|
||||
Log.i(EPUB_TAG, "Started EPUB parsing: ${cachedFile.name}.")
|
||||
|
||||
return try {
|
||||
yield()
|
||||
var readerText = listOf<ReaderText>()
|
||||
|
||||
val rawFile = cachedFile.rawFile
|
||||
if (rawFile == null || !rawFile.exists() || !rawFile.canRead()) return emptyList()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
ZipFile(file).use { zip ->
|
||||
ZipFile(rawFile).use { zip ->
|
||||
val tocEntry = zip.entries().toList().find { entry ->
|
||||
entry.name.endsWith(".ncx", ignoreCase = true)
|
||||
}
|
||||
|
|
@ -163,13 +167,12 @@ class EpubTextParser @Inject constructor(
|
|||
) {
|
||||
// Getting all text
|
||||
val content = zip.getInputStream(entry).bufferedReader().use { it.readText() }
|
||||
var readerText = documentParser.run {
|
||||
Jsoup.parse(content).parseDocument(
|
||||
zipFile = zip,
|
||||
imageEntries = imageEntries,
|
||||
includeChapter = false
|
||||
)
|
||||
}.toMutableList()
|
||||
var readerText = documentParser.parseDocument(
|
||||
document = Jsoup.parse(content),
|
||||
zipFile = zip,
|
||||
imageEntries = imageEntries,
|
||||
includeChapter = false
|
||||
).toMutableList()
|
||||
|
||||
// Adding chapter title from TOC if found
|
||||
getChapterTitleFromToc(
|
||||
|
|
|
|||
|
|
@ -6,65 +6,55 @@
|
|||
|
||||
package ua.acclorite.book_story.data.parser.fb2
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.parser.Parser
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.data.parser.FileParser
|
||||
import ua.acclorite.book_story.domain.file.CachedFile
|
||||
import ua.acclorite.book_story.domain.library.book.Book
|
||||
import ua.acclorite.book_story.domain.library.book.BookWithCover
|
||||
import ua.acclorite.book_story.domain.library.category.Category
|
||||
import ua.acclorite.book_story.domain.ui.UIText
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
|
||||
class Fb2FileParser @Inject constructor() : FileParser {
|
||||
|
||||
override suspend fun parse(file: File): BookWithCover? {
|
||||
override suspend fun parse(cachedFile: CachedFile): BookWithCover? {
|
||||
return try {
|
||||
val factory = DocumentBuilderFactory.newInstance()
|
||||
val builder = factory.newDocumentBuilder()
|
||||
val document = withContext(Dispatchers.IO) {
|
||||
builder.parse(file)
|
||||
val document = cachedFile.openInputStream()?.use {
|
||||
Jsoup.parse(it, null, "", Parser.xmlParser())
|
||||
}
|
||||
|
||||
val titleFromFile = extractElementContent(document, "book-title")
|
||||
val title = titleFromFile ?: file.nameWithoutExtension.trim()
|
||||
|
||||
val authorFirstName = extractElementContent(document, "first-name")
|
||||
val authorLastName = extractElementContent(document, "last-name")
|
||||
val authorFromFile = StringBuilder()
|
||||
|
||||
if (authorFirstName != null) {
|
||||
authorFromFile.append(
|
||||
"$authorFirstName "
|
||||
)
|
||||
}
|
||||
if (authorLastName != null) {
|
||||
authorFromFile.append(
|
||||
authorLastName
|
||||
)
|
||||
val title = document?.selectFirst("book-title")?.text()?.trim().run {
|
||||
if (isNullOrBlank()) {
|
||||
return@run cachedFile.name.substringBeforeLast(".").trim()
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
val author = if (authorFromFile.isNotBlank()) {
|
||||
UIText.StringValue(authorFromFile.toString().trim())
|
||||
} else {
|
||||
UIText.StringResource(R.string.unknown_author)
|
||||
val author = document?.selectFirst("author")?.text()?.trim().run {
|
||||
if (isNullOrBlank()) {
|
||||
return@run UIText.StringResource(R.string.unknown_author)
|
||||
}
|
||||
UIText.StringValue(this.trim())
|
||||
}
|
||||
|
||||
val descriptionFromFile = extractElementContent(document, "annotation")
|
||||
val description = document?.selectFirst("annotation")?.text()?.trim().run {
|
||||
if (isNullOrBlank()) {
|
||||
return@run null
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
BookWithCover(
|
||||
book = Book(
|
||||
title = title,
|
||||
author = author,
|
||||
description = descriptionFromFile,
|
||||
description = description,
|
||||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
filePath = cachedFile.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
|
|
@ -76,13 +66,4 @@ class Fb2FileParser @Inject constructor() : FileParser {
|
|||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractElementContent(document: Document, tagName: String): String? {
|
||||
val nodeList = document.getElementsByTagName(tagName)
|
||||
if (nodeList.length > 0) {
|
||||
val element = nodeList.item(0) as Element
|
||||
return element.textContent.trim()
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,168 +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.parser.fb2
|
||||
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.yield
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.NodeList
|
||||
import ua.acclorite.book_story.data.parser.MarkdownParser
|
||||
import ua.acclorite.book_story.data.parser.TextParser
|
||||
import ua.acclorite.book_story.domain.reader.ReaderText
|
||||
import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
|
||||
private const val FB2_TAG = "FB2 Parser"
|
||||
|
||||
class Fb2TextParser @Inject constructor(
|
||||
private val markdownParser: MarkdownParser
|
||||
) : TextParser {
|
||||
|
||||
override suspend fun parse(file: File): List<ReaderText> {
|
||||
Log.i(FB2_TAG, "Started FB2 parsing: ${file.name}.")
|
||||
|
||||
return try {
|
||||
val factory = DocumentBuilderFactory.newInstance()
|
||||
val builder = factory.newDocumentBuilder()
|
||||
val document = withContext(Dispatchers.IO) {
|
||||
builder.parse(file)
|
||||
}
|
||||
|
||||
val readerText = mutableListOf<ReaderText>()
|
||||
|
||||
val bodyNodes = document.getElementsByTagName("body")
|
||||
if (bodyNodes.length == 0) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
yield()
|
||||
|
||||
val unformattedLines = mutableListOf<String>()
|
||||
val bodyNode = bodyNodes.item(0) as Element
|
||||
val paragraphNodes = bodyNode.getElementsByTagName("p")
|
||||
|
||||
for (element in paragraphNodes.asList()) {
|
||||
yield()
|
||||
|
||||
if (element.textContent.isBlank()) {
|
||||
continue
|
||||
}
|
||||
|
||||
unformattedLines.add(
|
||||
element.textContent.trim()
|
||||
)
|
||||
}
|
||||
|
||||
yield()
|
||||
|
||||
val lines = mutableListOf<String>()
|
||||
unformattedLines.forEachIndexed { index, string ->
|
||||
try {
|
||||
yield()
|
||||
|
||||
val line = string.trim()
|
||||
|
||||
if (index == 0) {
|
||||
lines.add(line)
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
if (line.all { it.isDigit() }) {
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
if (line.first().isLowerCase()) {
|
||||
val currentLine = lines[lines.lastIndex]
|
||||
|
||||
if (currentLine.last() == '-') {
|
||||
if (currentLine[currentLine.lastIndex - 1].isLowerCase()) {
|
||||
lines[lines.lastIndex] = currentLine.dropLast(1) + line
|
||||
return@forEachIndexed
|
||||
}
|
||||
}
|
||||
|
||||
lines[lines.lastIndex] += " $line"
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
if (line.first().isUpperCase() || line.first().isDigit()) {
|
||||
lines.add(line)
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
if (line.first().isLetter()) {
|
||||
lines[lines.lastIndex] += " $line"
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return@forEachIndexed
|
||||
}
|
||||
}
|
||||
|
||||
yield()
|
||||
|
||||
var chapterAdded = false
|
||||
lines.forEach { line ->
|
||||
yield()
|
||||
|
||||
if (line.isNotBlank()) {
|
||||
when (line) {
|
||||
"***", "---" -> readerText.add(
|
||||
ReaderText.Separator
|
||||
)
|
||||
|
||||
else -> {
|
||||
if (!chapterAdded && line.clearAllMarkdown().isNotBlank()) {
|
||||
readerText.add(
|
||||
0, ReaderText.Chapter(
|
||||
title = line.clearAllMarkdown(),
|
||||
nested = false
|
||||
)
|
||||
)
|
||||
chapterAdded = true
|
||||
} else readerText.add(
|
||||
ReaderText.Text(
|
||||
line = markdownParser.parse(line)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yield()
|
||||
|
||||
if (
|
||||
readerText.filterIsInstance<ReaderText.Text>().isEmpty() ||
|
||||
readerText.filterIsInstance<ReaderText.Chapter>().isEmpty()
|
||||
) {
|
||||
Log.e(FB2_TAG, "Could not extract text from FB2.")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
Log.i(FB2_TAG, "Successfully finished FB2 parsing.")
|
||||
readerText
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun NodeList.asList(): List<Element> {
|
||||
val list = mutableListOf<Element>()
|
||||
for (i in 0 until this.length) {
|
||||
list.add(this.item(i) as Element)
|
||||
}
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
|
@ -7,25 +7,29 @@
|
|||
package ua.acclorite.book_story.data.parser.html
|
||||
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.parser.Parser
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.data.parser.FileParser
|
||||
import ua.acclorite.book_story.domain.file.CachedFile
|
||||
import ua.acclorite.book_story.domain.library.book.Book
|
||||
import ua.acclorite.book_story.domain.library.book.BookWithCover
|
||||
import ua.acclorite.book_story.domain.library.category.Category
|
||||
import ua.acclorite.book_story.domain.ui.UIText
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class HtmlFileParser @Inject constructor() : FileParser {
|
||||
|
||||
override suspend fun parse(file: File): BookWithCover? {
|
||||
override suspend fun parse(cachedFile: CachedFile): BookWithCover? {
|
||||
return try {
|
||||
val document = Jsoup.parse(file)
|
||||
val document = cachedFile.openInputStream()?.use {
|
||||
Jsoup.parse(it, null, "", Parser.htmlParser())
|
||||
}
|
||||
|
||||
val title = document.select("head > title").text().trim().run {
|
||||
ifBlank {
|
||||
file.nameWithoutExtension.trim()
|
||||
val title = document?.select("head > title")?.text()?.trim().run {
|
||||
if (isNullOrBlank()) {
|
||||
return@run cachedFile.name.substringBeforeLast(".").trim()
|
||||
}
|
||||
return@run this
|
||||
}
|
||||
|
||||
BookWithCover(
|
||||
|
|
@ -36,7 +40,7 @@ class HtmlFileParser @Inject constructor() : FileParser {
|
|||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
filePath = cachedFile.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
|
|
|
|||
|
|
@ -9,10 +9,11 @@ package ua.acclorite.book_story.data.parser.html
|
|||
import android.util.Log
|
||||
import kotlinx.coroutines.yield
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.parser.Parser
|
||||
import ua.acclorite.book_story.data.parser.DocumentParser
|
||||
import ua.acclorite.book_story.data.parser.TextParser
|
||||
import ua.acclorite.book_story.domain.file.CachedFile
|
||||
import ua.acclorite.book_story.domain.reader.ReaderText
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val HTML_TAG = "HTML Parser"
|
||||
|
|
@ -21,17 +22,18 @@ class HtmlTextParser @Inject constructor(
|
|||
private val documentParser: DocumentParser
|
||||
) : TextParser {
|
||||
|
||||
override suspend fun parse(file: File): List<ReaderText> {
|
||||
Log.i(HTML_TAG, "Started HTML parsing: ${file.name}.")
|
||||
override suspend fun parse(cachedFile: CachedFile): List<ReaderText> {
|
||||
Log.i(HTML_TAG, "Started HTML parsing: ${cachedFile.name}.")
|
||||
|
||||
return try {
|
||||
val readerText = documentParser.run {
|
||||
Jsoup.parse(file).parseDocument()
|
||||
val readerText = cachedFile.openInputStream()?.use { stream ->
|
||||
documentParser.parseDocument(Jsoup.parse(stream, null, "", Parser.htmlParser()))
|
||||
}
|
||||
|
||||
yield()
|
||||
|
||||
if (
|
||||
readerText.isNullOrEmpty() ||
|
||||
readerText.filterIsInstance<ReaderText.Text>().isEmpty() ||
|
||||
readerText.filterIsInstance<ReaderText.Chapter>().isEmpty()
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -11,24 +11,24 @@ import com.tom_roush.pdfbox.android.PDFBoxResourceLoader
|
|||
import com.tom_roush.pdfbox.pdmodel.PDDocument
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.data.parser.FileParser
|
||||
import ua.acclorite.book_story.domain.file.CachedFile
|
||||
import ua.acclorite.book_story.domain.library.book.Book
|
||||
import ua.acclorite.book_story.domain.library.book.BookWithCover
|
||||
import ua.acclorite.book_story.domain.library.category.Category
|
||||
import ua.acclorite.book_story.domain.ui.UIText
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class PdfFileParser @Inject constructor(
|
||||
private val application: Application
|
||||
) : FileParser {
|
||||
|
||||
override suspend fun parse(file: File): BookWithCover? {
|
||||
override suspend fun parse(cachedFile: CachedFile): BookWithCover? {
|
||||
return try {
|
||||
PDFBoxResourceLoader.init(application)
|
||||
val document = PDDocument.load(cachedFile.openInputStream())
|
||||
|
||||
val document = PDDocument.load(file)
|
||||
|
||||
val title = document.documentInformation.title ?: file.nameWithoutExtension.trim()
|
||||
val title = document.documentInformation.title
|
||||
?: cachedFile.name.substringBeforeLast(".").trim()
|
||||
val author = document.documentInformation.author.run {
|
||||
if (isNullOrBlank()) UIText.StringResource(R.string.unknown_author)
|
||||
else UIText.StringValue(this)
|
||||
|
|
@ -45,7 +45,7 @@ class PdfFileParser @Inject constructor(
|
|||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
filePath = cachedFile.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ import com.tom_roush.pdfbox.text.PDFTextStripper
|
|||
import kotlinx.coroutines.yield
|
||||
import ua.acclorite.book_story.data.parser.MarkdownParser
|
||||
import ua.acclorite.book_story.data.parser.TextParser
|
||||
import ua.acclorite.book_story.domain.file.CachedFile
|
||||
import ua.acclorite.book_story.domain.reader.ReaderText
|
||||
import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val PDF_TAG = "PDF Parser"
|
||||
|
|
@ -26,8 +26,8 @@ class PdfTextParser @Inject constructor(
|
|||
private val application: Application
|
||||
) : TextParser {
|
||||
|
||||
override suspend fun parse(file: File): List<ReaderText> {
|
||||
Log.i(PDF_TAG, "Started PDF parsing: ${file.name}.")
|
||||
override suspend fun parse(cachedFile: CachedFile): List<ReaderText> {
|
||||
Log.i(PDF_TAG, "Started PDF parsing: ${cachedFile.name}.")
|
||||
|
||||
return try {
|
||||
yield()
|
||||
|
|
@ -41,7 +41,7 @@ class PdfTextParser @Inject constructor(
|
|||
val pdfStripper = PDFTextStripper()
|
||||
pdfStripper.paragraphStart = "</br>"
|
||||
|
||||
PDDocument.load(file).use {
|
||||
PDDocument.load(cachedFile.openInputStream()).use {
|
||||
oldText = pdfStripper.getText(it)
|
||||
.replace("\r", "")
|
||||
}
|
||||
|
|
@ -160,20 +160,4 @@ class PdfTextParser @Inject constructor(
|
|||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -8,18 +8,18 @@ package ua.acclorite.book_story.data.parser.txt
|
|||
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.data.parser.FileParser
|
||||
import ua.acclorite.book_story.domain.file.CachedFile
|
||||
import ua.acclorite.book_story.domain.library.book.Book
|
||||
import ua.acclorite.book_story.domain.library.book.BookWithCover
|
||||
import ua.acclorite.book_story.domain.library.category.Category
|
||||
import ua.acclorite.book_story.domain.ui.UIText
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class TxtFileParser @Inject constructor() : FileParser {
|
||||
|
||||
override suspend fun parse(file: File): BookWithCover? {
|
||||
override suspend fun parse(cachedFile: CachedFile): BookWithCover? {
|
||||
return try {
|
||||
val title = file.nameWithoutExtension.trim()
|
||||
val title = cachedFile.name.substringBeforeLast(".").trim()
|
||||
val author = UIText.StringResource(R.string.unknown_author)
|
||||
|
||||
BookWithCover(
|
||||
|
|
@ -30,7 +30,7 @@ class TxtFileParser @Inject constructor() : FileParser {
|
|||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
progress = 0f,
|
||||
filePath = file.path,
|
||||
filePath = cachedFile.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
|
|
|
|||
|
|
@ -12,11 +12,9 @@ import kotlinx.coroutines.withContext
|
|||
import kotlinx.coroutines.yield
|
||||
import ua.acclorite.book_story.data.parser.MarkdownParser
|
||||
import ua.acclorite.book_story.data.parser.TextParser
|
||||
import ua.acclorite.book_story.domain.file.CachedFile
|
||||
import ua.acclorite.book_story.domain.reader.ReaderText
|
||||
import ua.acclorite.book_story.presentation.core.util.clearAllMarkdown
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileReader
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TXT_TAG = "TXT Parser"
|
||||
|
|
@ -25,35 +23,37 @@ class TxtTextParser @Inject constructor(
|
|||
private val markdownParser: MarkdownParser
|
||||
) : TextParser {
|
||||
|
||||
override suspend fun parse(file: File): List<ReaderText> {
|
||||
Log.i(TXT_TAG, "Started TXT parsing: ${file.name}.")
|
||||
override suspend fun parse(cachedFile: CachedFile): List<ReaderText> {
|
||||
Log.i(TXT_TAG, "Started TXT parsing: ${cachedFile.name}.")
|
||||
|
||||
return try {
|
||||
val readerText = mutableListOf<ReaderText>()
|
||||
var chapterAdded = false
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
BufferedReader(FileReader(file)).forEachLine { line ->
|
||||
if (line.isNotBlank()) {
|
||||
when (line) {
|
||||
"***", "---" -> readerText.add(
|
||||
ReaderText.Separator
|
||||
)
|
||||
cachedFile.openInputStream()?.bufferedReader()?.use { reader ->
|
||||
reader.forEachLine { line ->
|
||||
if (line.isNotBlank()) {
|
||||
when (line) {
|
||||
"***", "---" -> readerText.add(
|
||||
ReaderText.Separator
|
||||
)
|
||||
|
||||
else -> {
|
||||
if (!chapterAdded && line.clearAllMarkdown().isNotBlank()) {
|
||||
readerText.add(
|
||||
0, ReaderText.Chapter(
|
||||
title = line.clearAllMarkdown(),
|
||||
nested = false
|
||||
else -> {
|
||||
if (!chapterAdded && line.clearAllMarkdown().isNotBlank()) {
|
||||
readerText.add(
|
||||
0, ReaderText.Chapter(
|
||||
title = line.clearAllMarkdown(),
|
||||
nested = false
|
||||
)
|
||||
)
|
||||
chapterAdded = true
|
||||
} else readerText.add(
|
||||
ReaderText.Text(
|
||||
line = markdownParser.parse(line)
|
||||
)
|
||||
)
|
||||
chapterAdded = true
|
||||
} else readerText.add(
|
||||
ReaderText.Text(
|
||||
line = markdownParser.parse(line)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,13 @@ import ua.acclorite.book_story.data.local.room.BookDao
|
|||
import ua.acclorite.book_story.data.mapper.book.BookMapper
|
||||
import ua.acclorite.book_story.data.parser.FileParser
|
||||
import ua.acclorite.book_story.data.parser.TextParser
|
||||
import ua.acclorite.book_story.domain.file.CachedFileCompat
|
||||
import ua.acclorite.book_story.domain.library.book.Book
|
||||
import ua.acclorite.book_story.domain.library.book.BookWithCover
|
||||
import ua.acclorite.book_story.domain.reader.ReaderText
|
||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||
import ua.acclorite.book_story.domain.util.CoverImage
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
|
@ -98,14 +100,22 @@ class BookRepositoryImpl @Inject constructor(
|
|||
if (bookId == -1) return emptyList()
|
||||
|
||||
val book = database.findBookById(bookId)
|
||||
val file = File(book.filePath)
|
||||
val cachedFile = CachedFileCompat.fromFullPath(
|
||||
context = application,
|
||||
path = book.filePath,
|
||||
builder = CachedFileCompat.build(
|
||||
name = book.filePath.substringAfterLast(File.separator),
|
||||
path = book.filePath,
|
||||
isDirectory = false
|
||||
)
|
||||
)
|
||||
|
||||
if (!file.exists()) {
|
||||
if (cachedFile == null || !cachedFile.canAccess()) {
|
||||
Log.e(GET_TEXT, "File [$bookId] does not exist")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val readerText = textParser.parse(file)
|
||||
val readerText = textParser.parse(cachedFile)
|
||||
|
||||
if (
|
||||
readerText.filterIsInstance<ReaderText.Text>().isEmpty() ||
|
||||
|
|
@ -125,7 +135,7 @@ class BookRepositoryImpl @Inject constructor(
|
|||
*/
|
||||
override suspend fun insertBook(
|
||||
bookWithCover: BookWithCover
|
||||
): Boolean {
|
||||
) {
|
||||
Log.i(INSERT_BOOK, "Inserting ${bookWithCover.book.title}.")
|
||||
|
||||
val filesDir = application.filesDir
|
||||
|
|
@ -144,17 +154,14 @@ class BookRepositoryImpl @Inject constructor(
|
|||
val cover = File(coversDir, coverUri)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
FileOutputStream(cover).use { stream ->
|
||||
if (
|
||||
!bookWithCover.coverImage.copy(Bitmap.Config.RGB_565, false)
|
||||
.compress(
|
||||
Bitmap.CompressFormat.WEBP,
|
||||
20,
|
||||
stream
|
||||
)
|
||||
) {
|
||||
throw Exception("Couldn't save cover image")
|
||||
}
|
||||
BufferedOutputStream(FileOutputStream(cover)).use { output ->
|
||||
bookWithCover.coverImage
|
||||
.copy(Bitmap.Config.RGB_565, false)
|
||||
.compress(Bitmap.CompressFormat.WEBP, 20, output)
|
||||
.let { success ->
|
||||
if (success) return@let
|
||||
throw Exception("Couldn't save cover image")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -173,7 +180,6 @@ class BookRepositoryImpl @Inject constructor(
|
|||
val bookToInsert = bookMapper.toBookEntity(updatedBook)
|
||||
database.insertBook(bookToInsert)
|
||||
Log.i(INSERT_BOOK, "Successfully inserted book.")
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -218,16 +224,14 @@ class BookRepositoryImpl @Inject constructor(
|
|||
val cover = File(coversDir, uri)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
FileOutputStream(cover).use { stream ->
|
||||
if (
|
||||
!newCoverImage.copy(Bitmap.Config.RGB_565, false).compress(
|
||||
Bitmap.CompressFormat.WEBP,
|
||||
20,
|
||||
stream
|
||||
)
|
||||
) {
|
||||
throw Exception("Couldn't save cover image")
|
||||
}
|
||||
BufferedOutputStream(FileOutputStream(cover)).use { output ->
|
||||
newCoverImage
|
||||
.copy(Bitmap.Config.RGB_565, false)
|
||||
.compress(Bitmap.CompressFormat.WEBP, 20, output)
|
||||
.let { success ->
|
||||
if (success) return@let
|
||||
throw Exception("Couldn't save cover image")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -319,7 +323,21 @@ class BookRepositoryImpl @Inject constructor(
|
|||
override suspend fun canResetCover(bookId: Int): Boolean {
|
||||
val book = database.findBookById(bookId)
|
||||
|
||||
val defaultCoverUncompressed = fileParser.parse(File(book.filePath))?.coverImage
|
||||
val cachedFile = CachedFileCompat.fromFullPath(
|
||||
application,
|
||||
book.filePath,
|
||||
builder = CachedFileCompat.build(
|
||||
name = book.filePath.substringAfterLast(File.separator),
|
||||
path = book.filePath,
|
||||
isDirectory = false
|
||||
)
|
||||
)
|
||||
|
||||
if (cachedFile == null || !cachedFile.canAccess()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val defaultCoverUncompressed = fileParser.parse(cachedFile)?.coverImage
|
||||
?: return false
|
||||
|
||||
if (book.image == null) {
|
||||
|
|
@ -361,7 +379,21 @@ class BookRepositoryImpl @Inject constructor(
|
|||
}
|
||||
|
||||
val book = database.findBookById(bookId)
|
||||
val defaultCover = fileParser.parse(File(book.filePath))?.coverImage
|
||||
val cachedFile = CachedFileCompat.fromFullPath(
|
||||
application,
|
||||
book.filePath,
|
||||
builder = CachedFileCompat.build(
|
||||
name = book.filePath.substringAfterLast(File.separator),
|
||||
path = book.filePath,
|
||||
isDirectory = false
|
||||
)
|
||||
)
|
||||
|
||||
if (cachedFile == null || !cachedFile.canAccess()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val defaultCover = fileParser.parse(cachedFile)?.coverImage
|
||||
?: return false
|
||||
updateCoverImageOfBook(bookMapper.toBook(book), defaultCover)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,21 +7,13 @@
|
|||
package ua.acclorite.book_story.data.repository
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context.STORAGE_SERVICE
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.storage.StorageManager
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.withContext
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.data.local.room.BookDao
|
||||
import ua.acclorite.book_story.data.parser.FileParser
|
||||
import ua.acclorite.book_story.domain.browse.SelectableFile
|
||||
import ua.acclorite.book_story.domain.file.CachedFile
|
||||
import ua.acclorite.book_story.domain.file.CachedFileCompat
|
||||
import ua.acclorite.book_story.domain.library.book.NullableBook
|
||||
import ua.acclorite.book_story.domain.library.book.NullableBook.NotNull
|
||||
import ua.acclorite.book_story.domain.library.book.NullableBook.Null
|
||||
|
|
@ -29,16 +21,16 @@ import ua.acclorite.book_story.domain.repository.FileSystemRepository
|
|||
import ua.acclorite.book_story.domain.ui.UIText
|
||||
import ua.acclorite.book_story.presentation.core.constants.Constants
|
||||
import ua.acclorite.book_story.presentation.core.constants.provideExtensions
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val GET_BOOK_FROM_FILE = "BOOK FROM FILE, REPO"
|
||||
private const val GET_FILES_FROM_DEVICE = "FILES FROM DEVICE, REPO"
|
||||
private const val GET_FILES = "FILES, REPO"
|
||||
|
||||
/**
|
||||
* File System repository.
|
||||
* Manages all File System([java.io.File]) related work.
|
||||
* Manages all File System related work.
|
||||
*/
|
||||
@Singleton
|
||||
class FileSystemRepositoryImpl @Inject constructor(
|
||||
|
|
@ -48,23 +40,21 @@ class FileSystemRepositoryImpl @Inject constructor(
|
|||
) : FileSystemRepository {
|
||||
|
||||
/**
|
||||
* Get all matching files from device.
|
||||
* Gets all matching files from device.
|
||||
* Filters by [query] and sorts out not supported file formats and already added files.
|
||||
*/
|
||||
override suspend fun getFilesFromDevice(query: String): List<SelectableFile> {
|
||||
Log.i(GET_FILES_FROM_DEVICE, "Getting files from device by query: \"$query\".")
|
||||
override suspend fun getFiles(query: String): List<SelectableFile> {
|
||||
Log.i(GET_FILES, "Getting files from device, query: \"$query\".")
|
||||
|
||||
val existingPaths = database
|
||||
.searchBooks("")
|
||||
.map { it.filePath.trim() }
|
||||
.map { it.filePath }
|
||||
val supportedExtensions = Constants.provideExtensions()
|
||||
|
||||
/**
|
||||
* Verify that a file is valid and can be shown correctly.
|
||||
* Verify that [CachedFile] is valid and can be shown correctly.
|
||||
*/
|
||||
fun File.isValid(): Boolean {
|
||||
if (!exists() || !canRead() || !isFile) return false
|
||||
|
||||
fun CachedFile.isValid(): Boolean {
|
||||
// First: Ensuring supported extension
|
||||
supportedExtensions.any { ext ->
|
||||
name.endsWith(ext, ignoreCase = true)
|
||||
|
|
@ -72,104 +62,89 @@ class FileSystemRepositoryImpl @Inject constructor(
|
|||
|
||||
// Second: Ensuring query to match
|
||||
if (query.isNotBlank()) {
|
||||
name.contains(query.trim(), ignoreCase = true)
|
||||
.let { if (!it) return false }
|
||||
name.contains(query.trim(), ignoreCase = true).let {
|
||||
if (!it) return false
|
||||
}
|
||||
}
|
||||
|
||||
// Third: Ensuring that a file is not added already
|
||||
existingPaths.none { existingPath ->
|
||||
existingPath.equals(path.trim(), ignoreCase = true)
|
||||
existingPath.equals(path, ignoreCase = true)
|
||||
}.let { if (!it) return false }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all verified files from directory (root).
|
||||
*/
|
||||
suspend fun File.getFilesFromDirectory(): List<SelectableFile> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val semaphore = Semaphore(5)
|
||||
walk()
|
||||
.map { file ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
if (!file.isValid()) return@async null
|
||||
SelectableFile(
|
||||
name = file.name,
|
||||
path = file.path,
|
||||
size = file.length(),
|
||||
lastModified = file.lastModified(),
|
||||
selected = false
|
||||
)
|
||||
}
|
||||
}
|
||||
fun CachedFile.getSelectableFilesFromStorage(): List<SelectableFile> {
|
||||
val selectableFiles = mutableListOf<SelectableFile>()
|
||||
|
||||
walk { file ->
|
||||
if (!file.isValid()) return@walk
|
||||
|
||||
selectableFiles.add(
|
||||
SelectableFile(
|
||||
data = file,
|
||||
selected = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return selectableFiles
|
||||
}
|
||||
|
||||
fun getAllStorages(): List<CachedFile> {
|
||||
return application.contentResolver.persistedUriPermissions.mapNotNull { permission ->
|
||||
val storage = CachedFileCompat.fromUri(
|
||||
application,
|
||||
permission.uri,
|
||||
builder = CachedFileCompat.build(
|
||||
name = UUID.randomUUID().toString(),
|
||||
size = 0,
|
||||
lastModified = 0
|
||||
)
|
||||
)
|
||||
if (!storage.isDirectory) return@mapNotNull null
|
||||
|
||||
storage
|
||||
}.let { storages ->
|
||||
storages.filter { storage ->
|
||||
storages.none {
|
||||
it.path != storage.path && storage.path.startsWith(
|
||||
it.path,
|
||||
ignoreCase = true
|
||||
)
|
||||
}
|
||||
.toList()
|
||||
.awaitAll()
|
||||
.filterNotNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all storages (including SD).
|
||||
*/
|
||||
fun getAllStorageDirectories(): List<File> {
|
||||
val storageDirectories = mutableListOf<File>()
|
||||
return try {
|
||||
val storages = getAllStorages()
|
||||
val files = mutableListOf<SelectableFile>()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val storageManager = application.getSystemService(STORAGE_SERVICE) as StorageManager
|
||||
|
||||
val storageVolumes = storageManager.storageVolumes.mapNotNull { it.directory }
|
||||
for (volume in storageVolumes) {
|
||||
if (volume.exists() && volume.canRead() && volume.isDirectory) {
|
||||
storageDirectories.add(volume)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val internalStorage = Environment.getExternalStorageDirectory()
|
||||
if (internalStorage.exists() && internalStorage.canRead() && internalStorage.isDirectory) {
|
||||
storageDirectories.add(internalStorage)
|
||||
}
|
||||
|
||||
val storageVolumes = File("/storage")
|
||||
if (storageVolumes.exists() && storageVolumes.canRead() && storageVolumes.isDirectory) {
|
||||
storageVolumes.listFiles()?.forEach { volume ->
|
||||
if (
|
||||
volume.exists() &&
|
||||
volume.canRead() &&
|
||||
volume.isDirectory &&
|
||||
volume != internalStorage
|
||||
) {
|
||||
storageDirectories.add(volume)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (storage in storages) {
|
||||
files.addAll(storage.getSelectableFilesFromStorage())
|
||||
}
|
||||
|
||||
return storageDirectories
|
||||
Log.i(GET_FILES, "Successfully got all matching files.")
|
||||
files
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(GET_FILES, "Couldn't get all matching files.")
|
||||
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val storageDirectories = getAllStorageDirectories()
|
||||
val files = mutableListOf<SelectableFile>()
|
||||
|
||||
for (volume in storageDirectories) {
|
||||
files.addAll(volume.getFilesFromDirectory())
|
||||
}
|
||||
|
||||
Log.i(GET_FILES_FROM_DEVICE, "Successfully got all matching files.")
|
||||
return files
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets book from given file. If error happened, returns [NullableBook.Null].
|
||||
*/
|
||||
override suspend fun getBookFromFile(file: File): NullableBook {
|
||||
val parsedBook = fileParser.parse(file)
|
||||
override suspend fun getBookFromFile(cachedFile: CachedFile): NullableBook {
|
||||
val parsedBook = fileParser.parse(cachedFile)
|
||||
if (parsedBook == null) {
|
||||
Log.e(GET_BOOK_FROM_FILE, "Parsed file(${file.name}) is null.")
|
||||
Log.e(GET_BOOK_FROM_FILE, "Parsed file(${cachedFile.name}) is null.")
|
||||
return Null(
|
||||
file.name,
|
||||
cachedFile.name,
|
||||
UIText.StringResource(R.string.error_something_went_wrong)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,99 +6,47 @@
|
|||
|
||||
package ua.acclorite.book_story.data.repository
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.shouldShowRationale
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.yield
|
||||
import ua.acclorite.book_story.domain.repository.PermissionRepository
|
||||
import ua.acclorite.book_story.presentation.core.util.launchActivity
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val STORAGE_PERMISSION = "STORAGE PERM, REPO"
|
||||
private const val GRANT_URI = "GRANT URI PERM, REPO"
|
||||
private const val RELEASE_URI = "RELEASE URI PERM, REPO"
|
||||
|
||||
@Singleton
|
||||
class PermissionRepositoryImpl @Inject constructor() : PermissionRepository {
|
||||
class PermissionRepositoryImpl @Inject constructor(
|
||||
private val application: Application
|
||||
) : PermissionRepository {
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
override suspend fun grantStoragePermission(
|
||||
activity: ComponentActivity,
|
||||
storagePermissionState: PermissionState
|
||||
): Boolean {
|
||||
Log.i(STORAGE_PERMISSION, "Requested storage permission")
|
||||
val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||
override suspend fun grantPersistableUriPermission(uri: Uri) {
|
||||
Log.i(GRANT_URI, "Granting persistable uri permission to \"${uri.path}\" URI.")
|
||||
|
||||
val permissionGranted = if (legacyStoragePermission) {
|
||||
storagePermissionState.status.isGranted
|
||||
} else Environment.isExternalStorageManager()
|
||||
|
||||
if (permissionGranted) {
|
||||
Log.i(STORAGE_PERMISSION, "Granted: Storage Permission is already granted")
|
||||
return true
|
||||
try {
|
||||
application.contentResolver.takePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(GRANT_URI, "Could not grant URI permission.")
|
||||
}
|
||||
}
|
||||
|
||||
when (legacyStoragePermission) {
|
||||
true -> {
|
||||
if (!storagePermissionState.status.shouldShowRationale) {
|
||||
storagePermissionState.launchPermissionRequest()
|
||||
} else {
|
||||
val uri = Uri.parse("package:${activity.packageName}")
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)
|
||||
override suspend fun releasePersistableUriPermission(uri: Uri) {
|
||||
Log.i(RELEASE_URI, "Releasing persistable uri permission from \"${uri.path}\" URI.")
|
||||
|
||||
intent.launchActivity(activity) {
|
||||
Log.e(
|
||||
STORAGE_PERMISSION,
|
||||
"Could not launch \"ACTION_APPLICATION_DETAILS_SETTINGS\" activity"
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false -> {
|
||||
val uri = Uri.parse("package:${activity.packageName}")
|
||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, uri)
|
||||
|
||||
intent.launchActivity(activity) {
|
||||
Log.e(
|
||||
STORAGE_PERMISSION,
|
||||
"Could not launch \"ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION\" activity"
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
try {
|
||||
application.contentResolver.releasePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.w(RELEASE_URI, "No granted URI permission found.")
|
||||
}
|
||||
|
||||
for (i in 1..30) {
|
||||
val granted = if (legacyStoragePermission) {
|
||||
storagePermissionState.status.isGranted
|
||||
} else Environment.isExternalStorageManager()
|
||||
|
||||
if (!granted) {
|
||||
delay(1000)
|
||||
yield()
|
||||
continue
|
||||
}
|
||||
|
||||
yield()
|
||||
|
||||
Log.i(STORAGE_PERMISSION, "Successfully granted")
|
||||
return true
|
||||
break
|
||||
}
|
||||
|
||||
Log.e(STORAGE_PERMISSION, "Not granted: Timeout")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -7,12 +7,11 @@
|
|||
package ua.acclorite.book_story.domain.browse
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import ua.acclorite.book_story.domain.file.CachedFile
|
||||
import ua.acclorite.book_story.domain.util.Selected
|
||||
|
||||
@Immutable
|
||||
data class SelectableFile(
|
||||
val name: String,
|
||||
val path: String,
|
||||
val size: Long,
|
||||
val lastModified: Long,
|
||||
val selected: Boolean
|
||||
val data: CachedFile,
|
||||
val selected: Selected
|
||||
)
|
||||
|
|
@ -0,0 +1,311 @@
|
|||
/*
|
||||
* Book's Story — free and open-source Material You eBook reader.
|
||||
* Copyright (C) 2024-2025 Acclorite
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
package ua.acclorite.book_story.domain.file
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.anggrayudi.storage.file.DocumentFileCompat
|
||||
import com.anggrayudi.storage.file.getAbsolutePath
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Cached File.
|
||||
* Faster than [androidx.documentfile.provider.DocumentFile].
|
||||
* Saves all it's variables after initialized.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
@Immutable
|
||||
class CachedFile(
|
||||
private val context: Context,
|
||||
val uri: Uri,
|
||||
private val builder: CachedFileBuilder? = null
|
||||
) {
|
||||
@Immutable
|
||||
private data class QueryParams(
|
||||
val name: String,
|
||||
val size: Long,
|
||||
val lastModified: Long,
|
||||
val isDirectory: Boolean
|
||||
)
|
||||
|
||||
private val queryParams by lazy {
|
||||
getFileQueryParams()
|
||||
}
|
||||
val path: String by lazy { builder?.path ?: getFilePath() }
|
||||
val rawFile: File? by lazy { storeInCache() }
|
||||
|
||||
val name: String get() = builder?.name ?: queryParams.name
|
||||
val size: Long get() = builder?.size ?: queryParams.size
|
||||
val lastModified: Long get() = builder?.lastModified ?: queryParams.lastModified
|
||||
val isDirectory: Boolean get() = builder?.isDirectory ?: queryParams.isDirectory
|
||||
|
||||
fun canAccess(): Boolean {
|
||||
return try {
|
||||
context.contentResolver.query(uri, null, null, null, null)?.let {
|
||||
it.close()
|
||||
return true
|
||||
}
|
||||
throw Exception("Could not access URI: $uri")
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun openInputStream(): InputStream? {
|
||||
return try {
|
||||
context.contentResolver.openInputStream(uri)
|
||||
?: throw Exception("Failed to open InputStream for URI: $uri")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun listFiles(forEach: ((CachedFile) -> Unit)? = null): List<CachedFile> {
|
||||
if (!isDirectory || !canAccess()) return emptyList()
|
||||
|
||||
val cachedFiles = mutableListOf<CachedFile>()
|
||||
|
||||
val nameColumn = DocumentsContract.Document.COLUMN_DISPLAY_NAME
|
||||
val uriColumn = DocumentsContract.Document.COLUMN_DOCUMENT_ID
|
||||
val sizeColumn = DocumentsContract.Document.COLUMN_SIZE
|
||||
val lastModifiedColumn = DocumentsContract.Document.COLUMN_LAST_MODIFIED
|
||||
val isDirectoryColumn = DocumentsContract.Document.COLUMN_MIME_TYPE
|
||||
|
||||
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
|
||||
uri,
|
||||
DocumentsContract.getDocumentId(uri)
|
||||
)
|
||||
|
||||
context.contentResolver.query(
|
||||
childrenUri,
|
||||
arrayOf(
|
||||
nameColumn,
|
||||
uriColumn,
|
||||
sizeColumn,
|
||||
lastModifiedColumn,
|
||||
isDirectoryColumn
|
||||
),
|
||||
null, null, null
|
||||
)?.use { cursor ->
|
||||
if (cursor.count == 0) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
try {
|
||||
val nameIndex = cursor.getColumnIndexOrThrow(nameColumn)
|
||||
val uriIndex = cursor.getColumnIndexOrThrow(uriColumn)
|
||||
val sizeIndex = cursor.getColumnIndexOrThrow(sizeColumn)
|
||||
val lastModifiedIndex = cursor.getColumnIndexOrThrow(lastModifiedColumn)
|
||||
val isDirectoryIndex = cursor.getColumnIndexOrThrow(isDirectoryColumn)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val nameQuery = cursor.getString(nameIndex)
|
||||
val pathQuery = "$path/$nameQuery"
|
||||
val uriQuery = DocumentsContract.buildDocumentUriUsingTree(
|
||||
uri,
|
||||
cursor.getString(uriIndex)
|
||||
)
|
||||
val sizeQuery = cursor.getLong(sizeIndex)
|
||||
val lastModifiedQuery = cursor.getLong(lastModifiedIndex)
|
||||
val isDirectoryQuery = cursor.getString(isDirectoryIndex) ==
|
||||
DocumentsContract.Document.MIME_TYPE_DIR
|
||||
|
||||
val queryFile = CachedFileCompat.fromUri(
|
||||
context = context,
|
||||
uri = uriQuery,
|
||||
builder = CachedFileCompat.build(
|
||||
name = nameQuery,
|
||||
path = pathQuery,
|
||||
size = sizeQuery,
|
||||
lastModified = lastModifiedQuery,
|
||||
isDirectory = isDirectoryQuery
|
||||
)
|
||||
)
|
||||
|
||||
forEach?.invoke(queryFile)
|
||||
cachedFiles.add(queryFile)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
return cachedFiles
|
||||
}
|
||||
|
||||
fun walk(
|
||||
includeDirectories: Boolean = false,
|
||||
forEach: ((CachedFile) -> Unit)? = null
|
||||
): List<CachedFile> {
|
||||
val cachedFiles = mutableListOf<CachedFile>()
|
||||
|
||||
listFiles {
|
||||
when (it.isDirectory) {
|
||||
false -> {
|
||||
forEach?.invoke(it)
|
||||
cachedFiles.add(it)
|
||||
}
|
||||
|
||||
true -> {
|
||||
if (includeDirectories) cachedFiles.add(it)
|
||||
cachedFiles.addAll(it.walk(includeDirectories, forEach))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cachedFiles
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the file to [context].cacheDir and provides [java.io.File].
|
||||
*
|
||||
* @return null if the file is directory or failed
|
||||
*/
|
||||
private fun storeInCache(): File? {
|
||||
if (isDirectory) return null
|
||||
|
||||
val cacheDir = context.cacheDir
|
||||
val fileName = path.replace("_", "-").replace("/", "_")
|
||||
val cacheFile = File(cacheDir, fileName)
|
||||
val tempFile = File(cacheDir, "$fileName.tmp")
|
||||
|
||||
try {
|
||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
BufferedOutputStream(FileOutputStream(tempFile)).use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
} ?: throw IllegalStateException("Failed to open InputStream.")
|
||||
|
||||
if (!tempFile.renameTo(cacheFile)) {
|
||||
throw IllegalStateException("Failed to rename .tmp file.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
tempFile.delete()
|
||||
return null
|
||||
}
|
||||
|
||||
return cacheFile
|
||||
}
|
||||
|
||||
private fun getFileQueryParams(): QueryParams {
|
||||
val nameColumn = DocumentsContract.Document.COLUMN_DISPLAY_NAME
|
||||
val sizeColumn = DocumentsContract.Document.COLUMN_SIZE
|
||||
val lastModifiedColumn = DocumentsContract.Document.COLUMN_LAST_MODIFIED
|
||||
val isDirectoryColumn = DocumentsContract.Document.COLUMN_MIME_TYPE
|
||||
|
||||
val projection = mutableListOf<String>().apply {
|
||||
if (builder?.name == null) add(nameColumn)
|
||||
if (builder?.size == null) add(sizeColumn)
|
||||
if (builder?.lastModified == null) add(lastModifiedColumn)
|
||||
if (builder?.isDirectory == null) add(isDirectoryColumn)
|
||||
}
|
||||
|
||||
if (projection.isEmpty() && builder != null) {
|
||||
return QueryParams(
|
||||
name = builder.name!!,
|
||||
size = builder.size!!,
|
||||
lastModified = builder.lastModified!!,
|
||||
isDirectory = builder.isDirectory!!
|
||||
)
|
||||
}
|
||||
|
||||
context.contentResolver.query(
|
||||
uri,
|
||||
projection.toTypedArray(),
|
||||
null, null, null
|
||||
)?.use { cursor ->
|
||||
try {
|
||||
if (cursor.moveToFirst()) {
|
||||
val queryResult = mutableMapOf<String, Any?>()
|
||||
|
||||
projection.forEach { column ->
|
||||
when (column) {
|
||||
nameColumn -> {
|
||||
if (builder?.name == null) {
|
||||
queryResult[column] = cursor.getString(
|
||||
cursor.getColumnIndexOrThrow(column)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sizeColumn -> {
|
||||
if (builder?.size == null) {
|
||||
queryResult[column] = cursor.getLong(
|
||||
cursor.getColumnIndexOrThrow(column)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
lastModifiedColumn -> {
|
||||
if (builder?.lastModified == null) {
|
||||
queryResult[column] = cursor.getLong(
|
||||
cursor.getColumnIndexOrThrow(column)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
isDirectoryColumn -> {
|
||||
if (builder?.isDirectory == null) {
|
||||
queryResult[column] = cursor.getString(
|
||||
cursor.getColumnIndexOrThrow(column)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val nameQuery = queryResult.getOrElse(nameColumn) {
|
||||
builder?.name
|
||||
} as String
|
||||
|
||||
val sizeQuery = queryResult.getOrElse(sizeColumn) {
|
||||
builder?.size
|
||||
} as Long
|
||||
|
||||
val lastModifiedQuery = queryResult.getOrElse(lastModifiedColumn) {
|
||||
builder?.lastModified
|
||||
} as Long
|
||||
|
||||
val isDirectoryQuery = when (queryResult[isDirectoryColumn]) {
|
||||
DocumentsContract.Document.MIME_TYPE_DIR -> true
|
||||
null -> builder?.isDirectory!!
|
||||
else -> false
|
||||
}
|
||||
|
||||
return QueryParams(
|
||||
name = nameQuery,
|
||||
size = sizeQuery,
|
||||
lastModified = lastModifiedQuery,
|
||||
isDirectory = isDirectoryQuery
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
return QueryParams(
|
||||
name = "unknown_${UUID.randomUUID()}",
|
||||
size = 0,
|
||||
lastModified = 0,
|
||||
isDirectory = false
|
||||
)
|
||||
}
|
||||
|
||||
private fun getFilePath(): String {
|
||||
val tempFile = DocumentFileCompat.fromUri(context, uri)
|
||||
return tempFile?.getAbsolutePath(context)?.trimEnd('/') ?: ""
|
||||
}
|
||||
}
|
||||
|
|
@ -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.domain.file
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
data class CachedFileBuilder(
|
||||
val name: String?,
|
||||
val path: String?,
|
||||
val size: Long? = null,
|
||||
val lastModified: Long? = null,
|
||||
val isDirectory: Boolean? = null
|
||||
)
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Book's Story — free and open-source Material You eBook reader.
|
||||
* Copyright (C) 2024-2025 Acclorite
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
package ua.acclorite.book_story.domain.file
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import com.anggrayudi.storage.file.DocumentFileCompat
|
||||
|
||||
object CachedFileCompat {
|
||||
fun fromUri(context: Context, uri: Uri, builder: CachedFileBuilder? = null): CachedFile {
|
||||
return CachedFile(
|
||||
context = context,
|
||||
uri = when {
|
||||
DocumentsContract.isDocumentUri(context, uri) -> uri
|
||||
|
||||
DocumentsContract.isTreeUri(uri) -> {
|
||||
DocumentsContract.buildDocumentUriUsingTree(
|
||||
uri,
|
||||
DocumentsContract.getTreeDocumentId(uri)
|
||||
)
|
||||
}
|
||||
|
||||
else -> uri
|
||||
},
|
||||
builder = builder
|
||||
)
|
||||
}
|
||||
|
||||
fun fromFullPath(
|
||||
context: Context,
|
||||
path: String,
|
||||
builder: CachedFileBuilder? = null
|
||||
): CachedFile? {
|
||||
val uri = try {
|
||||
val fullPathUri = DocumentFileCompat.fromFullPath(context, path)?.uri
|
||||
if (fullPathUri == null) throw NullPointerException("Could not get URI from full path.")
|
||||
|
||||
fullPathUri
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
||||
try {
|
||||
val parentUri = DocumentFileCompat.getAccessibleRootDocumentFile(
|
||||
context = context,
|
||||
fullPath = path
|
||||
)?.uri
|
||||
if (parentUri == null) throw NullPointerException("Could not get parent URI.")
|
||||
|
||||
val storageId = DocumentFileCompat.getStorageId(context, path)
|
||||
if (storageId.isBlank()) throw NullPointerException("Could not get storageId.")
|
||||
|
||||
val basePath = DocumentFileCompat.getBasePath(context, path)
|
||||
if (basePath.isBlank()) throw NullPointerException("Could not get basePath.")
|
||||
|
||||
val documentUri = DocumentsContract.buildDocumentUriUsingTree(
|
||||
parentUri,
|
||||
"$storageId:$basePath"
|
||||
)
|
||||
if (documentUri == null) throw NullPointerException("Could not get document URI.")
|
||||
|
||||
documentUri
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
val cachedFile = CachedFile(
|
||||
context = context,
|
||||
uri = when {
|
||||
DocumentsContract.isDocumentUri(context, uri) -> uri
|
||||
|
||||
DocumentsContract.isTreeUri(uri) -> {
|
||||
DocumentsContract.buildDocumentUriUsingTree(
|
||||
uri,
|
||||
DocumentsContract.getTreeDocumentId(uri)
|
||||
)
|
||||
}
|
||||
|
||||
else -> uri
|
||||
},
|
||||
builder = builder
|
||||
)
|
||||
|
||||
if (!cachedFile.canAccess()) return null
|
||||
return cachedFile
|
||||
}
|
||||
|
||||
fun build(
|
||||
name: String? = null,
|
||||
path: String? = null,
|
||||
size: Long? = null,
|
||||
lastModified: Long? = null,
|
||||
isDirectory: Boolean? = null
|
||||
): CachedFileBuilder {
|
||||
return CachedFileBuilder(
|
||||
name = name,
|
||||
path = path,
|
||||
size = size,
|
||||
lastModified = lastModified,
|
||||
isDirectory = isDirectory
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ interface BookRepository {
|
|||
|
||||
suspend fun insertBook(
|
||||
bookWithCover: BookWithCover
|
||||
): Boolean
|
||||
)
|
||||
|
||||
suspend fun updateBook(
|
||||
book: Book
|
||||
|
|
|
|||
|
|
@ -7,16 +7,16 @@
|
|||
package ua.acclorite.book_story.domain.repository
|
||||
|
||||
import ua.acclorite.book_story.domain.browse.SelectableFile
|
||||
import ua.acclorite.book_story.domain.file.CachedFile
|
||||
import ua.acclorite.book_story.domain.library.book.NullableBook
|
||||
import java.io.File
|
||||
|
||||
interface FileSystemRepository {
|
||||
|
||||
suspend fun getFilesFromDevice(
|
||||
suspend fun getFiles(
|
||||
query: String = ""
|
||||
): List<SelectableFile>
|
||||
|
||||
suspend fun getBookFromFile(
|
||||
file: File
|
||||
cachedFile: CachedFile
|
||||
): NullableBook
|
||||
}
|
||||
|
|
@ -6,15 +6,15 @@
|
|||
|
||||
package ua.acclorite.book_story.domain.repository
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import android.net.Uri
|
||||
|
||||
interface PermissionRepository {
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
suspend fun grantStoragePermission(
|
||||
activity: ComponentActivity,
|
||||
storagePermissionState: PermissionState
|
||||
): Boolean
|
||||
suspend fun grantPersistableUriPermission(
|
||||
uri: Uri
|
||||
)
|
||||
|
||||
suspend fun releasePersistableUriPermission(
|
||||
uri: Uri
|
||||
)
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ class InsertBook @Inject constructor(
|
|||
private val repository: BookRepository
|
||||
) {
|
||||
|
||||
suspend fun execute(bookWithCover: BookWithCover): Boolean {
|
||||
return repository.insertBook(bookWithCover)
|
||||
suspend fun execute(bookWithCover: BookWithCover) {
|
||||
repository.insertBook(bookWithCover)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,16 +6,16 @@
|
|||
|
||||
package ua.acclorite.book_story.domain.use_case.file_system
|
||||
|
||||
import ua.acclorite.book_story.domain.file.CachedFile
|
||||
import ua.acclorite.book_story.domain.library.book.NullableBook
|
||||
import ua.acclorite.book_story.domain.repository.FileSystemRepository
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetBookFromFile @Inject constructor(
|
||||
private val repository: FileSystemRepository
|
||||
) {
|
||||
|
||||
suspend fun execute(file: File): NullableBook {
|
||||
return repository.getBookFromFile(file)
|
||||
suspend fun execute(cachedFile: CachedFile): NullableBook {
|
||||
return repository.getBookFromFile(cachedFile)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,11 +10,11 @@ import ua.acclorite.book_story.domain.browse.SelectableFile
|
|||
import ua.acclorite.book_story.domain.repository.FileSystemRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetFilesFromDevice @Inject constructor(
|
||||
class GetFiles @Inject constructor(
|
||||
private val repository: FileSystemRepository
|
||||
) {
|
||||
|
||||
suspend fun execute(query: String): List<SelectableFile> {
|
||||
return repository.getFilesFromDevice(query)
|
||||
return repository.getFiles(query)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Book's Story — free and open-source Material You eBook reader.
|
||||
* Copyright (C) 2024-2025 Acclorite
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
package ua.acclorite.book_story.domain.use_case.permission
|
||||
|
||||
import android.net.Uri
|
||||
import ua.acclorite.book_story.domain.repository.PermissionRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class GrantPersistableUriPermission @Inject constructor(
|
||||
private val repository: PermissionRepository
|
||||
) {
|
||||
|
||||
suspend fun execute(uri: Uri) {
|
||||
repository.grantPersistableUriPermission(uri)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +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.domain.use_case.permission
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import ua.acclorite.book_story.domain.repository.PermissionRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class GrantStoragePermission @Inject constructor(
|
||||
private val repository: PermissionRepository
|
||||
) {
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
suspend fun execute(
|
||||
activity: ComponentActivity,
|
||||
storagePermissionState: PermissionState
|
||||
): Boolean {
|
||||
return repository.grantStoragePermission(
|
||||
activity = activity,
|
||||
storagePermissionState = storagePermissionState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Book's Story — free and open-source Material You eBook reader.
|
||||
* Copyright (C) 2024-2025 Acclorite
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
package ua.acclorite.book_story.domain.use_case.permission
|
||||
|
||||
import android.net.Uri
|
||||
import ua.acclorite.book_story.domain.repository.PermissionRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class ReleasePersistableUriPermission @Inject constructor(
|
||||
private val repository: PermissionRepository
|
||||
) {
|
||||
|
||||
suspend fun execute(uri: Uri) {
|
||||
repository.releasePersistableUriPermission(uri)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,9 +14,11 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.domain.file.CachedFileCompat
|
||||
import ua.acclorite.book_story.domain.library.book.Book
|
||||
import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar
|
||||
import ua.acclorite.book_story.presentation.core.components.modal_bottom_sheet.ModalBottomSheet
|
||||
|
|
@ -24,7 +26,6 @@ import ua.acclorite.book_story.presentation.core.constants.Constants
|
|||
import ua.acclorite.book_story.presentation.core.constants.provideExtensions
|
||||
import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategoryTitle
|
||||
import ua.acclorite.book_story.ui.book_info.BookInfoEvent
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
|
@ -38,10 +39,14 @@ fun BookInfoDetailsBottomSheet(
|
|||
val pattern = remember { SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault()) }
|
||||
val lastOpened = remember(book.lastOpened) { pattern.format(Date(book.lastOpened ?: 0)) }
|
||||
|
||||
val fileSize = remember(book.filePath) {
|
||||
val file = File(book.filePath)
|
||||
if (file.exists()) {
|
||||
val sizeBytes = file.length()
|
||||
val context = LocalContext.current
|
||||
val cachedFile = remember(book.filePath) {
|
||||
CachedFileCompat.fromFullPath(context, book.filePath)
|
||||
}
|
||||
|
||||
val fileSize = remember(cachedFile) {
|
||||
if (cachedFile != null && cachedFile.canAccess()) {
|
||||
val sizeBytes = cachedFile.size
|
||||
val sizeKB = sizeBytes / 1024f
|
||||
val sizeMB = sizeKB / 1024f
|
||||
when {
|
||||
|
|
@ -54,11 +59,12 @@ fun BookInfoDetailsBottomSheet(
|
|||
}
|
||||
}
|
||||
|
||||
val fileExists = remember(book.filePath) {
|
||||
File(book.filePath).let {
|
||||
it.exists() && it.isFile && Constants.provideExtensions().any { ext ->
|
||||
it.name.endsWith(ext, ignoreCase = true)
|
||||
}
|
||||
val fileExists = remember(cachedFile) {
|
||||
cachedFile.let {
|
||||
it != null && it.canAccess() && !it.isDirectory && Constants.provideExtensions()
|
||||
.any { ext ->
|
||||
it.name.endsWith(ext, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ import androidx.compose.material.ExperimentalMaterialApi
|
|||
import androidx.compose.material.pullrefresh.PullRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import ua.acclorite.book_story.domain.browse.BrowseLayout
|
||||
import ua.acclorite.book_story.domain.browse.SelectableFile
|
||||
import ua.acclorite.book_story.domain.library.book.SelectableNullableBook
|
||||
|
|
@ -22,13 +20,12 @@ import ua.acclorite.book_story.domain.util.Dialog
|
|||
import ua.acclorite.book_story.ui.browse.BrowseEvent
|
||||
import ua.acclorite.book_story.ui.main.MainEvent
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalPermissionsApi::class)
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun BrowseContent(
|
||||
files: List<SelectableFile>,
|
||||
selectedBooksAddDialog: List<SelectableNullableBook>,
|
||||
refreshState: PullRefreshState,
|
||||
storagePermissionState: PermissionState,
|
||||
loadingAddDialog: Boolean,
|
||||
dialog: Dialog?,
|
||||
bottomSheet: BottomSheet?,
|
||||
|
|
@ -45,7 +42,6 @@ fun BrowseContent(
|
|||
selectedItemsCount: Int,
|
||||
isRefreshing: Boolean,
|
||||
isLoading: Boolean,
|
||||
isError: Boolean,
|
||||
dialogHidden: Boolean,
|
||||
filesEmpty: Boolean,
|
||||
showSearch: Boolean,
|
||||
|
|
@ -58,25 +54,19 @@ fun BrowseContent(
|
|||
clearSelectedFiles: (BrowseEvent.OnClearSelectedFiles) -> Unit,
|
||||
selectFiles: (BrowseEvent.OnSelectFiles) -> Unit,
|
||||
selectFile: (BrowseEvent.OnSelectFile) -> Unit,
|
||||
permissionCheck: (BrowseEvent.OnPermissionCheck) -> Unit,
|
||||
dismissBottomSheet: (BrowseEvent.OnDismissBottomSheet) -> Unit,
|
||||
showFilterBottomSheet: (BrowseEvent.OnShowFilterBottomSheet) -> Unit,
|
||||
actionPermissionDialog: (BrowseEvent.OnActionPermissionDialog) -> Unit,
|
||||
dismissPermissionDialog: (BrowseEvent.OnDismissPermissionDialog) -> Unit,
|
||||
showAddDialog: (BrowseEvent.OnShowAddDialog) -> Unit,
|
||||
dismissAddDialog: (BrowseEvent.OnDismissAddDialog) -> Unit,
|
||||
actionAddDialog: (BrowseEvent.OnActionAddDialog) -> Unit,
|
||||
selectAddDialog: (BrowseEvent.OnSelectAddDialog) -> Unit,
|
||||
changePinnedPaths: (MainEvent.OnChangeBrowsePinnedPaths) -> Unit,
|
||||
navigateToLibrary: () -> Unit,
|
||||
navigateToHelp: () -> Unit,
|
||||
navigateToBrowseSettings: () -> Unit,
|
||||
) {
|
||||
BrowseDialog(
|
||||
dialog = dialog,
|
||||
storagePermissionState = storagePermissionState,
|
||||
loadingAddDialog = loadingAddDialog,
|
||||
actionPermissionDialog = actionPermissionDialog,
|
||||
dismissPermissionDialog = dismissPermissionDialog,
|
||||
actionAddDialog = actionAddDialog,
|
||||
dismissAddDialog = dismissAddDialog,
|
||||
selectedBooksAddDialog = selectedBooksAddDialog,
|
||||
|
|
@ -114,16 +104,13 @@ fun BrowseContent(
|
|||
requestFocus = requestFocus,
|
||||
clearSelectedFiles = clearSelectedFiles,
|
||||
selectFiles = selectFiles,
|
||||
storagePermissionState = storagePermissionState,
|
||||
isLoading = isLoading,
|
||||
isError = isError,
|
||||
filesEmpty = filesEmpty,
|
||||
permissionCheck = permissionCheck,
|
||||
selectFile = selectFile,
|
||||
showFilterBottomSheet = showFilterBottomSheet,
|
||||
showAddDialog = showAddDialog,
|
||||
changePinnedPaths = changePinnedPaths,
|
||||
navigateToHelp = navigateToHelp
|
||||
navigateToBrowseSettings = navigateToBrowseSettings
|
||||
)
|
||||
|
||||
BrowseBackHandler(
|
||||
|
|
|
|||
|
|
@ -7,37 +7,22 @@
|
|||
package ua.acclorite.book_story.presentation.browse
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import ua.acclorite.book_story.domain.library.book.SelectableNullableBook
|
||||
import ua.acclorite.book_story.domain.util.Dialog
|
||||
import ua.acclorite.book_story.ui.browse.BrowseEvent
|
||||
import ua.acclorite.book_story.ui.browse.BrowseScreen
|
||||
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun BrowseDialog(
|
||||
dialog: Dialog?,
|
||||
storagePermissionState: PermissionState,
|
||||
loadingAddDialog: Boolean,
|
||||
selectedBooksAddDialog: List<SelectableNullableBook>,
|
||||
dismissAddDialog: (BrowseEvent.OnDismissAddDialog) -> Unit,
|
||||
actionAddDialog: (BrowseEvent.OnActionAddDialog) -> Unit,
|
||||
selectAddDialog: (BrowseEvent.OnSelectAddDialog) -> Unit,
|
||||
actionPermissionDialog: (BrowseEvent.OnActionPermissionDialog) -> Unit,
|
||||
dismissPermissionDialog: (BrowseEvent.OnDismissPermissionDialog) -> Unit,
|
||||
navigateToLibrary: () -> Unit
|
||||
) {
|
||||
when (dialog) {
|
||||
BrowseScreen.PERMISSION_DIALOG -> {
|
||||
BrowsePermissionDialog(
|
||||
storagePermissionState = storagePermissionState,
|
||||
actionPermissionDialog = actionPermissionDialog,
|
||||
dismissPermissionDialog = dismissPermissionDialog
|
||||
)
|
||||
}
|
||||
|
||||
BrowseScreen.ADD_DIALOG -> {
|
||||
BrowseAddDialog(
|
||||
loadingAddDialog = loadingAddDialog,
|
||||
|
|
|
|||
|
|
@ -12,52 +12,23 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility
|
||||
import ua.acclorite.book_story.presentation.core.components.placeholder.EmptyPlaceholder
|
||||
import ua.acclorite.book_story.presentation.core.components.placeholder.ErrorPlaceholder
|
||||
import ua.acclorite.book_story.ui.browse.BrowseEvent
|
||||
import ua.acclorite.book_story.ui.theme.Transitions
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun BoxScope.BrowseEmptyPlaceholder(
|
||||
filesEmpty: Boolean,
|
||||
dialogHidden: Boolean,
|
||||
isError: Boolean,
|
||||
isLoading: Boolean,
|
||||
isRefreshing: Boolean,
|
||||
storagePermissionState: PermissionState,
|
||||
permissionCheck: (BrowseEvent.OnPermissionCheck) -> Unit,
|
||||
navigateToHelp: () -> Unit
|
||||
navigateToBrowseSettings: () -> Unit
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isError,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
enter = Transitions.DefaultTransitionIn,
|
||||
exit = Transitions.NoExitAnimation
|
||||
) {
|
||||
ErrorPlaceholder(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
errorMessage = stringResource(id = R.string.error_permission),
|
||||
icon = painterResource(id = R.drawable.error),
|
||||
actionTitle = stringResource(id = R.string.grant_permission)
|
||||
) {
|
||||
permissionCheck(
|
||||
BrowseEvent.OnPermissionCheck(
|
||||
storagePermissionState = storagePermissionState
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = !isLoading
|
||||
&& dialogHidden
|
||||
&& filesEmpty
|
||||
&& !isError
|
||||
&& !isRefreshing,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
enter = Transitions.DefaultTransitionIn,
|
||||
|
|
@ -66,9 +37,9 @@ fun BoxScope.BrowseEmptyPlaceholder(
|
|||
EmptyPlaceholder(
|
||||
message = stringResource(id = R.string.browse_empty),
|
||||
icon = painterResource(id = R.drawable.empty_browse),
|
||||
actionTitle = stringResource(id = R.string.get_help),
|
||||
actionTitle = stringResource(id = R.string.set_up_scanning),
|
||||
action = {
|
||||
navigateToHelp()
|
||||
navigateToBrowseSettings()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ import java.util.Locale
|
|||
fun BrowseGridFileItem(file: SelectableFile, hasSelectedItems: Boolean) {
|
||||
val lastModified = rememberSaveable {
|
||||
SimpleDateFormat("dd MMM yyyy", Locale.getDefault())
|
||||
.format(Date(file.lastModified))
|
||||
.format(Date(file.data.lastModified))
|
||||
}
|
||||
|
||||
val sizeBytes = rememberSaveable { file.size }
|
||||
val sizeBytes = rememberSaveable { file.data.size }
|
||||
val fileSizeKB = rememberSaveable {
|
||||
if (sizeBytes > 0) sizeBytes.toDouble() / 1024.0 else 0.0
|
||||
}
|
||||
|
|
@ -94,7 +94,7 @@ fun BrowseGridFileItem(file: SelectableFile, hasSelectedItems: Boolean) {
|
|||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
StyledText(
|
||||
text = file.name,
|
||||
text = file.data.name,
|
||||
style = MaterialTheme.typography.bodyLarge.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ fun BrowseGridLayout(
|
|||
|
||||
items(
|
||||
group.files,
|
||||
key = { it.path }
|
||||
key = { it.data.path }
|
||||
) { selectableFile ->
|
||||
Box(Modifier.animateItem()) {
|
||||
itemContent(selectableFile, group.files)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ fun BrowseLayout(
|
|||
) {
|
||||
val groupedFiles = remember(files, pinnedPaths) {
|
||||
files.groupBy { file ->
|
||||
file.path.substringBeforeLast(File.separator)
|
||||
file.data.path.substringBeforeLast(File.separator)
|
||||
}.map { (header, files) ->
|
||||
GroupedFiles(
|
||||
header = header,
|
||||
|
|
|
|||
|
|
@ -41,10 +41,10 @@ import java.util.Locale
|
|||
fun RowScope.BrowseListFileItem(file: SelectableFile) {
|
||||
val lastModified = rememberSaveable {
|
||||
SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault())
|
||||
.format(Date(file.lastModified))
|
||||
.format(Date(file.data.lastModified))
|
||||
}
|
||||
|
||||
val sizeBytes = rememberSaveable { file.size }
|
||||
val sizeBytes = rememberSaveable { file.data.size }
|
||||
val fileSizeKB = rememberSaveable {
|
||||
if (sizeBytes > 0) sizeBytes.toDouble() / 1024.0 else 0.0
|
||||
}
|
||||
|
|
@ -83,7 +83,7 @@ fun RowScope.BrowseListFileItem(file: SelectableFile) {
|
|||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(verticalArrangement = Arrangement.Center) {
|
||||
StyledText(
|
||||
text = file.name,
|
||||
text = file.data.name,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = MaterialTheme.typography.bodyLarge.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ fun BrowseListLayout(
|
|||
|
||||
items(
|
||||
group.files,
|
||||
key = { it.path }
|
||||
key = { it.data.path }
|
||||
) { selectableFile ->
|
||||
Box(Modifier.animateItem()) {
|
||||
itemContent(selectableFile, group.files)
|
||||
|
|
|
|||
|
|
@ -1,52 +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.presentation.browse
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.SdStorage
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.core.components.dialog.Dialog
|
||||
import ua.acclorite.book_story.presentation.core.util.LocalActivity
|
||||
import ua.acclorite.book_story.ui.browse.BrowseEvent
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun BrowsePermissionDialog(
|
||||
storagePermissionState: PermissionState,
|
||||
actionPermissionDialog: (BrowseEvent.OnActionPermissionDialog) -> Unit,
|
||||
dismissPermissionDialog: (BrowseEvent.OnDismissPermissionDialog) -> Unit
|
||||
) {
|
||||
val activity = LocalActivity.current
|
||||
|
||||
Dialog(
|
||||
title = stringResource(id = R.string.storage_permission),
|
||||
icon = Icons.Outlined.SdStorage,
|
||||
description = stringResource(id = R.string.storage_permission_description),
|
||||
actionEnabled = true,
|
||||
disableOnClick = false,
|
||||
onDismiss = {
|
||||
dismissPermissionDialog(
|
||||
BrowseEvent.OnDismissPermissionDialog(
|
||||
storagePermissionState = storagePermissionState
|
||||
)
|
||||
)
|
||||
},
|
||||
onAction = {
|
||||
actionPermissionDialog(
|
||||
BrowseEvent.OnActionPermissionDialog(
|
||||
activity = activity,
|
||||
storagePermissionState = storagePermissionState
|
||||
)
|
||||
)
|
||||
},
|
||||
withContent = false
|
||||
)
|
||||
}
|
||||
|
|
@ -19,20 +19,17 @@ import androidx.compose.material3.Scaffold
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import ua.acclorite.book_story.domain.browse.BrowseLayout
|
||||
import ua.acclorite.book_story.domain.browse.SelectableFile
|
||||
import ua.acclorite.book_story.ui.browse.BrowseEvent
|
||||
import ua.acclorite.book_story.ui.main.MainEvent
|
||||
import ua.acclorite.book_story.ui.theme.DefaultTransition
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalPermissionsApi::class)
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun BrowseScaffold(
|
||||
files: List<SelectableFile>,
|
||||
refreshState: PullRefreshState,
|
||||
storagePermissionState: PermissionState,
|
||||
listState: LazyListState,
|
||||
gridState: LazyGridState,
|
||||
layout: BrowseLayout,
|
||||
|
|
@ -46,7 +43,6 @@ fun BrowseScaffold(
|
|||
selectedItemsCount: Int,
|
||||
isRefreshing: Boolean,
|
||||
isLoading: Boolean,
|
||||
isError: Boolean,
|
||||
dialogHidden: Boolean,
|
||||
filesEmpty: Boolean,
|
||||
showSearch: Boolean,
|
||||
|
|
@ -59,11 +55,10 @@ fun BrowseScaffold(
|
|||
clearSelectedFiles: (BrowseEvent.OnClearSelectedFiles) -> Unit,
|
||||
selectFiles: (BrowseEvent.OnSelectFiles) -> Unit,
|
||||
selectFile: (BrowseEvent.OnSelectFile) -> Unit,
|
||||
permissionCheck: (BrowseEvent.OnPermissionCheck) -> Unit,
|
||||
showFilterBottomSheet: (BrowseEvent.OnShowFilterBottomSheet) -> Unit,
|
||||
showAddDialog: (BrowseEvent.OnShowAddDialog) -> Unit,
|
||||
changePinnedPaths: (MainEvent.OnChangeBrowsePinnedPaths) -> Unit,
|
||||
navigateToHelp: () -> Unit,
|
||||
navigateToBrowseSettings: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
|
|
@ -149,12 +144,9 @@ fun BrowseScaffold(
|
|||
BrowseEmptyPlaceholder(
|
||||
filesEmpty = filesEmpty,
|
||||
dialogHidden = dialogHidden,
|
||||
isError = isError,
|
||||
isLoading = isLoading,
|
||||
isRefreshing = isRefreshing,
|
||||
storagePermissionState = storagePermissionState,
|
||||
permissionCheck = permissionCheck,
|
||||
navigateToHelp = navigateToHelp
|
||||
navigateToBrowseSettings = navigateToBrowseSettings
|
||||
)
|
||||
|
||||
BrowseRefreshIndicator(
|
||||
|
|
|
|||
|
|
@ -37,8 +37,9 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -68,7 +69,9 @@ fun Dialog(
|
|||
val imeInsets = WindowInsets.ime.asPaddingValues().let {
|
||||
it.calculateTopPadding() + it.calculateBottomPadding()
|
||||
}.value
|
||||
val screenHeightDp = LocalConfiguration.current.screenHeightDp
|
||||
val screenHeightDp = with(LocalDensity.current) {
|
||||
LocalWindowInfo.current.containerSize.height.toDp().value
|
||||
}
|
||||
|
||||
val maxHeight = animateDpAsState(
|
||||
(screenHeightDp - imeInsets - 32).dp,
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ package ua.acclorite.book_story.presentation.settings.browse
|
|||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import ua.acclorite.book_story.presentation.settings.browse.filter.BrowseFilterSubcategory
|
||||
import ua.acclorite.book_story.presentation.settings.browse.general.BrowseGeneralSubcategory
|
||||
import ua.acclorite.book_story.presentation.settings.browse.scan.BrowseScanSubcategory
|
||||
import ua.acclorite.book_story.presentation.settings.browse.sort.BrowseSortSubcategory
|
||||
|
||||
fun LazyListScope.BrowseSettingsCategory() {
|
||||
BrowseScanSubcategory()
|
||||
BrowseGeneralSubcategory()
|
||||
BrowseFilterSubcategory()
|
||||
BrowseSortSubcategory(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Book's Story — free and open-source Material You eBook reader.
|
||||
* Copyright (C) 2024-2025 Acclorite
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
@file:Suppress("FunctionName")
|
||||
|
||||
package ua.acclorite.book_story.presentation.settings.browse.scan
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.settings.browse.scan.components.BrowseScanOption
|
||||
import ua.acclorite.book_story.presentation.settings.browse.scan.components.BrowseScanOptionNote
|
||||
import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategory
|
||||
|
||||
fun LazyListScope.BrowseScanSubcategory(
|
||||
titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary },
|
||||
title: @Composable () -> String = { stringResource(id = R.string.scan_browse_settings) },
|
||||
showTitle: Boolean = true,
|
||||
showDivider: Boolean = true
|
||||
) {
|
||||
SettingsSubcategory(
|
||||
titleColor = titleColor,
|
||||
title = title,
|
||||
showTitle = showTitle,
|
||||
showDivider = showDivider
|
||||
) {
|
||||
item {
|
||||
BrowseScanOption()
|
||||
}
|
||||
|
||||
item {
|
||||
BrowseScanOptionNote()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
* 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.settings.browse.scan.components
|
||||
|
||||
import android.content.Context
|
||||
import android.content.UriPermission
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.outlined.Clear
|
||||
import androidx.compose.material.icons.outlined.Folder
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.anggrayudi.storage.file.DocumentFileCompat
|
||||
import com.anggrayudi.storage.file.getBasePath
|
||||
import com.anggrayudi.storage.file.getRootPath
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.core.components.common.IconButton
|
||||
import ua.acclorite.book_story.presentation.core.components.common.StyledText
|
||||
import ua.acclorite.book_story.presentation.core.util.noRippleClickable
|
||||
import ua.acclorite.book_story.ui.browse.BrowseScreen
|
||||
import ua.acclorite.book_story.ui.settings.SettingsEvent
|
||||
import ua.acclorite.book_story.ui.settings.SettingsModel
|
||||
import ua.acclorite.book_story.ui.theme.dynamicListItemColor
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun BrowseScanOption() {
|
||||
val settingsModel = hiltViewModel<SettingsModel>()
|
||||
val context = LocalContext.current
|
||||
|
||||
fun getPersistedUriPermissions(): List<UriPermission> {
|
||||
return context.contentResolver?.persistedUriPermissions.let { permissions ->
|
||||
if (permissions.isNullOrEmpty()) return@let emptyList()
|
||||
permissions.sortedBy { it.uri.path?.lowercase() }
|
||||
}
|
||||
}
|
||||
|
||||
val persistedUriPermissions = remember {
|
||||
mutableStateListOf<UriPermission>().apply {
|
||||
clear()
|
||||
addAll(getPersistedUriPermissions())
|
||||
}
|
||||
}
|
||||
|
||||
val persistedUriIntent = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocumentTree()
|
||||
) { uri ->
|
||||
if (uri == null) return@rememberLauncherForActivityResult
|
||||
settingsModel.onEvent(
|
||||
SettingsEvent.OnGrantPersistableUriPermission(
|
||||
uri = uri
|
||||
)
|
||||
)
|
||||
|
||||
persistedUriPermissions.clear()
|
||||
persistedUriPermissions.addAll(getPersistedUriPermissions())
|
||||
BrowseScreen.refreshListChannel.trySend(Unit)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize()
|
||||
) {
|
||||
persistedUriPermissions.forEachIndexed { index, permission ->
|
||||
BrowseScanFolderItem(
|
||||
index = index,
|
||||
permission = permission,
|
||||
context = context,
|
||||
releasePersistableUriPermission = {
|
||||
settingsModel.onEvent(
|
||||
SettingsEvent.OnReleasePersistableUriPermission(
|
||||
uri = permission.uri
|
||||
)
|
||||
)
|
||||
|
||||
persistedUriPermissions.clear()
|
||||
persistedUriPermissions.addAll(getPersistedUriPermissions())
|
||||
BrowseScreen.refreshListChannel.trySend(Unit)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BrowseScanAction(
|
||||
requestPersistableUriPermission = {
|
||||
persistedUriIntent.launch(null)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BrowseScanFolderItem(
|
||||
index: Int,
|
||||
permission: UriPermission,
|
||||
context: Context,
|
||||
releasePersistableUriPermission: () -> Unit
|
||||
) {
|
||||
val permissionFile = DocumentFileCompat.fromUri(context, permission.uri) ?: return
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 18.dp,
|
||||
vertical = 8.dp
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(18.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Folder,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.dynamicListItemColor(index))
|
||||
.padding(11.dp)
|
||||
.size(22.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
StyledText(
|
||||
text = permissionFile.getBasePath(context),
|
||||
style = MaterialTheme.typography.bodyLarge.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
)
|
||||
StyledText(
|
||||
text = permissionFile.getRootPath(context),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.size(24.dp),
|
||||
icon = Icons.Outlined.Clear,
|
||||
contentDescription = R.string.remove_content_desc,
|
||||
disableOnClick = false,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
) {
|
||||
releasePersistableUriPermission()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BrowseScanAction(
|
||||
requestPersistableUriPermission: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 18.dp, vertical = 18.dp)
|
||||
.noRippleClickable {
|
||||
requestPersistableUriPermission()
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
StyledText(
|
||||
text = stringResource(id = R.string.add_folder),
|
||||
style = MaterialTheme.typography.labelLarge.copy(
|
||||
color = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.settings.browse.scan.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategoryNote
|
||||
|
||||
@Composable
|
||||
fun BrowseScanOptionNote() {
|
||||
SettingsSubcategoryNote(
|
||||
text = stringResource(
|
||||
id = R.string.browse_scan_option_note
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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.settings.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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.presentation.core.components.common.StyledText
|
||||
|
||||
@Composable
|
||||
fun SettingsSubcategoryNote(
|
||||
text: String,
|
||||
horizontalPadding: Dp = 18.dp,
|
||||
verticalPadding: Dp = 8.dp
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = horizontalPadding,
|
||||
vertical = verticalPadding
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
imageVector = Icons.Outlined.Info,
|
||||
contentDescription = stringResource(id = R.string.note_content_desc),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
StyledText(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,24 +10,17 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import ua.acclorite.book_story.domain.navigator.StackEvent
|
||||
import ua.acclorite.book_story.domain.ui.ButtonItem
|
||||
import ua.acclorite.book_story.ui.main.MainEvent
|
||||
import ua.acclorite.book_story.ui.start.StartEvent
|
||||
import ua.acclorite.book_story.ui.start.StartScreen
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun StartContent(
|
||||
currentPage: Int,
|
||||
stackEvent: StackEvent,
|
||||
storagePermissionGranted: Boolean,
|
||||
storagePermissionState: PermissionState,
|
||||
languages: List<ButtonItem>,
|
||||
changeLanguage: (MainEvent.OnChangeLanguage) -> Unit,
|
||||
storagePermissionRequest: (StartEvent.OnStoragePermissionRequest) -> Unit,
|
||||
navigateForward: () -> Unit,
|
||||
navigateBack: () -> Unit,
|
||||
navigateToBrowse: () -> Unit,
|
||||
|
|
@ -46,11 +39,8 @@ fun StartContent(
|
|||
StartSettings(
|
||||
currentPage = currentPage,
|
||||
stackEvent = stackEvent,
|
||||
storagePermissionGranted = storagePermissionGranted,
|
||||
storagePermissionState = storagePermissionState,
|
||||
languages = languages,
|
||||
changeLanguage = changeLanguage,
|
||||
storagePermissionRequest = storagePermissionRequest,
|
||||
navigateForward = navigateForward
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,39 +7,27 @@
|
|||
package ua.acclorite.book_story.presentation.start
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import ua.acclorite.book_story.domain.navigator.StackEvent
|
||||
import ua.acclorite.book_story.domain.ui.ButtonItem
|
||||
import ua.acclorite.book_story.presentation.core.util.LocalActivity
|
||||
import ua.acclorite.book_story.ui.main.MainEvent
|
||||
import ua.acclorite.book_story.ui.start.StartEvent
|
||||
import ua.acclorite.book_story.ui.start.StartScreen
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun StartSettings(
|
||||
currentPage: Int,
|
||||
stackEvent: StackEvent,
|
||||
storagePermissionGranted: Boolean,
|
||||
storagePermissionState: PermissionState,
|
||||
languages: List<ButtonItem>,
|
||||
changeLanguage: (MainEvent.OnChangeLanguage) -> Unit,
|
||||
storagePermissionRequest: (StartEvent.OnStoragePermissionRequest) -> Unit,
|
||||
navigateForward: () -> Unit
|
||||
) {
|
||||
val activity = LocalActivity.current
|
||||
|
||||
StartSettingsScaffold(
|
||||
currentPage = currentPage,
|
||||
storagePermissionGranted = storagePermissionGranted,
|
||||
navigateForward = navigateForward
|
||||
) {
|
||||
StartContentTransition(
|
||||
targetValue = when (currentPage) {
|
||||
0 -> StartScreen.GENERAL_SETTINGS
|
||||
1 -> StartScreen.APPEARANCE_SETTINGS
|
||||
else -> StartScreen.PERMISSION_SETTINGS
|
||||
else -> StartScreen.SCAN_SETTINGS
|
||||
},
|
||||
stackEvent = stackEvent
|
||||
) { page ->
|
||||
|
|
@ -56,13 +44,8 @@ fun StartSettings(
|
|||
StartSettingsLayoutAppearance()
|
||||
}
|
||||
|
||||
StartScreen.PERMISSION_SETTINGS -> {
|
||||
StartSettingsLayoutPermissions(
|
||||
activity = activity,
|
||||
storagePermissionGranted = storagePermissionGranted,
|
||||
storagePermissionState = storagePermissionState,
|
||||
storagePermissionRequest = storagePermissionRequest
|
||||
)
|
||||
StartScreen.SCAN_SETTINGS -> {
|
||||
StartSettingsLayoutScan()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ import ua.acclorite.book_story.presentation.core.components.common.StyledText
|
|||
|
||||
@Composable
|
||||
fun StartSettingsBottomBar(
|
||||
currentPage: Int,
|
||||
storagePermissionGranted: Boolean,
|
||||
navigateForward: () -> Unit
|
||||
) {
|
||||
Column {
|
||||
|
|
@ -34,8 +32,7 @@ fun StartSettingsBottomBar(
|
|||
.padding(bottom = 8.dp)
|
||||
.padding(horizontal = 18.dp)
|
||||
.fillMaxWidth(),
|
||||
onClick = { navigateForward() },
|
||||
enabled = storagePermissionGranted || currentPage != 2
|
||||
onClick = { navigateForward() }
|
||||
) {
|
||||
StyledText(text = stringResource(id = R.string.next))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,68 +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
|
||||
*/
|
||||
|
||||
@file:Suppress("FunctionName")
|
||||
|
||||
package ua.acclorite.book_story.presentation.start
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategoryTitle
|
||||
import ua.acclorite.book_story.ui.start.StartEvent
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
fun LazyListScope.StartSettingsLayoutPermissions(
|
||||
activity: ComponentActivity,
|
||||
storagePermissionGranted: Boolean,
|
||||
storagePermissionState: PermissionState,
|
||||
storagePermissionRequest: (StartEvent.OnStoragePermissionRequest) -> Unit,
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
SettingsSubcategoryTitle(
|
||||
modifier = Modifier,
|
||||
title = stringResource(id = R.string.start_permissions_preferences),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
StartSettingsLayoutPermissionsItem(
|
||||
title = stringResource(id = R.string.start_permissions_storage),
|
||||
description = stringResource(id = R.string.start_permissions_storage_desc),
|
||||
isOptional = false,
|
||||
isGranted = storagePermissionGranted,
|
||||
onGrantClick = {
|
||||
storagePermissionRequest(
|
||||
StartEvent.OnStoragePermissionRequest(
|
||||
activity = activity,
|
||||
storagePermissionState = storagePermissionState
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +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.presentation.start
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.core.components.common.StyledText
|
||||
|
||||
@Composable
|
||||
fun StartSettingsLayoutPermissionsItem(
|
||||
title: String,
|
||||
description: String,
|
||||
isOptional: Boolean,
|
||||
isGranted: Boolean,
|
||||
onGrantClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 18.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.Center) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
StyledText(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
maxLines = 1
|
||||
)
|
||||
|
||||
if (!isOptional) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = stringResource(id = R.string.required_content_desc),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(15.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
StyledText(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
if (!isGranted) {
|
||||
if (!isOptional) {
|
||||
Button(
|
||||
onClick = onGrantClick,
|
||||
contentPadding = ButtonDefaults.TextButtonContentPadding
|
||||
) {
|
||||
StyledText(
|
||||
text = stringResource(id = R.string.grant),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
} else {
|
||||
TextButton(onClick = onGrantClick) {
|
||||
StyledText(text = stringResource(id = R.string.grant))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
StyledText(
|
||||
text = stringResource(id = R.string.start_permissions_granted),
|
||||
modifier = Modifier.padding(ButtonDefaults.TextButtonContentPadding),
|
||||
style = MaterialTheme.typography.labelLarge.copy(
|
||||
color = MaterialTheme.colorScheme.primary.copy(0.7f)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Book's Story — free and open-source Material You eBook reader.
|
||||
* Copyright (C) 2024-2025 Acclorite
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
@file:Suppress("FunctionName")
|
||||
|
||||
package ua.acclorite.book_story.presentation.start
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ua.acclorite.book_story.R
|
||||
import ua.acclorite.book_story.presentation.settings.browse.scan.components.BrowseScanOption
|
||||
import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategoryTitle
|
||||
|
||||
fun LazyListScope.StartSettingsLayoutScan() {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
SettingsSubcategoryTitle(
|
||||
title = stringResource(id = R.string.start_scan_preferences),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
BrowseScanOption()
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
|
|
@ -29,8 +29,6 @@ import ua.acclorite.book_story.presentation.core.components.common.StyledText
|
|||
|
||||
@Composable
|
||||
fun StartSettingsScaffold(
|
||||
currentPage: Int,
|
||||
storagePermissionGranted: Boolean,
|
||||
navigateForward: () -> Unit,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
|
|
@ -39,8 +37,6 @@ fun StartSettingsScaffold(
|
|||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
bottomBar = {
|
||||
StartSettingsBottomBar(
|
||||
currentPage = currentPage,
|
||||
storagePermissionGranted = storagePermissionGranted,
|
||||
navigateForward = navigateForward
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,16 +4,11 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalPermissionsApi::class)
|
||||
|
||||
package ua.acclorite.book_story.ui.browse
|
||||
|
||||
import android.content.Context
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import ua.acclorite.book_story.domain.browse.SelectableFile
|
||||
import ua.acclorite.book_story.domain.library.book.SelectableNullableBook
|
||||
|
||||
|
|
@ -54,19 +49,6 @@ sealed class BrowseEvent {
|
|||
|
||||
data object OnDismissBottomSheet : BrowseEvent()
|
||||
|
||||
data class OnPermissionCheck(
|
||||
val storagePermissionState: PermissionState
|
||||
) : BrowseEvent()
|
||||
|
||||
data class OnActionPermissionDialog(
|
||||
val activity: ComponentActivity,
|
||||
val storagePermissionState: PermissionState
|
||||
) : BrowseEvent()
|
||||
|
||||
data class OnDismissPermissionDialog(
|
||||
val storagePermissionState: PermissionState
|
||||
) : BrowseEvent()
|
||||
|
||||
data object OnShowAddDialog : BrowseEvent()
|
||||
|
||||
data object OnDismissAddDialog : BrowseEvent()
|
||||
|
|
|
|||
|
|
@ -6,12 +6,8 @@
|
|||
|
||||
package ua.acclorite.book_story.ui.browse
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
@ -32,18 +28,15 @@ import ua.acclorite.book_story.domain.library.book.NullableBook
|
|||
import ua.acclorite.book_story.domain.library.book.SelectableNullableBook
|
||||
import ua.acclorite.book_story.domain.use_case.book.InsertBook
|
||||
import ua.acclorite.book_story.domain.use_case.file_system.GetBookFromFile
|
||||
import ua.acclorite.book_story.domain.use_case.file_system.GetFilesFromDevice
|
||||
import ua.acclorite.book_story.domain.use_case.permission.GrantStoragePermission
|
||||
import ua.acclorite.book_story.domain.use_case.file_system.GetFiles
|
||||
import ua.acclorite.book_story.presentation.core.util.showToast
|
||||
import ua.acclorite.book_story.ui.library.LibraryScreen
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.map
|
||||
|
||||
@HiltViewModel
|
||||
class BrowseModel @Inject constructor(
|
||||
private val grantStoragePermission: GrantStoragePermission,
|
||||
private val getFilesFromDevice: GetFilesFromDevice,
|
||||
private val getFiles: GetFiles,
|
||||
private val getBookFromFile: GetBookFromFile,
|
||||
private val insertBook: InsertBook
|
||||
) : ViewModel() {
|
||||
|
|
@ -79,10 +72,8 @@ class BrowseModel @Inject constructor(
|
|||
|
||||
private var refreshJob: Job? = null
|
||||
private var changeSearchQueryJob: Job? = null
|
||||
private var storagePermissionJob: Job? = null
|
||||
private var getAddDialogBooksJob: Job? = null
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
fun onEvent(event: BrowseEvent) {
|
||||
when (event) {
|
||||
is BrowseEvent.OnRefreshList -> {
|
||||
|
|
@ -191,11 +182,11 @@ class BrowseModel @Inject constructor(
|
|||
val editedList = _state.value.files.map { file ->
|
||||
if (
|
||||
event.files.any {
|
||||
file.path.startsWith(it.path)
|
||||
file.data.path.startsWith(it.data.path)
|
||||
} && event.includedFileFormats.run {
|
||||
if (isEmpty()) return@run true
|
||||
any {
|
||||
file.path.endsWith(it, ignoreCase = true)
|
||||
file.data.path.endsWith(it, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
|
@ -225,7 +216,7 @@ class BrowseModel @Inject constructor(
|
|||
is BrowseEvent.OnSelectFile -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val editedList = _state.value.files.map { file ->
|
||||
if (event.file.path == file.path) {
|
||||
if (event.file.data.path == file.data.path) {
|
||||
file.copy(
|
||||
selected = !file.selected
|
||||
)
|
||||
|
|
@ -271,79 +262,6 @@ class BrowseModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
is BrowseEvent.OnPermissionCheck -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||
val isPermissionGranted = if (!legacyPermission) {
|
||||
Environment.isExternalStorageManager()
|
||||
} else event.storagePermissionState.status.isGranted
|
||||
|
||||
if (isPermissionGranted) {
|
||||
return@launch
|
||||
}
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
dialog = BrowseScreen.PERMISSION_DIALOG,
|
||||
isError = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is BrowseEvent.OnActionPermissionDialog -> {
|
||||
storagePermissionJob?.cancel()
|
||||
storagePermissionJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
grantStoragePermission.execute(
|
||||
activity = event.activity,
|
||||
storagePermissionState = event.storagePermissionState
|
||||
).apply {
|
||||
if (this) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
dialog = null,
|
||||
isError = false
|
||||
)
|
||||
}
|
||||
onEvent(
|
||||
BrowseEvent.OnRefreshList(
|
||||
loading = true,
|
||||
hideSearch = false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is BrowseEvent.OnDismissPermissionDialog -> {
|
||||
viewModelScope.launch {
|
||||
val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||
val permissionGranted = if (!legacyPermission) {
|
||||
Environment.isExternalStorageManager()
|
||||
} else event.storagePermissionState.status.isGranted
|
||||
|
||||
storagePermissionJob?.cancel()
|
||||
_state.update {
|
||||
it.copy(
|
||||
isError = !permissionGranted,
|
||||
dialog = null
|
||||
)
|
||||
}
|
||||
|
||||
if (permissionGranted) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
onEvent(
|
||||
BrowseEvent.OnRefreshList(
|
||||
loading = true,
|
||||
hideSearch = false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is BrowseEvent.OnShowAddDialog -> {
|
||||
viewModelScope.launch {
|
||||
_state.update {
|
||||
|
|
@ -374,10 +292,9 @@ class BrowseModel @Inject constructor(
|
|||
}
|
||||
return@launch
|
||||
}
|
||||
.map { File(it.path) }
|
||||
.forEach {
|
||||
yield()
|
||||
books.add(getBookFromFile.execute(it))
|
||||
books.add(getBookFromFile.execute(it.data))
|
||||
}
|
||||
|
||||
yield()
|
||||
|
|
@ -417,35 +334,8 @@ class BrowseModel @Inject constructor(
|
|||
return@mapNotNull null
|
||||
}.ifEmpty { return@launch }
|
||||
|
||||
var failed = false
|
||||
for (book in booksToInsert) {
|
||||
if (!insertBook.execute(book.bookWithCover!!)) {
|
||||
failed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
dialog = null
|
||||
)
|
||||
}
|
||||
onEvent(
|
||||
BrowseEvent.OnRefreshList(
|
||||
loading = false,
|
||||
hideSearch = false
|
||||
)
|
||||
)
|
||||
onEvent(BrowseEvent.OnClearSelectedFiles)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
event.context
|
||||
.getString(R.string.error_something_went_wrong)
|
||||
.showToast(context = event.context)
|
||||
}
|
||||
|
||||
return@launch
|
||||
insertBook.execute(book.bookWithCover!!)
|
||||
}
|
||||
|
||||
LibraryScreen.refreshListChannel.trySend(0)
|
||||
|
|
@ -512,7 +402,7 @@ class BrowseModel @Inject constructor(
|
|||
private suspend fun getFilesFromDownloads(
|
||||
query: String = if (_state.value.showSearch) _state.value.searchQuery else ""
|
||||
) {
|
||||
getFilesFromDevice.execute(query).apply {
|
||||
getFiles.execute(query).apply {
|
||||
yield()
|
||||
_state.update {
|
||||
it.copy(
|
||||
|
|
@ -525,13 +415,6 @@ class BrowseModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun resetScreen() {
|
||||
viewModelScope.launch {
|
||||
storagePermissionJob?.cancel()
|
||||
_state.update { it.copy(isError = false) }
|
||||
}
|
||||
}
|
||||
|
||||
fun filterList(
|
||||
files: List<SelectableFile>,
|
||||
sortOrderDescending: Boolean,
|
||||
|
|
@ -555,7 +438,7 @@ class BrowseModel @Inject constructor(
|
|||
|
||||
return filter { file ->
|
||||
includedFilterItems.any {
|
||||
file.path.endsWith(
|
||||
file.data.path.endsWith(
|
||||
it, ignoreCase = true
|
||||
)
|
||||
}
|
||||
|
|
@ -568,19 +451,19 @@ class BrowseModel @Inject constructor(
|
|||
compareByWithOrder<SelectableFile> {
|
||||
when (sortOrder) {
|
||||
BrowseSortOrder.NAME -> {
|
||||
it.name.trim()
|
||||
it.data.name.trim()
|
||||
}
|
||||
|
||||
BrowseSortOrder.FILE_FORMAT -> {
|
||||
it.path.substringAfterLast(".").lowercase().trimEnd()
|
||||
it.data.path.substringAfterLast(".").lowercase().trimEnd()
|
||||
}
|
||||
|
||||
BrowseSortOrder.FILE_SIZE -> {
|
||||
it.size
|
||||
it.data.size
|
||||
}
|
||||
|
||||
else -> {
|
||||
it.lastModified
|
||||
it.data.lastModified
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
package ua.acclorite.book_story.ui.browse
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
|
|
@ -20,8 +19,6 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
|
|
@ -31,16 +28,13 @@ import ua.acclorite.book_story.domain.browse.BrowseLayout
|
|||
import ua.acclorite.book_story.domain.navigator.Screen
|
||||
import ua.acclorite.book_story.presentation.browse.BrowseContent
|
||||
import ua.acclorite.book_story.presentation.navigator.LocalNavigator
|
||||
import ua.acclorite.book_story.ui.help.HelpScreen
|
||||
import ua.acclorite.book_story.ui.library.LibraryScreen
|
||||
import ua.acclorite.book_story.ui.main.MainModel
|
||||
import ua.acclorite.book_story.ui.settings.BrowseSettingsScreen
|
||||
|
||||
@Parcelize
|
||||
object BrowseScreen : Screen, Parcelable {
|
||||
|
||||
@IgnoredOnParcel
|
||||
const val PERMISSION_DIALOG = "permission_dialog"
|
||||
|
||||
@IgnoredOnParcel
|
||||
const val ADD_DIALOG = "add_dialog"
|
||||
|
||||
|
|
@ -65,7 +59,7 @@ object BrowseScreen : Screen, Parcelable {
|
|||
@IgnoredOnParcel
|
||||
private var initialGridOffset = 0
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalPermissionsApi::class)
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.current
|
||||
|
|
@ -78,9 +72,6 @@ object BrowseScreen : Screen, Parcelable {
|
|||
val listState = rememberLazyListState(initialListIndex, initialListOffset)
|
||||
val gridState = rememberLazyGridState(initialGridIndex, initialGridOffset)
|
||||
|
||||
val storagePermissionState = rememberPermissionState(
|
||||
permission = Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
)
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val refreshState = rememberPullRefreshState(
|
||||
refreshing = state.value.isRefreshing,
|
||||
|
|
@ -105,14 +96,6 @@ object BrowseScreen : Screen, Parcelable {
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
screenModel.onEvent(
|
||||
BrowseEvent.OnPermissionCheck(
|
||||
storagePermissionState = storagePermissionState
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
resetScrollPositionCompositionChannel.receiveAsFlow().collectLatest {
|
||||
initialListIndex = 0
|
||||
|
|
@ -124,8 +107,6 @@ object BrowseScreen : Screen, Parcelable {
|
|||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
screenModel.resetScreen()
|
||||
|
||||
initialListIndex = 0
|
||||
initialListOffset = 0
|
||||
initialGridIndex = 0
|
||||
|
|
@ -149,7 +130,6 @@ object BrowseScreen : Screen, Parcelable {
|
|||
files = files.value,
|
||||
selectedBooksAddDialog = state.value.selectedBooksAddDialog,
|
||||
refreshState = refreshState,
|
||||
storagePermissionState = storagePermissionState,
|
||||
loadingAddDialog = state.value.loadingAddDialog,
|
||||
dialog = state.value.dialog,
|
||||
bottomSheet = state.value.bottomSheet,
|
||||
|
|
@ -166,7 +146,6 @@ object BrowseScreen : Screen, Parcelable {
|
|||
selectedItemsCount = state.value.selectedItemsCount,
|
||||
isRefreshing = state.value.isRefreshing,
|
||||
isLoading = state.value.isLoading,
|
||||
isError = state.value.isError,
|
||||
dialogHidden = state.value.dialog == null,
|
||||
filesEmpty = files.value.isEmpty(),
|
||||
showSearch = state.value.showSearch,
|
||||
|
|
@ -179,11 +158,8 @@ object BrowseScreen : Screen, Parcelable {
|
|||
clearSelectedFiles = screenModel::onEvent,
|
||||
selectFiles = screenModel::onEvent,
|
||||
selectFile = screenModel::onEvent,
|
||||
permissionCheck = screenModel::onEvent,
|
||||
showFilterBottomSheet = screenModel::onEvent,
|
||||
dismissBottomSheet = screenModel::onEvent,
|
||||
actionPermissionDialog = screenModel::onEvent,
|
||||
dismissPermissionDialog = screenModel::onEvent,
|
||||
showAddDialog = screenModel::onEvent,
|
||||
dismissAddDialog = screenModel::onEvent,
|
||||
selectAddDialog = screenModel::onEvent,
|
||||
|
|
@ -192,8 +168,8 @@ object BrowseScreen : Screen, Parcelable {
|
|||
navigateToLibrary = {
|
||||
navigator.push(LibraryScreen, saveInBackStack = false)
|
||||
},
|
||||
navigateToHelp = {
|
||||
navigator.push(HelpScreen(fromStart = false))
|
||||
navigateToBrowseSettings = {
|
||||
navigator.push(BrowseSettingsScreen)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ data class BrowseState(
|
|||
|
||||
val isLoading: Boolean = true,
|
||||
val isRefreshing: Boolean = false,
|
||||
val isError: Boolean = false,
|
||||
|
||||
val selectedItemsCount: Int = 0,
|
||||
val hasSelectedItems: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -169,4 +169,9 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
cacheDir.deleteRecursively()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
@ -4,18 +4,24 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalPermissionsApi::class)
|
||||
|
||||
package ua.acclorite.book_story.ui.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import ua.acclorite.book_story.domain.util.ID
|
||||
|
||||
@Immutable
|
||||
sealed class SettingsEvent {
|
||||
data class OnGrantPersistableUriPermission(
|
||||
val uri: Uri
|
||||
) : SettingsEvent()
|
||||
|
||||
data class OnReleasePersistableUriPermission(
|
||||
val uri: Uri
|
||||
) : SettingsEvent()
|
||||
|
||||
data class OnSelectColorPreset(
|
||||
val id: ID
|
||||
) : SettingsEvent()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ package ua.acclorite.book_story.ui.settings
|
|||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
@ -27,6 +26,8 @@ import ua.acclorite.book_story.domain.use_case.color_preset.GetColorPresets
|
|||
import ua.acclorite.book_story.domain.use_case.color_preset.ReorderColorPresets
|
||||
import ua.acclorite.book_story.domain.use_case.color_preset.SelectColorPreset
|
||||
import ua.acclorite.book_story.domain.use_case.color_preset.UpdateColorPreset
|
||||
import ua.acclorite.book_story.domain.use_case.permission.GrantPersistableUriPermission
|
||||
import ua.acclorite.book_story.domain.use_case.permission.ReleasePersistableUriPermission
|
||||
import ua.acclorite.book_story.presentation.core.constants.Constants
|
||||
import ua.acclorite.book_story.presentation.core.constants.provideDefaultColorPreset
|
||||
import ua.acclorite.book_story.presentation.core.util.showToast
|
||||
|
|
@ -39,7 +40,9 @@ class SettingsModel @Inject constructor(
|
|||
private val updateColorPreset: UpdateColorPreset,
|
||||
private val selectColorPreset: SelectColorPreset,
|
||||
private val reorderColorPresets: ReorderColorPresets,
|
||||
private val deleteColorPreset: DeleteColorPreset
|
||||
private val deleteColorPreset: DeleteColorPreset,
|
||||
private val grantPersistableUriPermission: GrantPersistableUriPermission,
|
||||
private val releasePersistableUriPermission: ReleasePersistableUriPermission
|
||||
) : ViewModel() {
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
|
@ -92,9 +95,24 @@ class SettingsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
fun onEvent(event: SettingsEvent) {
|
||||
when (event) {
|
||||
is SettingsEvent.OnGrantPersistableUriPermission -> {
|
||||
viewModelScope.launch {
|
||||
grantPersistableUriPermission.execute(
|
||||
event.uri
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is SettingsEvent.OnReleasePersistableUriPermission -> {
|
||||
viewModelScope.launch {
|
||||
releasePersistableUriPermission.execute(
|
||||
event.uri
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is SettingsEvent.OnSelectColorPreset -> {
|
||||
viewModelScope.launch {
|
||||
cancelColorPresetJobs()
|
||||
|
|
|
|||
|
|
@ -1,26 +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
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalPermissionsApi::class)
|
||||
|
||||
package ua.acclorite.book_story.ui.start
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
|
||||
@Immutable
|
||||
sealed class StartEvent {
|
||||
data class OnCheckPermissions(
|
||||
val storagePermissionState: PermissionState
|
||||
) : StartEvent()
|
||||
|
||||
data class OnStoragePermissionRequest(
|
||||
val activity: ComponentActivity,
|
||||
val storagePermissionState: PermissionState
|
||||
) : StartEvent()
|
||||
}
|
||||
|
|
@ -1,92 +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.ui.start
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.yield
|
||||
import ua.acclorite.book_story.domain.use_case.permission.GrantStoragePermission
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class StartModel @Inject constructor(
|
||||
private val grantStoragePermission: GrantStoragePermission
|
||||
) : ViewModel() {
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
private val _state = MutableStateFlow(StartState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
private var storagePermissionJob: Job? = null
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
fun onEvent(event: StartEvent) {
|
||||
when (event) {
|
||||
is StartEvent.OnCheckPermissions -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||
val storagePermissionGranted = if (!legacyStoragePermission) {
|
||||
Environment.isExternalStorageManager()
|
||||
} else event.storagePermissionState.status.isGranted
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
storagePermissionGranted = storagePermissionGranted
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is StartEvent.OnStoragePermissionRequest -> {
|
||||
storagePermissionJob?.cancel()
|
||||
storagePermissionJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
grantStoragePermission.execute(
|
||||
activity = event.activity,
|
||||
storagePermissionState = event.storagePermissionState
|
||||
).apply {
|
||||
if (this) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
storagePermissionGranted = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetScreen() {
|
||||
viewModelScope.launch {
|
||||
_state.update {
|
||||
storagePermissionJob?.cancel()
|
||||
StartState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
|
||||
mutex.withLock {
|
||||
yield()
|
||||
this.value = function(this.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,21 +6,16 @@
|
|||
|
||||
package ua.acclorite.book_story.ui.start
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import ua.acclorite.book_story.domain.navigator.Screen
|
||||
|
|
@ -49,27 +44,21 @@ object StartScreen : Screen, Parcelable {
|
|||
const val APPEARANCE_SETTINGS = "appearance_settings"
|
||||
|
||||
@IgnoredOnParcel
|
||||
const val PERMISSION_SETTINGS = "permission_settings"
|
||||
|
||||
const val SCAN_SETTINGS = "scan_settings"
|
||||
|
||||
@IgnoredOnParcel
|
||||
const val DONE = "done"
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.current
|
||||
val screenModel = hiltViewModel<StartModel>()
|
||||
val mainModel = hiltViewModel<MainModel>()
|
||||
|
||||
val state = screenModel.state.collectAsStateWithLifecycle()
|
||||
val mainState = mainModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
val activity = LocalActivity.current
|
||||
val storagePermissionState = rememberPermissionState(
|
||||
permission = Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
)
|
||||
|
||||
val currentPage = remember { mutableIntStateOf(0) }
|
||||
val stackEvent = remember { mutableStateOf(StackEvent.Default) }
|
||||
|
|
@ -85,27 +74,13 @@ object StartScreen : Screen, Parcelable {
|
|||
}.sortedBy { it.title }
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
screenModel.onEvent(
|
||||
StartEvent.OnCheckPermissions(
|
||||
storagePermissionState = storagePermissionState
|
||||
)
|
||||
)
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { screenModel.resetScreen() }
|
||||
}
|
||||
|
||||
StartContent(
|
||||
currentPage = currentPage.intValue,
|
||||
stackEvent = stackEvent.value,
|
||||
storagePermissionGranted = state.value.storagePermissionGranted,
|
||||
storagePermissionState = storagePermissionState,
|
||||
languages = languages,
|
||||
changeLanguage = mainModel::onEvent,
|
||||
storagePermissionRequest = screenModel::onEvent,
|
||||
navigateForward = {
|
||||
if ((currentPage.intValue + 1 == 3) && !state.value.storagePermissionGranted) {
|
||||
if (currentPage.intValue + 1 == 4) {
|
||||
return@StartContent
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +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.ui.start
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
data class StartState(
|
||||
val storagePermissionGranted: Boolean = false
|
||||
)
|
||||
|
|
@ -9,14 +9,12 @@
|
|||
<resources>
|
||||
<string name="history_screen">التاريخ</string>
|
||||
<string name="add_books_description">يمكنك اختيار أي الكُتب تريد إضافتها. يمكنك تعديل العنوان و صورة غلاف الكتاب لاحقا. عملية التحميل يمكن أن تستغرق حتى بضع دقائق.</string>
|
||||
<string name="storage_permission_description">نحتاج إذن التخزين لفحص جهازك من أجل الكُتب. من دون هذا الإذن، لن تكون قادرا على إضافة كتاب.</string>
|
||||
<string name="move_book_description">اختر الفئة التي تريد نقل هذا الكتاب إليها. سيتم نقل هذا الكتاب إلى الفئة المُحدّدة.</string>
|
||||
<string name="library_screen">المكتبة</string>
|
||||
<string name="browse_screen">تصفح</string>
|
||||
<string name="settings_screen">الإعدادات</string>
|
||||
<string name="about_screen">عن التطبيق</string>
|
||||
<string name="help_screen">مساعدة</string>
|
||||
<string name="storage_permission">منح الإذن؟</string>
|
||||
<string name="add_books">إضافة الكُتب؟</string>
|
||||
<string name="move_books">نقل الكُتب؟</string>
|
||||
<string name="delete_books">حذف الكُتب؟</string>
|
||||
|
|
@ -36,9 +34,6 @@
|
|||
<string name="history_empty">دع آخر كتاب قرأته يفتح صفحات قصة جديدة عظيمة من رحلة قرائتك</string>
|
||||
<string name="error_could_not_reset_cover">غير قادر على إعادة ضبط صورة الغلاف.</string>
|
||||
<string name="cancel">إلغاء</string>
|
||||
<string name="grant">امنح</string>
|
||||
<string name="grant_permission">امنح الإذن</string>
|
||||
<string name="get_help">أحتاج المساعدة</string>
|
||||
<string name="add_book">أضف كتابا˝</string>
|
||||
<string name="never">أبدا˝</string>
|
||||
<string name="undo">تراجع</string>
|
||||
|
|
@ -51,7 +46,6 @@
|
|||
<string name="language_option">لغة التطبيق</string>
|
||||
<string name="dark_theme_option">سمة داكنة</string>
|
||||
<string name="help_desc_how_to_manage_history_2">التاريخ</string>
|
||||
<string name="error_permission">نحتاج إذن التخزين لفحص جهازك من أجل الكُتب</string>
|
||||
<string name="error_no_description">لا يوجد وصف.</string>
|
||||
<string name="browse_empty">قم بتوسيع مجموعة كتب مكتبتك باستخدام تنزيلاتك</string>
|
||||
<string name="selected_items_count_query">حُددت %1$s</string>
|
||||
|
|
@ -301,7 +295,6 @@
|
|||
<string name="start_welcome_desc">إلى عالم حيث الكلمات تسحر وتعلم وتقود إلى مغامرات لا تصدق! دعنا نرتب بعض الأمور!</string>
|
||||
<string name="start_welcome">مرحبًا بك في Book\'s Story!</string>
|
||||
<string name="delete_books_content_desc">احذف الكُتب المُحدّدة</string>
|
||||
<string name="start_permissions_storage_desc">مطلوب مسح جهازك والعثور على الكُتب</string>
|
||||
<string name="delete_color_preset_content_desc">احذف إعداد اللون المسبق</string>
|
||||
<string name="create_color_preset_content_desc">أنشئ إعداد لون مسبق جديد</string>
|
||||
<string name="shuffle_color_preset_content_desc">اخلط الألوان</string>
|
||||
|
|
@ -333,7 +326,6 @@
|
|||
<string name="go_back_content_desc">ارجع</string>
|
||||
<string name="open_reader_settings_content_desc">إعدادات القارئ</string>
|
||||
<string name="cutout_padding_option_desc">تطبيق الهامش على منطقة القطع</string>
|
||||
<string name="required_content_desc">مطلوب</string>
|
||||
<string name="error_content_desc">خطأ</string>
|
||||
<string name="cover_image_not_found_content_desc">لم يتم العثور غلاف صورة</string>
|
||||
<string name="more_content_desc">المزيد</string>
|
||||
|
|
@ -341,11 +333,8 @@
|
|||
<string name="path_changed">نجح تغيير مسار الملف.</string>
|
||||
<string name="revert_content_desc">تراجع</string>
|
||||
<string name="arrow_content_desc">سهم</string>
|
||||
<string name="start_permissions_granted">منحت</string>
|
||||
<string name="start_permissions_storage">إذن التخزين</string>
|
||||
<string name="yesterday">أمس</string>
|
||||
<string name="sort_order_content_desc">ترتيب الفرز</string>
|
||||
<string name="start_permissions_preferences">الأذونات</string>
|
||||
<string name="today">اليوم</string>
|
||||
<string name="checkbox_content_desc">مربّع تأشير</string>
|
||||
<string name="github_profile_content_desc">ملف تعريف GitHub</string>
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@
|
|||
<string name="help_translate_option">Pomozte překládat</string>
|
||||
<string name="filter_tab">Filtr</string>
|
||||
<string name="reader_tab">Čtečka</string>
|
||||
<string name="get_help">Potřebuji pomoc</string>
|
||||
<string name="browse_layout_list">List</string>
|
||||
<string name="absolute_dark_option">Úplně tmavé</string>
|
||||
<string name="delete_book">Smazat knihu?</string>
|
||||
|
|
@ -80,7 +79,6 @@
|
|||
<string name="browse_screen">Procházet</string>
|
||||
<string name="error_no_share_app">Žádná aplikace pro sdílení nebyla nalezena.</string>
|
||||
<string name="help_desc_how_to_add_books_2">Procházet</string>
|
||||
<string name="grant_permission">Povolit oprávnění</string>
|
||||
<string name="browse_grid_size_option">Velikost mřížky</string>
|
||||
<string name="delete_books">Smazat knihy?</string>
|
||||
<string name="font_style_normal">Normální</string>
|
||||
|
|
@ -137,11 +135,9 @@
|
|||
<string name="about_screen">O aplikaci</string>
|
||||
<string name="add_books">Přidat knihy?</string>
|
||||
<string name="move_book">Přesunout knihu?</string>
|
||||
<string name="storage_permission_description">Potřebujeme povolení, abychom mohli skenovat knihy na Vašem úložišti. Bez povolení si nebudete moci přidat jakoukoli knihu.</string>
|
||||
<string name="delete_book_description">Smaže to tuto knihu z databáze. Nesmaže to na vašem zařízení.</string>
|
||||
<string name="error_no_description">Žádný popis.</string>
|
||||
<string name="error_something_went_wrong">Něco se pokazilo.</string>
|
||||
<string name="grant">Povolit</string>
|
||||
<string name="add_book">Přidat knihu</string>
|
||||
<string name="copy">Kopírovat</string>
|
||||
<string name="translate">Přeložit</string>
|
||||
|
|
@ -195,7 +191,6 @@
|
|||
<string name="custom_screen_brightness_option">Vlastní jas obrazovky</string>
|
||||
<string name="horizontal_gesture_sensitivity_option">Citlivost</string>
|
||||
<string name="help_desc_how_to_customize_app_2">Nastavení</string>
|
||||
<string name="start_permissions_storage_desc">Vyžadováno, abychom mohli skenoval Vaše úložiště a hledat knihy</string>
|
||||
<string name="start_language_preferences">Preference jazyka</string>
|
||||
<string name="start_theme_preferences">Preference témat</string>
|
||||
<string name="history_content_desc">Historie čtení</string>
|
||||
|
|
@ -203,7 +198,6 @@
|
|||
<string name="cover_image_content_desc">Obálka</string>
|
||||
<string name="delete_books_content_desc">Smazat vybrané knihy</string>
|
||||
<string name="browse_content_desc">Přidat knihy</string>
|
||||
<string name="start_permissions_granted">Povoleno</string>
|
||||
<string name="move_books_content_desc">Přesunout vybrané knihy</string>
|
||||
<string name="start_welcome">Vítejte v Book\'s Story!</string>
|
||||
<string name="help_title_how_to_move_or_delete_books">Jak přesunu nebo smažu knihy?</string>
|
||||
|
|
@ -219,14 +213,11 @@
|
|||
<string name="go_back_content_desc">Jít zpět</string>
|
||||
<string name="continue_reading_content_desc">Pokračovat ve čtení</string>
|
||||
<string name="library_content_desc">Vaše knihovna</string>
|
||||
<string name="required_content_desc">Vyžadován</string>
|
||||
<string name="open_reader_settings_content_desc">Nastavení čtečky</string>
|
||||
<string name="delete_whole_history_content_desc">Smazat celou historii</string>
|
||||
<string name="color_preset_placeholder">Pojmenujete nastavení…</string>
|
||||
<string name="help_desc_how_to_move_or_delete_books_2">Knihovna</string>
|
||||
<string name="start_welcome_desc">Ve světe, kde slova okouzlují, učí a vedou k neuvěřitelným dobrodružstvím! Pojďme nastavit pár věcí!</string>
|
||||
<string name="start_permissions_storage">Oprávnění úložiště</string>
|
||||
<string name="start_permissions_preferences">Oprávnění</string>
|
||||
<string name="yellow2_theme">Makemake</string>
|
||||
<string name="start_done">A je to!</string>
|
||||
<string name="today">Dnes</string>
|
||||
|
|
|
|||
|
|
@ -28,9 +28,6 @@
|
|||
<string name="selected_items_count_query">%1$s ausgewählt</string>
|
||||
<string name="color_preset_selected_query">Ausgewählt «%1$s»</string>
|
||||
<string name="cancel">Abbrechen</string>
|
||||
<string name="grant">Gewähren</string>
|
||||
<string name="grant_permission">Erteilen der Berechtigung</string>
|
||||
<string name="get_help">Ich brauche Hilfe</string>
|
||||
<string name="add_book">Buch hinzufügen</string>
|
||||
<string name="move">Verschieben</string>
|
||||
<string name="never">Niemals</string>
|
||||
|
|
@ -120,9 +117,6 @@
|
|||
<string name="help_desc_how_to_read_book_2">Bibliothek</string>
|
||||
<string name="help_title_how_to_customize_reader">Wie passe ich den Reader an?</string>
|
||||
<string name="help_desc_how_to_customize_reader_1">Beginnen Sie mit dem Lesen des Buches (siehe: Wie lese ich ein Buch?), und bringen Sie dann die oberen und unteren Balken zum Vorschein, indem Sie irgendwo auf den Bildschirm klicken. Klicken Sie dann auf das Zahnradsymbol in der oberen rechten Ecke. Dadurch werden alle Reader-Einstellungen angezeigt. Der zweite Weg ist, zu den</string>
|
||||
<string name="start_permissions_granted">Gewährt</string>
|
||||
<string name="start_permissions_storage">Speicherberechtigung</string>
|
||||
<string name="start_permissions_storage_desc">Erforderlich, um Ihr Gerät zu scannen und Bücher zu finden</string>
|
||||
<string name="start_done">Alles ist erledigt!</string>
|
||||
<string name="start_done_desc">Sie haben die Grundeinstellungen der App erfolgreich abgeschlossen. Möchten Sie anhand des Hilfebildschirms erfahren, wie Sie die App verwenden?</string>
|
||||
<string name="today">Heute</string>
|
||||
|
|
@ -147,12 +141,9 @@
|
|||
<string name="app_icon_content_desc">App-Symbol</string>
|
||||
<string name="help_desc_how_to_add_books_3">Wenn Sie die heruntergeladenen Bücher nicht sehen, versuchen Sie, die Liste zu aktualisieren, indem Sie sie nach unten ziehen. Stellen Sie sicher, dass Ihre Bücher das unterstützte Dateiformat haben. Klicken Sie dann auf das Buch, um es auszuwählen, oder halten Sie gedrückt, um seinen Speicherort anzuzeigen. Nachdem Sie alle gewünschten Bücher ausgewählt haben, klicken Sie auf das Häkchensymbol in der oberen rechten Ecke, warten Sie, bis alle Bücher geladen sind, und klicken Sie auf «Hinzufügen». Jetzt sollten Sie alle Bücher sehen, die Sie in der Liste hinzugefügt haben</string>
|
||||
<string name="help_screen">Hilfe</string>
|
||||
<string name="storage_permission">Berechtigung erteilen?</string>
|
||||
<string name="add_books">Bücher hinzufügen?</string>
|
||||
<string name="storage_permission_description">Wir benötigen eine Speicherberechtigung, um Ihr Gerät nach Büchern zu durchsuchen. Ohne diese Berechtigung können Sie kein Buch hinzufügen.</string>
|
||||
<string name="delete_books_description">Es werden alle ausgewählten Bücher (%1$s) aus der Datenbank gelöscht. Ausgewählte Bücher werden nicht von Ihrem Gerät gelöscht.</string>
|
||||
<string name="delete_history_description">Dadurch wird der gesamte Verlauf aus der Datenbank gelöscht. Nach dem Löschen des gesamten Verlaufs wird die Reihenfolge der Bücher zurückgesetzt.</string>
|
||||
<string name="error_permission">Wir benötigen eine Speicherberechtigung, um Ihr Gerät nach Büchern zu durchsuchen</string>
|
||||
<string name="error_something_went_wrong">Etwas ist schief gelaufen.</string>
|
||||
<string name="error_no_translator">Keine Übersetzungsapp gefunden.</string>
|
||||
<string name="error_no_browser">Keine Browserapp gefunden.</string>
|
||||
|
|
@ -214,13 +205,11 @@
|
|||
<string name="start_welcome_desc">In eine Welt, in der Worte verzaubern, lehren und zu unglaublichen Abenteuern führen! Lassen Sie uns ein paar Dinge vorbereiten!</string>
|
||||
<string name="start_language_preferences">Spracheinstellungen</string>
|
||||
<string name="start_theme_preferences">Themeneinstellungen</string>
|
||||
<string name="start_permissions_preferences">Berechtigungen</string>
|
||||
<string name="delete_history_element_content_desc">Lösche Verlaufselement</string>
|
||||
<string name="library_content_desc">Deine Bibliothek</string>
|
||||
<string name="history_content_desc">Leseverlauf</string>
|
||||
<string name="browse_content_desc">Bücher hinzufügen</string>
|
||||
<string name="arrow_content_desc">Pfeil</string>
|
||||
<string name="required_content_desc">Vorausgesetzt</string>
|
||||
<string name="reset_start_content_desc">Startanleitung zurücksetzen</string>
|
||||
<string name="open_in_web_content_desc">Im Internet öffnen</string>
|
||||
<string name="delete_color_preset_content_desc">Farbvoreinstellung löschen</string>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
<string name="settings_screen">Ajustes</string>
|
||||
<string name="about_screen">Acerca de la aplicación</string>
|
||||
<string name="help_screen">Ayuda</string>
|
||||
<string name="storage_permission">¿Conceder permiso?</string>
|
||||
<string name="add_books">¿Añadir libros?</string>
|
||||
<string name="move_books">¿Mover libros?</string>
|
||||
<string name="delete_books">¿Borrar libros?</string>
|
||||
|
|
@ -23,15 +22,12 @@
|
|||
<string name="move_books_description">Elija la categoría a la que desea mover todos los libros seleccionados (%1$s). Todos los libros seleccionados serán trasladados a la categoría seleccionada.</string>
|
||||
<string name="move_book_description">Elija la categoría a la que quiere mover este libro. Este libro será trasladado a la categoría seleccionada.</string>
|
||||
<string name="delete_history_description">Eliminará todo el historial de la base de datos. Después de borrar todo el historial, el orden de los libros se restablecerá.</string>
|
||||
<string name="error_permission">Necesitamos permiso de almacenamiento para escanear su dispositivo en busca de libros</string>
|
||||
<string name="error_no_description">Sin descripción.</string>
|
||||
<string name="error_something_went_wrong">Algo salió mal.</string>
|
||||
<string name="error_no_browser">No se encontró ninguna aplicación de navegador.</string>
|
||||
<string name="error_closed_source">Fuente cerrada</string>
|
||||
<string name="selected_items_count_query">%1$s seleccionado</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="grant">Conceder</string>
|
||||
<string name="get_help">Necesito ayuda</string>
|
||||
<string name="add_book">Añadir un libro</string>
|
||||
<string name="move">Mover</string>
|
||||
<string name="change_cover">Cambiar portada</string>
|
||||
|
|
@ -75,11 +71,9 @@
|
|||
<string name="help_desc_how_to_customize_app_3">Puedes hacerlo desde las pantallas de Biblioteca, Historial y Explorar haciendo clic en tres puntos en la esquina superior derecha y seleccionando «Ajustes» desde el menú desplegable. A continuación, verá todos los ajustes disponibles para esta aplicación. ¡No dudes en experimentar con ellos!</string>
|
||||
<string name="start_language_preferences">Configuración del idioma</string>
|
||||
<string name="start_theme_preferences">Preferencias de tema</string>
|
||||
<string name="start_permissions_preferences">Permisos</string>
|
||||
<string name="start_welcome">¡Bienvenid@ a Book\'s Story!</string>
|
||||
<string name="drag_content_desc">Arrastrar</string>
|
||||
<string name="delete">Borrar</string>
|
||||
<string name="storage_permission_description">Necesitamos permiso de almacenamiento para escanear su dispositivo en busca de libros. Sin este permino, no podrá añadir libros.</string>
|
||||
<string name="delete_books_description">Eliminará todos los libros seleccionados(%1$s) de la base de datos. No eliminará los libros seleccionados de su dispositivo.</string>
|
||||
<string name="add_books_description">Puede escoger qué libros desea añadir. Puede editar el título y la portada del libro más adelante. El proceso de carga puede durar varios minutos.</string>
|
||||
<string name="delete_book_description">Eliminará este libro de la base de datos. No eliminará este libro de su dispositivo.</string>
|
||||
|
|
@ -90,7 +84,6 @@
|
|||
<string name="library_empty">Lea sus libros favoritos y ordénelos en categorías</string>
|
||||
<string name="history_empty">Deje que su último libro leído abra las páginas de una nueva gran historia en su viaje de lectura</string>
|
||||
<string name="translate">Traducir</string>
|
||||
<string name="grant_permission">Conceder permiso</string>
|
||||
<string name="never">Nunca</string>
|
||||
<string name="copy">Copiar</string>
|
||||
<string name="done">Hecho</string>
|
||||
|
|
@ -179,11 +172,8 @@
|
|||
<string name="help_desc_how_to_read_book_2">Biblioteca</string>
|
||||
<string name="help_title_how_to_use_double_click_translation">¿Cómo utilizo la traducción de doble clic?</string>
|
||||
<string name="start_welcome_desc">¡Hacia un mundo donde las palabras cautivan, enseñan y conducen a increíbles aventuras! ¡Vamos a preparar algunas cosas!</string>
|
||||
<string name="start_permissions_granted">Concedido</string>
|
||||
<string name="start_done">¡Todo está listo!</string>
|
||||
<string name="start_permissions_storage">Permiso de almacenamiento</string>
|
||||
<string name="help_desc_how_to_use_double_click_translation_1">En primer lugar, vaya a los ajustes del lector (ver: ¿Cómo personalizo el lector?), luego encienda el ajuste correspondiente. Después de eso, comienza a leer el libro (ver: ¿Cómo leo un libro?). Ahora si hace doble clic en el párrafo deseado, el traductor externo se abrirá con el texto del párrafo.</string>
|
||||
<string name="start_permissions_storage_desc">Necesario para escanear tu dispositivo y encontrar los libros</string>
|
||||
<string name="help_desc_how_to_customize_reader_2">Ajustes</string>
|
||||
<string name="help_desc_how_to_manage_history_1">Ir al</string>
|
||||
<string name="help_desc_how_to_manage_history_2">Historial</string>
|
||||
|
|
@ -212,7 +202,6 @@
|
|||
<string name="delete_history_element_content_desc">Eliminar elemento del historial</string>
|
||||
<string name="library_content_desc">Tu biblioteca</string>
|
||||
<string name="arrow_content_desc">Flecha</string>
|
||||
<string name="required_content_desc">Necesario</string>
|
||||
<string name="open_in_web_content_desc">Abrir en la web</string>
|
||||
<string name="shuffle_color_preset_content_desc">Colores aleatorios</string>
|
||||
<string name="open_reader_settings_content_desc">Ajustes del lector</string>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
<string name="help_screen">Aide</string>
|
||||
<string name="delete_books">Supprimer des livres ?</string>
|
||||
<string name="move_books">Déplacer des livres ?</string>
|
||||
<string name="storage_permission">Autoriser ?</string>
|
||||
<string name="delete_book">Supprimer le livre ?</string>
|
||||
<string name="move_book">Déplacer le livre ?</string>
|
||||
<string name="delete_history">Effacer l\'historique de lecture ?</string>
|
||||
|
|
@ -21,7 +20,6 @@
|
|||
<string name="delete_books_description">Cela supprimera tous les livres sélectionnés (%1$s) de la base de données. Ceci ne supprimera pas les livres sélectionnés de votre appareil.</string>
|
||||
<string name="delete_book_description">Cela supprimera ce livre de la base de données. Ceci ne le supprimera pas de votre appareil.</string>
|
||||
<string name="move_book_description">Choisissez la catégorie dans laquelle vous souhaitez déplacer ce livre. Ce dernier sera déplacé vers la catégorie choisie.</string>
|
||||
<string name="error_permission">Nous avons besoin de l\'autorisation de stockage pour scanner votre appareil à la recherche de livres</string>
|
||||
<string name="error_no_translator">Aucune application de traduction n\'a été trouvée.</string>
|
||||
<string name="error_no_dictionary">Aucune application de dictionnaire n\'a été trouvée.</string>
|
||||
<string name="error_no_share_app">Aucune application pour partager n\'a été trouvée.</string>
|
||||
|
|
@ -178,10 +176,6 @@
|
|||
<string name="start_welcome_desc">Pour un monde où les mots charment, enseignent et mènent à des aventures incroyables! Mettons les choses au point!</string>
|
||||
<string name="start_language_preferences">Préférences de langue</string>
|
||||
<string name="start_theme_preferences">Préférences de thèmes</string>
|
||||
<string name="start_permissions_preferences">Permissions</string>
|
||||
<string name="start_permissions_granted">Autorisé</string>
|
||||
<string name="start_permissions_storage">Permission de Stockage</string>
|
||||
<string name="start_permissions_storage_desc">Nécessaire pour scanner votre appareil et trouver des livres</string>
|
||||
<string name="start_done">Tout est prêt!</string>
|
||||
<string name="yesterday">Hier</string>
|
||||
<string name="continue_reading_content_desc">Poursuivre la lecture</string>
|
||||
|
|
@ -206,7 +200,6 @@
|
|||
<string name="library_content_desc">Votre librairie</string>
|
||||
<string name="browse_content_desc">Ajouter les livres</string>
|
||||
<string name="arrow_content_desc">Flèche</string>
|
||||
<string name="required_content_desc">Requis</string>
|
||||
<string name="reset_start_content_desc">Guide de réinitialisation</string>
|
||||
<string name="open_in_web_content_desc">Ouvrir en ligne</string>
|
||||
<string name="delete_color_preset_content_desc">Supprimer le préréglage de couleur</string>
|
||||
|
|
@ -214,7 +207,6 @@
|
|||
<string name="tryzub_content_desc">Tryzub</string>
|
||||
<string name="github_profile_content_desc">Profile Github</string>
|
||||
<string name="drag_content_desc">Glisser</string>
|
||||
<string name="storage_permission_description">Nous avons besoin de l\'autorisation de stockage pour scanner votre appareil et trouver des livres. Sans cette autorisation, vous ne pourrez pas ajouter de livre.</string>
|
||||
<string name="move_books_description">Choisissez la catégorie dans laquelle vous souhaitez déplacer tous les livres sélectionnés (%1$s). Tous les livres sélectionnés seront déplacés vers la catégorie choisie.</string>
|
||||
<string name="add_books">Ajouter des livres ?</string>
|
||||
<string name="error_no_description">Pas de description.</string>
|
||||
|
|
@ -224,9 +216,6 @@
|
|||
<string name="error_could_not_get_text">Le contenu du livre est vide. Veuillez vous assurer que le contenu du livre n\'est pas vide et de le mettre à jour.</string>
|
||||
<string name="history_empty">Laissez le dernier livre que vous avez lu ouvrir les pages d\'une grande et nouvelle histoire dans votre aventure de lecture</string>
|
||||
<string name="cancel">Annuler</string>
|
||||
<string name="grant_permission">Accorder la permission</string>
|
||||
<string name="grant">Autoriser</string>
|
||||
<string name="get_help">J\'ai besoin d\'aide</string>
|
||||
<string name="add_book">Ajouter un livre</string>
|
||||
<string name="color_tab">Couleurs</string>
|
||||
<string name="general_settings">Paramètres généraux</string>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
<string name="about_screen">ऐप के बारे में जानकारी</string>
|
||||
<string name="default_string">डिफ़ॉल्ट</string>
|
||||
<string name="help_screen">सहायता</string>
|
||||
<string name="storage_permission">अनुमति दें?</string>
|
||||
<string name="add_books">किताबें शामिल करें ?</string>
|
||||
<string name="move_books">किताबें स्थानांतरित करें?</string>
|
||||
<string name="delete_book">किताब हटाएं?</string>
|
||||
|
|
@ -25,9 +24,6 @@
|
|||
<string name="selected_items_count_query">%1$s चुना हुआ है</string>
|
||||
<string name="color_preset_selected_query">«%1$s» चुना गया है</string>
|
||||
<string name="cancel">रद्द करें</string>
|
||||
<string name="grant_permission">अनुमति दें</string>
|
||||
<string name="grant">अनुदान</string>
|
||||
<string name="get_help">मुझे सहायता की आवश्यकता है</string>
|
||||
<string name="add_book">एक किताब जोड़ें</string>
|
||||
<string name="never">कभी नहीं</string>
|
||||
<string name="undo">पूर्ववत् करें</string>
|
||||
|
|
@ -81,12 +77,10 @@
|
|||
<string name="system_reader_settings">सिस्टम</string>
|
||||
<string name="filter_browse_settings">फ़िल्टर</string>
|
||||
<string name="add_books_description">आप चुन सकते हैं कि आप कौन सी किताबें जोड़ना चाहते हैं। आप बाद में किताब का शीर्षक और कवर छवि संपादित कर सकते हैं। लोडिंग प्रक्रिया में कई मिनट लग सकते हैं।</string>
|
||||
<string name="storage_permission_description">हमें आपकी डिवाइस को पुस्तकों के लिए स्कैन करने के लिए संग्रहण अनुमति की आवश्यकता है। इस अनुमति के बिना, आप कोई पुस्तक नहीं डाल सकेंगे ।</string>
|
||||
<string name="delete_books_description">यह डेटाबेस से सभी चयनित पुस्तकों(%1$s) को हटा देगा। यह आपके डिवाइस से चयनित पुस्तकों को नहीं हटाएगा।</string>
|
||||
<string name="delete_book_description">यह इस पुस्तक को डेटाबेस से हटा देगा. यह आपकी डिवाइस से इस पुस्तक को नहीं हटाएगा।</string>
|
||||
<string name="move_book_description">वह श्रेणी चुनें जिसमें आप इस पुस्तक को ले जाना चाहते हैं। यह पुस्तक चयनित श्रेणी में ले जाई जाएगी।</string>
|
||||
<string name="delete_history_description">यह डेटाबेस से पूरा इतिहास मिटा देगा। पूरा इतिहास मिटाने के बाद, किताबों का क्रम फिर से सेट हो जाएगा।</string>
|
||||
<string name="error_permission">हमें किताबों के लिए आपके डिवाइस को स्कैन करने के लिए भंडारण अनुमति की आवश्यकता है</string>
|
||||
<string name="error_no_description">कोई वर्णन नहीं।</string>
|
||||
<string name="error_no_dictionary">कोई शब्दकोश ऐप नहीं मिला ।</string>
|
||||
<string name="error_no_browser">कोई ब्राउज़र ऐप नहीं मिला ।</string>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
<string name="move">Sposta</string>
|
||||
<string name="undo">Annulla</string>
|
||||
<string name="translate">Traduci</string>
|
||||
<string name="grant_permission">Concedi permesso</string>
|
||||
<string name="browse_settings">Esplora</string>
|
||||
<string name="delete_cover">Elimina immagine di copertina</string>
|
||||
<string name="text_reader_settings">Testo</string>
|
||||
|
|
@ -52,18 +51,14 @@
|
|||
<string name="theme_appearance_settings">Preferenze tema</string>
|
||||
<string name="go_back_content_desc">Indietro</string>
|
||||
<string name="paragraph_height_option">Spaziatura dei paragrafi</string>
|
||||
<string name="storage_permission">Concedi permesso?</string>
|
||||
<string name="font_style_italic">Corsivo</string>
|
||||
<string name="error_no_translator">Nessuna app traduttore trovata.</string>
|
||||
<string name="start_permissions_preferences">Permessi</string>
|
||||
<string name="history_content_desc">Cronologia lettura</string>
|
||||
<string name="required_content_desc">Richiesto</string>
|
||||
<string name="app_version_option">Versione app</string>
|
||||
<string name="books_moved">Tutti i libri selezionati sono stati spostati correttamente.</string>
|
||||
<string name="error_no_browser">Nessuna app browser trovata.</string>
|
||||
<string name="delete_history">Cancellare la cronologia di lettura?</string>
|
||||
<string name="error_no_dictionary">Nessuna app dizionario trovata.</string>
|
||||
<string name="storage_permission_description">Abbiamo bisogno del permesso di archiviazione per la scansione del Tuo Dispositivo per i libri. Senza questo permesso, non sarai in grado di aggiungere i libri.</string>
|
||||
<string name="done">Fatto</string>
|
||||
<string name="share">Condividi</string>
|
||||
<string name="yes_go_to_help">Sì, vai ad Aiuto</string>
|
||||
|
|
@ -98,7 +93,6 @@
|
|||
<string name="delete_books_description">Eliminerà tutti i libri selezionati (%1$s) dal database. Non eliminerà i libri selezionati dal tuo dispositivo.</string>
|
||||
<string name="error_no_description">Nessuna descrizione.</string>
|
||||
<string name="error_something_went_wrong">Qualcosa è andato storto.</string>
|
||||
<string name="error_permission">Abbiamo bisogno del permesso di archiviazione per cercare i libri nel tuo dispositivo</string>
|
||||
<string name="library_empty">Leggi i tuoi libri preferiti e ordinali per categoria</string>
|
||||
<string name="sort_tab">Ordina</string>
|
||||
<string name="color_tab">Colori</string>
|
||||
|
|
@ -124,7 +118,6 @@
|
|||
<string name="delete_books">Eliminare libri?</string>
|
||||
<string name="copy">Copia</string>
|
||||
<string name="general_tab">Generale</string>
|
||||
<string name="grant">Concedi</string>
|
||||
<string name="help_translate_option">Aiuta a tradurre</string>
|
||||
<string name="yellow_theme">Venere</string>
|
||||
<string name="dictionary">Dizionario</string>
|
||||
|
|
@ -132,7 +125,6 @@
|
|||
<string name="theme_contrast_option">Contrasto tema</string>
|
||||
<string name="search_content_desc">Cerca</string>
|
||||
<string name="green_theme">Terra</string>
|
||||
<string name="get_help">Ho bisogno di aiuto</string>
|
||||
<string name="theme_contrast_high">Alto</string>
|
||||
<string name="author">Autore</string>
|
||||
<string name="browse_layout_list">Lista</string>
|
||||
|
|
@ -287,9 +279,6 @@
|
|||
<string name="filter_content_desc">Filtro</string>
|
||||
<string name="help_desc_how_to_customize_reader_3">e seleziona la categoria Lettore per personalizzare le impostazioni generali oppure la categoria Aspetto per personalizzare le impostazioni Colori.</string>
|
||||
<string name="help_title_how_to_use_double_click_translation">Come si usa la traduzione con doppio clic?</string>
|
||||
<string name="start_permissions_granted">Concesso</string>
|
||||
<string name="start_permissions_storage">Autorizzazione di archiviazione</string>
|
||||
<string name="start_permissions_storage_desc">Necessario per scansionare il tuo dispositivo e trovare i libri</string>
|
||||
<string name="start_done_desc">Hai completato con successo le impostazioni di base dell\'app. Vuoi imparare a usare l\'app leggendo la schermata di Aiuto?</string>
|
||||
<string name="add_files_content_desc">Aggiungi file</string>
|
||||
<string name="library_content_desc">La tua libreria</string>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@
|
|||
<string name="library_screen">Biblioteka</string>
|
||||
<string name="browse_screen">Przeglądaj</string>
|
||||
<string name="cancel">Anuluj</string>
|
||||
<string name="grant">Udziel</string>
|
||||
<string name="delete">Usuń</string>
|
||||
<string name="sort_browse_settings">Sortuj</string>
|
||||
<string name="red_color">Czerwony</string>
|
||||
|
|
@ -58,7 +57,6 @@
|
|||
<string name="planning_tab">Planowanie</string>
|
||||
<string name="fast_color_preset_change_option">Szybka zmiana ustawienia koloru</string>
|
||||
<string name="about_screen">O aplikacji</string>
|
||||
<string name="storage_permission">Przyznać dostęp?</string>
|
||||
<string name="add_books">Dodać książki?</string>
|
||||
<string name="delete_books">Usunąć książki?</string>
|
||||
<string name="delete_book">Usunąć książkę?</string>
|
||||
|
|
@ -67,7 +65,6 @@
|
|||
<string name="move_books_description">Wybierz kategorię, do której chcesz przenieść wszystkie wybrane książki (%1$s). Wszystkie wybrane książki zostaną przeniesione do wybranej kategorii.</string>
|
||||
<string name="delete_book_description">To spowoduje usunięcie tej książki z bazy danych, ale nie usunie jej z twojego urządzenia.</string>
|
||||
<string name="delete_history_description">To spowoduje usuniecie całej historii z bazy danych. Po usunięciu całej historii kolejność książek zostanie zresetowana.</string>
|
||||
<string name="error_permission">Potrzebujemy uprawnień do przechowywania, aby przeskanować Twoje urządzenie w poszukiwaniu książek</string>
|
||||
<string name="error_no_description">Brak opisu.</string>
|
||||
<string name="error_something_went_wrong">Coś poszło nie tak.</string>
|
||||
<string name="error_no_dictionary">Nie znaleziono aplikacji słownikowej.</string>
|
||||
|
|
@ -129,15 +126,12 @@
|
|||
<string name="dropped_tab">Porzucone</string>
|
||||
<string name="browse_layout_option">Tryb wyświetlania</string>
|
||||
<string name="add_books_description">Możesz wybrać książki, które chcesz dodać. Możesz później edytować tytuł i obraz okładki książki. Proces ładowania może potrwać do kilku minut.</string>
|
||||
<string name="storage_permission_description">Potrzebujemy uprawnień do przechowywania, aby przeskanować Twoje urządzenie w poszukiwaniu książek. Bez tego pozwolenia nie będziesz mógł dodać książki.</string>
|
||||
<string name="error_no_browser">Nie znaleziono aplikacji przeglądarki.</string>
|
||||
<string name="purple_theme">Jowisz</string>
|
||||
<string name="pink_theme">Saturn</string>
|
||||
<string name="yellow_theme">Wenus</string>
|
||||
<string name="aqua_theme">Uran</string>
|
||||
<string name="reader_tab">Czytelnik</string>
|
||||
<string name="grant_permission">Udziel zgody</string>
|
||||
<string name="get_help">Potrzebuję pomocy</string>
|
||||
<string name="add_book">Dodaj książkę</string>
|
||||
<string name="translate">Przetłumacz</string>
|
||||
<string name="appearance_settings_desc">Motyw, kolory</string>
|
||||
|
|
@ -182,7 +176,6 @@
|
|||
<string name="credits_contribution">Wnieśli wkład</string>
|
||||
<string name="yesterday">Wczoraj</string>
|
||||
<string name="selected_content_desc">Wybrane</string>
|
||||
<string name="start_permissions_storage_desc">Wymagane do przeskanowania urządzenia i znalezienia książek</string>
|
||||
<string name="reset_start_content_desc">Zresetuj przewodnik startu</string>
|
||||
<string name="theme_contrast_high">Wysoki</string>
|
||||
<string name="cover_image_changed">Obraz okładki został pomyślnie zmieniony.</string>
|
||||
|
|
@ -209,7 +202,6 @@
|
|||
<string name="help_desc_how_to_use_double_click_translation_1">Najpierw przejdź do ustawień czytnika (patrz: Jak mogę dostosować czytnik?), A następnie włącz odpowiednie ustawienie. Następnie zacznij czytać książkę (patrz: Jak mogę przeczytać książkę?). Teraz, jeśli klikniesz dwukrotnie żądany akapit, zewnętrzny tłumacz zostanie otwarty z tekstem akapitu.</string>
|
||||
<string name="help_title_how_to_create_color_presets">Jak mogę tworzyć i modyfikować ustawienia kolorów?</string>
|
||||
<string name="start_language_preferences">Preferencje języka</string>
|
||||
<string name="start_permissions_storage">Uprawnienie do przechowywania</string>
|
||||
<string name="today">Dzisiaj</string>
|
||||
<string name="go_back_content_desc">Wróć</string>
|
||||
<string name="cover_image_content_desc">Obraz okładki</string>
|
||||
|
|
@ -219,7 +211,6 @@
|
|||
<string name="delete_history_element_content_desc">Usuń element historii</string>
|
||||
<string name="library_content_desc">Twoja biblioteka</string>
|
||||
<string name="arrow_content_desc">Strzałka</string>
|
||||
<string name="required_content_desc">Wymagane</string>
|
||||
<string name="open_in_web_content_desc">Otwórz w sieci</string>
|
||||
<string name="drag_content_desc">Przeciągnij</string>
|
||||
<string name="help_desc_how_to_move_or_delete_books_3">Przytrzymaj obraz okładki pierwszej książki, aż zostanie wybrany, teraz możesz po prostu kliknąć wszystkie obrazy okładki książek, które chcesz wybrać. Możesz także wybrać książki w wielu kategoriach. Po wybraniu wszystkich żądanych książek możesz kliknąć ikonę strzałki, aby przenieść oraz ikonę kosza, aby usunąć książki.</string>
|
||||
|
|
@ -227,8 +218,6 @@
|
|||
<string name="help_desc_how_to_create_color_presets_1">Przejdź do ustawień wyglądu (patrz: Jak mogę dostosować czytnik?). Możesz zmodyfikować wszystkie ustawienia koloru w polu poniżej « Ustawienie koloru » w podkategorii « Kolory ». Aby utworzyć nowe ustawienie kolorów, kliknij przycisk ikony « Dodaj ». Istnieje wiele sposobów modyfikowania ustawienia koloru: klikając pole tekstowe, możesz zmienić nazwę ustawienia koloru, przesuwając suwaki, możesz zmienić kolory ustawionego koloru, klikając przycisk ikony « Losowo pomieszaj », możesz wymieszać wszystkie kolory. Jeśli chcesz usunąć ustawienie koloru, kliknij przycisk ikony « Usuń ».</string>
|
||||
<string name="help_desc_how_to_use_color_presets_1">Przejdź do ustawień wyglądu (patrz: Jak mogę dostosować czytnik?). Możesz wybrać, który kolor ma być używany w ustawieniu « Ustawienia kolorów ». Kliknij żądane ustawienie koloru, aby go wybrać, spowoduje to również zmianę kolorów w programie Reader. Jeśli chcesz zmienić kolejność ustawienia koloru, wybierz go i przytrzymaj przycisk ikony « Uchwyt », a następnie przesuń go do żądanej pozycji. Możesz szybko zmienić ustawienia kolorów, włączając odpowiednie ustawienie w podkategorii « Kolory » ustawienia wyglądu, a następnie przesuwając palcem w lewo lub w prawo w programie Reader, aby szybko zmienić ustawienie kolorów.</string>
|
||||
<string name="start_theme_preferences">Ustawienia wyglądu</string>
|
||||
<string name="start_permissions_preferences">Uprawnienia</string>
|
||||
<string name="start_permissions_granted">Przyznane</string>
|
||||
<string name="start_done_desc">Pomyślnie wykonałeś podstawowe ustawienie aplikacji. Czy chcesz dowiedzieć się, jak korzystać z aplikacji, czytając Pomoc?</string>
|
||||
<string name="file_icon_content_desc">Plik</string>
|
||||
<string name="select_all_files_content_desc">Wybierz wszystkie pliki</string>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@
|
|||
<string name="history_screen">Histórico</string>
|
||||
<string name="browse_screen">Explorar</string>
|
||||
<string name="fullscreen_option">Tela cheia</string>
|
||||
<string name="grant">Permitir</string>
|
||||
<string name="done">Feito</string>
|
||||
<string name="dark_theme_off">Desabilitado</string>
|
||||
<string name="delete_books">Apagar livros?</string>
|
||||
|
|
@ -56,7 +55,6 @@
|
|||
<string name="misc_reader_settings">Miscelânea</string>
|
||||
<string name="pure_dark_off">Desabilitado</string>
|
||||
<string name="add_books">Adicionar livros?</string>
|
||||
<string name="storage_permission_description">Precisamos de permissão de acesso ao armazenamento para procurar livros no seu dispositivo. Sem essa permissão, você não será capaz de adicionar um livro.</string>
|
||||
<string name="delete">Apagar</string>
|
||||
<string name="undo">Desfazer</string>
|
||||
<string name="sort_tab">Organizar</string>
|
||||
|
|
@ -69,7 +67,6 @@
|
|||
<string name="dark_theme_follow_system">Sistema</string>
|
||||
<string name="theme_contrast_standard">Padrão</string>
|
||||
<string name="font_style_normal">Normal</string>
|
||||
<string name="storage_permission">Dar permissão?</string>
|
||||
<string name="delete_history">Apagar histórico de leitura?</string>
|
||||
<string name="author">Autor</string>
|
||||
<string name="blue_color">Azul</string>
|
||||
|
|
@ -106,16 +103,13 @@
|
|||
<string name="file_icon_content_desc">Arquivo</string>
|
||||
<string name="error_content_desc">Erro</string>
|
||||
<string name="search_content_desc">Pesquisar</string>
|
||||
<string name="start_permissions_preferences">Permissões</string>
|
||||
<string name="copied">Copiado</string>
|
||||
<string name="required_content_desc">Requerido</string>
|
||||
<string name="alignment_justify">Justificar</string>
|
||||
<string name="move_books_description">Escolha a categoria da qual você quer mover todos os livros selecionados(%1$s). Todos os livros selecionados serão movidos para essa categoria.</string>
|
||||
<string name="delete_books_description">Isto irá apagar todos os livros selecionados(%1$s) da base de dados. Isso não irá apagar os livros do seu dispositivo.</string>
|
||||
<string name="alignment_center">Centro</string>
|
||||
<string name="help_desc_how_to_customize_reader_2">Configurações</string>
|
||||
<string name="revert_content_desc">Reverter</string>
|
||||
<string name="start_permissions_granted">Permitir</string>
|
||||
<string name="help_desc_how_to_customize_app_2">Configurações</string>
|
||||
<string name="browse_sort_order_name">Alfabeticamente</string>
|
||||
<string name="selected_content_desc">Selecionado</string>
|
||||
|
|
@ -160,7 +154,6 @@
|
|||
<string name="browse_grid_size_per_row">por linha</string>
|
||||
<string name="already_read_tab">Lido</string>
|
||||
<string name="browse_grid_size_option">Tamanho da grade</string>
|
||||
<string name="grant_permission">Dar permissão</string>
|
||||
<string name="error_no_description">Sem descrição.</string>
|
||||
<string name="selected_items_count_query">%1$s selecionado</string>
|
||||
<string name="error_closed_source">Código fechado</string>
|
||||
|
|
@ -183,7 +176,6 @@
|
|||
<string name="continue_reading_content_desc">Continuar a ler</string>
|
||||
<string name="go_back_content_desc">Voltar</string>
|
||||
<string name="start_language_preferences">Prefêrencias linguísticas</string>
|
||||
<string name="start_permissions_storage">Permissão de armazenamento</string>
|
||||
<string name="whats_new_option">O que há de novo</string>
|
||||
<string name="help_translate_option">Ajude a traduzir</string>
|
||||
<string name="start_done">Está tudo feito!</string>
|
||||
|
|
@ -200,7 +192,6 @@
|
|||
<string name="github_profile_content_desc">Perfil do Github</string>
|
||||
<string name="screen_orientation_option">Orientação da tela</string>
|
||||
<string name="error_something_went_wrong">Algo deu errado.</string>
|
||||
<string name="get_help">Preciso d eajuda</string>
|
||||
<string name="change_cover">Trocar imagem da capa</string>
|
||||
<string name="reader_settings_desc">Fonte, texto, tradutor</string>
|
||||
<string name="browse_settings_desc">Geral, filtro, ordem</string>
|
||||
|
|
@ -223,7 +214,6 @@
|
|||
<string name="horizontal_gesture_sensitivity_option">Sensibilidade</string>
|
||||
<string name="horizontal_gesture_off">Desativar</string>
|
||||
<string name="app_version_option_desc_1">Book\'s Story v%1$s</string>
|
||||
<string name="error_permission">Precisamos de permissão de armazenamento para escanear seu dispositivo em busca de livros</string>
|
||||
<string name="error_no_browser">Nenhum aplicativo de navegador encontrado.</string>
|
||||
<string name="error_no_share_app">Nenhum aplicativo para compartilhar encontrado.</string>
|
||||
<string name="error_no_dictionary">Nenhum aplicativo de dicionário encontrado.</string>
|
||||
|
|
@ -306,7 +296,6 @@
|
|||
<string name="purple_gray_theme">Ceres</string>
|
||||
<string name="book_deleted">Esse livro foi apagado com sucesso.</string>
|
||||
<string name="help_desc_how_to_read_book_1">Ir para</string>
|
||||
<string name="start_permissions_storage_desc">Necessário para escanear seu dispositivo e encontrar livros</string>
|
||||
<string name="help_desc_how_to_add_books_1">Ir para</string>
|
||||
<string name="reset_start_content_desc">Redefinir guia de introdução</string>
|
||||
<string name="progress_bar_font_size_option">Tamanho da fonte</string>
|
||||
|
|
|
|||
|
|
@ -19,9 +19,6 @@
|
|||
<string name="color_preset_query">முன்னமைக்கப்பட்ட %1$s</string>
|
||||
<string name="color_preset_selected_query">தேர்ந்தெடுக்கப்பட்ட «%1$s»</string>
|
||||
<string name="cancel">ரத்துசெய்</string>
|
||||
<string name="grant">மானியம்</string>
|
||||
<string name="grant_permission">இசைவு வழங்கவும்</string>
|
||||
<string name="get_help">எனக்கு உதவி தேவை</string>
|
||||
<string name="delete">நீக்கு</string>
|
||||
<string name="next">அடுத்தது</string>
|
||||
<string name="yes_go_to_help">ஆம், உதவிக்குச் செல்லுங்கள்</string>
|
||||
|
|
@ -83,7 +80,6 @@
|
|||
<string name="screen_orientation_locked_landscape">பூட்டப்பட்ட நிலப்பரப்பு</string>
|
||||
<string name="unknown">தெரியவில்லை</string>
|
||||
<string name="help_desc_how_to_read_book_1">செல்லுங்கள்</string>
|
||||
<string name="start_permissions_storage">சேமிப்பக இசைவு</string>
|
||||
<string name="app_version_option">பயன்பாட்டு பதிப்பு</string>
|
||||
<string name="licenses_option">திறந்த மூல உரிமங்கள்</string>
|
||||
<string name="help_desc_how_to_manage_history_2">வரலாறு</string>
|
||||
|
|
@ -97,13 +93,11 @@
|
|||
<string name="credits_fonts">எழுத்துருக்கள்</string>
|
||||
<string name="history_empty">நீங்கள் படித்த உங்கள் கடைசி நூல் உங்கள் வாசிப்பு பயணத்தின் புதிய சிறந்த கதையின் பக்கங்களைத் திறக்கட்டும்</string>
|
||||
<string name="share">பங்கு</string>
|
||||
<string name="start_permissions_preferences">அனுமதிகள்</string>
|
||||
<string name="never">ஒருபோதும்</string>
|
||||
<string name="browse_layout_option">காட்சி முறை</string>
|
||||
<string name="browse_sort_order_file_format">கோப்பு வடிவம்</string>
|
||||
<string name="default_string">இயல்புநிலை</string>
|
||||
<string name="delete_history">வாசிப்பு வரலாற்றை நீக்கவா?</string>
|
||||
<string name="error_permission">புத்தகங்களுக்காக உங்கள் சாதனத்தை ச்கேன் செய்ய எங்களுக்கு சேமிப்பக இசைவு தேவை</string>
|
||||
<string name="error_no_description">விளக்கம் இல்லை.</string>
|
||||
<string name="selected_items_count_query">%1$s தேர்ந்தெடுக்கப்பட்டன</string>
|
||||
<string name="undo">செயல்தவிர்</string>
|
||||
|
|
@ -161,7 +155,6 @@
|
|||
<string name="today">இன்று</string>
|
||||
<string name="error_content_desc">பிழை</string>
|
||||
<string name="browse_content_desc">புத்தகங்களைச் சேர்க்கவும்</string>
|
||||
<string name="required_content_desc">தேவை</string>
|
||||
<string name="open_in_web_content_desc">வலையில் திறந்திருக்கும்</string>
|
||||
<string name="delete_color_preset_content_desc">வண்ண முன்னமைவை நீக்கு</string>
|
||||
<string name="create_color_preset_content_desc">புதிய வண்ண முன்னமைவை உருவாக்கவும்</string>
|
||||
|
|
@ -179,12 +172,10 @@
|
|||
<string name="settings_screen">அமைப்புகள்</string>
|
||||
<string name="about_screen">பயன்பாடு பற்றி</string>
|
||||
<string name="help_screen">உதவி</string>
|
||||
<string name="storage_permission">இசைவு வழங்கவா?</string>
|
||||
<string name="move_books">புத்தகங்களை நகர்த்தவா?</string>
|
||||
<string name="delete_books">புத்தகங்களை நீக்கவா?</string>
|
||||
<string name="delete_book">புத்தகத்தை நீக்கவா?</string>
|
||||
<string name="move_book">புத்தகத்தை நகர்த்தவா?</string>
|
||||
<string name="storage_permission_description">புத்தகங்களுக்காக உங்கள் சாதனத்தை ச்கேன் செய்ய எங்களுக்கு சேமிப்பக இசைவு தேவை. இந்த அனுமதியின்றி, நீங்கள் ஒரு புத்தகத்தைச் சேர்க்க முடியாது.</string>
|
||||
<string name="add_books_description">நீங்கள் எந்த புத்தகங்களைச் சேர்க்க விரும்புகிறீர்கள் என்பதை நீங்கள் தேர்வு செய்யலாம். புத்தகத்தின் தலைப்பு மற்றும் படத்தை பின்னர் திருத்தலாம். ஏற்றுதல் செயல்முறை பல நிமிடங்கள் வரை ஆகலாம்.</string>
|
||||
<string name="move_books_description">தேர்ந்தெடுக்கப்பட்ட அனைத்து புத்தகங்களையும் (%1$s) நகர்த்த விரும்பும் வகையைத் தேர்வுசெய்க. தேர்ந்தெடுக்கப்பட்ட அனைத்து புத்தகங்களும் தேர்ந்தெடுக்கப்பட்ட வகைக்கு மாற்றப்படும்.</string>
|
||||
<string name="delete_books_description">இது தரவுத்தளத்திலிருந்து தேர்ந்தெடுக்கப்பட்ட அனைத்து புத்தகங்களையும் (%1$s) நீக்கிவிடும். இது உங்கள் சாதனத்திலிருந்து தேர்ந்தெடுக்கப்பட்ட புத்தகங்களை நீக்காது.</string>
|
||||
|
|
@ -290,8 +281,6 @@
|
|||
<string name="help_desc_how_to_create_color_presets_1">தோற்ற அமைப்புகளுக்குச் செல்லுங்கள் (பார்க்க: வாசகரை எவ்வாறு தனிப்பயனாக்குவது?). «வண்ண முன்னமைவு» கீழ் «வண்ணங்கள்» துணைப்பிரிவு ஆகியவற்றின் கீழ் பெட்டியில் உள்ள அனைத்து வண்ண முன்னமைவின் அமைப்புகளையும் நீங்கள் மாற்றலாம். புதிய வண்ண முன்னமைவை உருவாக்க, «சேர்» ஐகான்-பொன்ட்டனைக் சொடுக்கு செய்க. ஒரு வண்ண முன்னமைவை மாற்ற பல வழிகள் உள்ளன: உரை புலத்தைக் சொடுக்கு செய்வதன் மூலம் நீங்கள் வண்ண முன்னமைவின் பெயரை மாற்றலாம், ச்லைடர்களை நகர்த்துவதன் மூலம் வண்ண முன்னமைவின் வண்ணங்களை மாற்றலாம், «சஃபிள்» ஐகான்-பொத்தானைக் சொடுக்கு செய்வதன் மூலம் நீங்கள் அனைத்தையும் கலக்கலாம் நிறங்கள். நீங்கள் வண்ண முன்னமைவை நீக்க விரும்பினால், «நீக்கு» ஐகான்-பொத்தானைக் சொடுக்கு செய்க.</string>
|
||||
<string name="help_desc_how_to_use_perception_expander_1">புலனுணர்வு விரிவாக்கம் உங்களுக்கு விரைவாக படிக்க உதவக்கூடும். முதலில், அதை வாசகர் அமைப்புகளில் இயக்கவும் (பார்க்க: வாசகரை எவ்வாறு தனிப்பயனாக்குவது?). இயக்கப்பட்டதும், வாசகரின் இருபுறமும் இரண்டு செங்குத்து கோடுகள் தோன்றும். உங்கள் வாசிப்பு வேகத்தை மேலும் மேம்படுத்த, மையத்தைச் சுற்றி குறைந்த இடத்தை உருவாக்க திணிப்பை சரிசெய்யவும். இதைப் பயன்படுத்த, உங்கள் புற பார்வை பக்கங்களில் உரையை எடுக்க அனுமதிக்கும் போது திரையின் மையத்தில் கவனம் செலுத்துங்கள். நீங்கள் படிக்கும்போது, ஒவ்வொரு வரியுக்கும் பிறகு பக்கங்களை நேரடியாகப் பார்க்காமல் மையத்தில் உங்கள் கவனத்தை பராமரிக்கவும்.</string>
|
||||
<string name="start_theme_preferences">கருப்பொருள் விருப்பத்தேர்வுகள்</string>
|
||||
<string name="start_permissions_granted">வழங்கப்பட்டது</string>
|
||||
<string name="start_permissions_storage_desc">உங்கள் சாதனத்தை ச்கேன் செய்து புத்தகங்களைக் கண்டுபிடிக்க வேண்டும்</string>
|
||||
<string name="start_done_desc">பயன்பாட்டின் அடிப்படை அமைப்பை நீங்கள் வெற்றிகரமாக முடித்துவிட்டீர்கள். உதவித் திரையைப் படிப்பதன் மூலம் பயன்பாட்டை எவ்வாறு பயன்படுத்துவது என்பதை அறிய விரும்புகிறீர்களா?</string>
|
||||
<string name="yesterday">நேற்று</string>
|
||||
<string name="continue_reading_content_desc">தொடர்ந்து படிக்கவும்</string>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
<string name="help_screen">Yardım</string>
|
||||
|
||||
<!-- Dialog Labels -->
|
||||
<string name="storage_permission">İzin verilsin mi?</string>
|
||||
<string name="add_books">Kitaplar eklensin mi?</string>
|
||||
<string name="move_books">Kitaplar taşınsın mı?</string>
|
||||
<string name="delete_books">Kitaplar silinsin mi?</string>
|
||||
|
|
@ -27,10 +26,6 @@
|
|||
<string name="edit">Düzenle</string>
|
||||
|
||||
<!-- Dialog Descriptions -->
|
||||
<string name="storage_permission_description">
|
||||
Cihazınızı kitaplar için tarayabilmemiz adına depolama iznine ihtiyacımız var.
|
||||
Bu izin olmadan kitap ekleyemezsiniz.
|
||||
</string>
|
||||
<string name="add_books_description">
|
||||
Eklemek istediğiniz kitapları seçebilirsiniz.
|
||||
Kitabın başlığını ve kapak resmini daha sonra düzenleyebilirsiniz.
|
||||
|
|
@ -57,9 +52,6 @@
|
|||
Geçmişi sildikten sonra kitapların sıralaması sıfırlanacaktır.
|
||||
</string>
|
||||
<!-- Errors -->
|
||||
<string name="error_permission">
|
||||
Cihazınızı kitaplar için tarayabilmemiz adına depolama iznine ihtiyacımız var
|
||||
</string>
|
||||
<string name="error_no_description">
|
||||
Açıklama yok.
|
||||
</string>
|
||||
|
|
@ -99,9 +91,6 @@
|
|||
|
||||
<!-- Action Texts -->
|
||||
<string name="cancel">İptal</string>
|
||||
<string name="grant">Ver</string>
|
||||
<string name="grant_permission">İzin ver</string>
|
||||
<string name="get_help">Yardıma ihtiyacım var</string>
|
||||
<string name="add_book">Kitap ekle</string>
|
||||
<string name="move">Taşı</string>
|
||||
<string name="delete">Sil</string>
|
||||
|
|
@ -539,10 +528,6 @@
|
|||
</string>
|
||||
<string name="start_language_preferences">Dil tercihleri</string>
|
||||
<string name="start_theme_preferences">Tema tercihleri</string>
|
||||
<string name="start_permissions_preferences">İzinler</string>
|
||||
<string name="start_permissions_granted">Verildi</string>
|
||||
<string name="start_permissions_storage">Depolama izni</string>
|
||||
<string name="start_permissions_storage_desc">Cihazınızı tarayıp kitapları bulmak için gereklidir</string>
|
||||
<string name="start_done">Her şey tamam!</string>
|
||||
<string name="start_done_desc">
|
||||
Uygulamanın temel ayarlarını başarıyla tamamladınız.
|
||||
|
|
@ -579,7 +564,6 @@
|
|||
<string name="history_content_desc">Okuma geçmişi</string>
|
||||
<string name="browse_content_desc">Kitap ekle</string>
|
||||
<string name="arrow_content_desc">Ok</string>
|
||||
<string name="required_content_desc">Gerekli</string>
|
||||
<string name="reset_start_content_desc">Başlangıç rehberini sıfırla</string>
|
||||
<string name="open_in_web_content_desc">Web\'de aç</string>
|
||||
<string name="delete_color_preset_content_desc">Renk ön ayarını sil</string>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
<string name="help_screen">Довідка</string>
|
||||
|
||||
<!-- Dialog Labels -->
|
||||
<string name="storage_permission">Надати Дозвіл?</string>
|
||||
<string name="add_books">Додати книги?</string>
|
||||
<string name="move_books">Перемістити книги?</string>
|
||||
<string name="delete_books">Видалити книги?</string>
|
||||
|
|
@ -27,15 +26,14 @@
|
|||
<string name="edit">Редагувати</string>
|
||||
|
||||
<!-- Dialog Descriptions -->
|
||||
<string name="storage_permission_description">Нам потрібен дозвіл на сховище, щоб просканувати Ваш Девайс на наявність книг. Без цього дозволу ви не зможете додати книгу.</string>
|
||||
<string name="add_books_description">Ви можете вибрати книги, які хочете додати. Ви зможете відредагувати Назву та Обкладинку книги пізніше. Процес завантаження може зайняти декілька хвилин.</string>
|
||||
<string name="move_books_description">Виберіть категорію, в яку ви хочете перемістити всі обрані книги(%1$s). Всі обрані книги будуть переміщені в обрану категорію.</string>
|
||||
<string name="delete_books_description">Це видалить всі обрані книги(%1$s) з бази даних. Це не видалить обрані книги з вашого пристрою.</string>
|
||||
<string name="delete_book_description">Це видалить цю книгу з бази даних. Це не видалить цю книгу з вашого пристрою.</string>
|
||||
<string name="move_book_description">Виберіть категорію, в яку ви хочете перемістити цю книгу. Ця книга буде переміщена в обрану категорію.</string>
|
||||
<string name="delete_history_description">Це видалить всю історію з бази даних. Після видалення всієї історії порядок книг буде скинутий.</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_permission">Нам потрібен дозвіл на сховище, щоб просканувати Ваш Девайс на наявність книг</string>
|
||||
<string name="error_no_description">Немає опису.</string>
|
||||
<string name="error_something_went_wrong">Який жах! Щось пішло не так.</string>
|
||||
<string name="error_no_translator">Не знайдено жодного перекладача.</string>
|
||||
|
|
@ -43,12 +41,12 @@
|
|||
<string name="error_no_browser">Не знайдено жодного браузера.</string>
|
||||
<string name="error_no_share_app">Не знайдено застосунка щоб поділитися.</string>
|
||||
<string name="error_could_not_get_text">
|
||||
Файл порожній, пошкоджений або відсутній.
|
||||
Перевірте файл або змініть шлях і спробуйте ще раз.
|
||||
Файл порожній, відсутній або недоступний.
|
||||
Перевірте файл, налаштування сканування або змініть шлях і спробуйте ще раз.
|
||||
</string>
|
||||
<string name="error_could_not_reset_cover">Не вдалося скинути обкладинку.</string>
|
||||
<string name="error_closed_source">Закритий код</string>
|
||||
<string name="error_no_file">Файла не існує</string>
|
||||
<string name="error_no_file">Файла не існує або він недоступний</string>
|
||||
|
||||
<!-- Is Empty messages -->
|
||||
<string name="search_field_empty">Пошук…</string>
|
||||
|
|
@ -63,9 +61,6 @@
|
|||
|
||||
<!-- Action Texts -->
|
||||
<string name="cancel">Відмінити</string>
|
||||
<string name="grant">Надати</string>
|
||||
<string name="grant_permission">Надати дозвіл</string>
|
||||
<string name="get_help">Мені потрібна допомога</string>
|
||||
<string name="add_book">Додати книгу</string>
|
||||
<string name="move">Перемістити</string>
|
||||
<string name="delete">Видалити</string>
|
||||
|
|
@ -82,6 +77,8 @@
|
|||
<string name="share">Поділитися</string>
|
||||
<string name="ok">ОК</string>
|
||||
<string name="change_path">Змінити шлях</string>
|
||||
<string name="add_folder">Додати теку</string>
|
||||
<string name="set_up_scanning">Налаштувати сканування</string>
|
||||
|
||||
<!-- Book -->
|
||||
<string name="unknown_author">Невідомий</string>
|
||||
|
|
@ -128,10 +125,18 @@
|
|||
<string name="progress_reader_settings">Прогрес</string>
|
||||
<string name="theme_appearance_settings">Налаштування теми</string>
|
||||
<string name="colors_appearance_settings">Кольори</string>
|
||||
<string name="scan_browse_settings">Сканування</string>
|
||||
<string name="general_browse_settings">Головні</string>
|
||||
<string name="filter_browse_settings">Фільтр</string>
|
||||
<string name="sort_browse_settings">Сотрування</string>
|
||||
|
||||
<!-- Settings notes -->
|
||||
<string name="browse_scan_option_note">
|
||||
Ці теки використовуються для сканування та отримання файлів у Огляді.
|
||||
Видалення теки тут не видалить фактичні файли чи книги з Бібліотеки.
|
||||
Щоб відкликати доступ, натисніть відповідну кнопку.
|
||||
</string>
|
||||
|
||||
<!-- Settings options -->
|
||||
<!-- General -->
|
||||
<string name="language_option">Мова застосунка</string>
|
||||
|
|
@ -407,10 +412,7 @@
|
|||
<string name="start_welcome_desc">У світ, де слова чарують, вчать та ведуть у неймовірні пригоди! Давайте дещо налаштуємо!</string>
|
||||
<string name="start_language_preferences">Налаштування мови</string>
|
||||
<string name="start_theme_preferences">Налаштування теми</string>
|
||||
<string name="start_permissions_preferences">Дозволи</string>
|
||||
<string name="start_permissions_granted">Надано</string>
|
||||
<string name="start_permissions_storage">Дозвіл на сховище</string>
|
||||
<string name="start_permissions_storage_desc">Потрібний, щоб просканувати ваш пристрій та знайти книги</string>
|
||||
<string name="start_scan_preferences">Налаштування сканування</string>
|
||||
<string name="start_done">Все готово!</string>
|
||||
<string name="start_done_desc">Ви успішно закінчили базове налаштовування застосунку. Бажаєте дізнатися, як користуватися застосунком, прочитавши екран Довідки?</string>
|
||||
|
||||
|
|
@ -444,7 +446,6 @@
|
|||
<string name="history_content_desc">Історія читання</string>
|
||||
<string name="browse_content_desc">Додавайте книги</string>
|
||||
<string name="arrow_content_desc">Стрілка</string>
|
||||
<string name="required_content_desc">Необхідно</string>
|
||||
<string name="reset_start_content_desc">Скинути початковий посібник</string>
|
||||
<string name="open_in_web_content_desc">Відкрити в браузері</string>
|
||||
<string name="delete_color_preset_content_desc">Видалити пресет кольорів</string>
|
||||
|
|
@ -464,5 +465,7 @@
|
|||
<string name="edit_content_desc">Редагувати поле</string>
|
||||
<string name="expand_content_desc">Розгорнути</string>
|
||||
<string name="collapse_content_desc">Згорнути</string>
|
||||
<string name="remove_content_desc">Прибрати</string>
|
||||
<string name="note_content_desc">Примітка</string>
|
||||
|
||||
</resources>
|
||||
|
|
@ -21,14 +21,11 @@
|
|||
<string name="never">永不</string>
|
||||
<string name="color_preset_query">预设 %1$s</string>
|
||||
<string name="cancel">取消</string>
|
||||
<string name="error_permission">我们需要存储权限以扫描您的设备上的书</string>
|
||||
<string name="library_screen">书库</string>
|
||||
<string name="history_screen">历史</string>
|
||||
<string name="browse_screen">浏览</string>
|
||||
<string name="about_screen">关于 app</string>
|
||||
<string name="storage_permission">授予权限?</string>
|
||||
<string name="move_book">移动书籍?</string>
|
||||
<string name="storage_permission_description">我们需要存储权限以扫描您的设备上的书。没有此权限,您将无法添加书。</string>
|
||||
<string name="delete_books_description">所有已选择的书(%1$s)将会从数据库中删除,但不会从您的设备上删除。</string>
|
||||
<string name="delete_book_description">这本书将会从数据库中删除,但不会从您的设备上删除。</string>
|
||||
<string name="move_book_description">选择你想将这本书移动到的分类。这本书将会被移动到指定的分类。</string>
|
||||
|
|
@ -44,8 +41,6 @@
|
|||
<string name="library_empty">阅读您喜欢的书籍并对其进行分类</string>
|
||||
<string name="history_empty">让您读完的最后一本书为您打开阅读之旅的新篇章</string>
|
||||
<string name="color_preset_selected_query">已选择 «%1$s»</string>
|
||||
<string name="grant">授予</string>
|
||||
<string name="grant_permission">授予权限</string>
|
||||
<string name="undo">撤销</string>
|
||||
<string name="next">下一个</string>
|
||||
<string name="unknown_author">未知</string>
|
||||
|
|
@ -55,7 +50,6 @@
|
|||
<string name="already_read_tab">已阅读</string>
|
||||
<string name="add_books">添加书籍?</string>
|
||||
<string name="error_no_browser">未发现浏览器应用。</string>
|
||||
<string name="get_help">我需要帮助</string>
|
||||
<string name="add_book">添加一本书</string>
|
||||
<string name="move">移动</string>
|
||||
<string name="delete">删除</string>
|
||||
|
|
@ -260,11 +254,8 @@
|
|||
<string name="yesterday">昨天</string>
|
||||
<string name="continue_reading_content_desc">继续阅读</string>
|
||||
<string name="cover_image_content_desc">封面图片</string>
|
||||
<string name="start_permissions_preferences">权限</string>
|
||||
<string name="start_done">设置完成!</string>
|
||||
<string name="error_content_desc">错误</string>
|
||||
<string name="start_permissions_granted">已授权</string>
|
||||
<string name="start_permissions_storage">存储权限</string>
|
||||
<string name="start_done_desc">你已经成功完成了应用基本设置。要参阅帮助以获取更多使用技巧吗?</string>
|
||||
<string name="cover_image_not_found_content_desc">未找到封面图片</string>
|
||||
<string name="start_language_preferences">语言偏好</string>
|
||||
|
|
@ -293,7 +284,6 @@
|
|||
<string name="history_content_desc">阅读历史</string>
|
||||
<string name="browse_content_desc">添加书籍</string>
|
||||
<string name="arrow_content_desc">箭头</string>
|
||||
<string name="required_content_desc">必填</string>
|
||||
<string name="reset_start_content_desc">重置引导页面</string>
|
||||
<string name="delete_books_content_desc">删除所选书籍</string>
|
||||
<string name="revert_content_desc">撤销</string>
|
||||
|
|
@ -301,7 +291,6 @@
|
|||
<string name="checkbox_content_desc">复选框</string>
|
||||
<string name="add_files_content_desc">添加文件</string>
|
||||
<string name="select_all_files_content_desc">选择所有文件</string>
|
||||
<string name="start_permissions_storage_desc">需要扫描您的设备来查找书籍</string>
|
||||
<string name="delete_whole_history_content_desc">删除全部历史记录</string>
|
||||
<string name="open_in_web_content_desc">在网页中打开</string>
|
||||
<string name="create_color_preset_content_desc">创建新颜色预设</string>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
<string name="help_screen">Help</string>
|
||||
|
||||
<!-- Dialog Labels -->
|
||||
<string name="storage_permission">Grant Permission?</string>
|
||||
<string name="add_books">Add books?</string>
|
||||
<string name="move_books">Move books?</string>
|
||||
<string name="delete_books">Delete books?</string>
|
||||
|
|
@ -29,10 +28,6 @@
|
|||
<string name="edit">Edit</string>
|
||||
|
||||
<!-- Dialog Descriptions -->
|
||||
<string name="storage_permission_description">
|
||||
We need storage permission to scan Your Device for books.
|
||||
Without this permission, you will not be able to add a book.
|
||||
</string>
|
||||
<string name="add_books_description">
|
||||
You can choose which books you want to add.
|
||||
You can edit Title and Cover image of the book later.
|
||||
|
|
@ -60,26 +55,19 @@
|
|||
</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_permission">
|
||||
We need storage permission to scan Your Device for books
|
||||
</string>
|
||||
<string name="error_no_description">
|
||||
No description.
|
||||
</string>
|
||||
<string name="error_something_went_wrong">
|
||||
Something went wrong.
|
||||
</string>
|
||||
<string name="error_no_description">No description.</string>
|
||||
<string name="error_something_went_wrong">Something went wrong.</string>
|
||||
<string name="error_no_translator">No translator app found.</string>
|
||||
<string name="error_no_dictionary">No dictionary app found.</string>
|
||||
<string name="error_no_browser">No browser app found.</string>
|
||||
<string name="error_no_share_app">No app to share found.</string>
|
||||
<string name="error_could_not_get_text">
|
||||
File is empty, corrupted, or missing.
|
||||
Please verify the file or update the path and try again.
|
||||
File is empty, missing, or inaccessible.
|
||||
Verify the file, scan settings or change the path, then try again.
|
||||
</string>
|
||||
<string name="error_could_not_reset_cover">Could not reset cover image.</string>
|
||||
<string name="error_closed_source">Closed source</string>
|
||||
<string name="error_no_file">File does not exist</string>
|
||||
<string name="error_no_file">File does not exist or inaccessible</string>
|
||||
|
||||
<!-- Is Empty messages -->
|
||||
<string name="search_field_empty">
|
||||
|
|
@ -102,9 +90,6 @@
|
|||
|
||||
<!-- Action Texts -->
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="grant">Grant</string>
|
||||
<string name="grant_permission">Grant permission</string>
|
||||
<string name="get_help">I need help</string>
|
||||
<string name="add_book">Add a book</string>
|
||||
<string name="move">Move</string>
|
||||
<string name="delete">Delete</string>
|
||||
|
|
@ -121,6 +106,8 @@
|
|||
<string name="share">Share</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="change_path">Change path</string>
|
||||
<string name="add_folder">Add folder</string>
|
||||
<string name="set_up_scanning">Set up scanning</string>
|
||||
|
||||
<!-- Book -->
|
||||
<string name="unknown_author">Unknown</string>
|
||||
|
|
@ -167,10 +154,18 @@
|
|||
<string name="progress_reader_settings">Progress</string>
|
||||
<string name="theme_appearance_settings">Theme preferences</string>
|
||||
<string name="colors_appearance_settings">Colors</string>
|
||||
<string name="scan_browse_settings">Scan</string>
|
||||
<string name="general_browse_settings">General</string>
|
||||
<string name="filter_browse_settings">Filter</string>
|
||||
<string name="sort_browse_settings">Sort</string>
|
||||
|
||||
<!-- Settings notes -->
|
||||
<string name="browse_scan_option_note">
|
||||
These folders are used to scan and retrieve files in the Browse.
|
||||
Removing a folder here will not delete actual files or books from the Library.
|
||||
To revoke access, click the corresponding button.
|
||||
</string>
|
||||
|
||||
<!-- Settings options -->
|
||||
<!-- General -->
|
||||
<string name="language_option">App language</string>
|
||||
|
|
@ -545,10 +540,7 @@
|
|||
</string>
|
||||
<string name="start_language_preferences">Language preferences</string>
|
||||
<string name="start_theme_preferences">Theme preferences</string>
|
||||
<string name="start_permissions_preferences">Permissions</string>
|
||||
<string name="start_permissions_granted">Granted</string>
|
||||
<string name="start_permissions_storage">Storage permission</string>
|
||||
<string name="start_permissions_storage_desc">Required to scan your device and find books</string>
|
||||
<string name="start_scan_preferences">Scan preferences</string>
|
||||
<string name="start_done">Everything\'s done!</string>
|
||||
<string name="start_done_desc">
|
||||
You\'re successfully done basic setting of the app.
|
||||
|
|
@ -585,7 +577,6 @@
|
|||
<string name="history_content_desc">Reading history</string>
|
||||
<string name="browse_content_desc">Add books</string>
|
||||
<string name="arrow_content_desc">Arrow</string>
|
||||
<string name="required_content_desc">Required</string>
|
||||
<string name="reset_start_content_desc">Reset start guide</string>
|
||||
<string name="open_in_web_content_desc">Open in web</string>
|
||||
<string name="delete_color_preset_content_desc">Delete color preset</string>
|
||||
|
|
@ -608,5 +599,7 @@
|
|||
<string name="edit_content_desc">Edit field</string>
|
||||
<string name="expand_content_desc">Expand</string>
|
||||
<string name="collapse_content_desc">Collapse</string>
|
||||
<string name="remove_content_desc">Remove</string>
|
||||
<string name="note_content_desc">Note</string>
|
||||
|
||||
</resources>
|
||||
Loading…
Add table
Add a link
Reference in a new issue