From 8cbe14b0e0a0498af0d60241c52054d377da2627 Mon Sep 17 00:00:00 2001 From: Acclorite <140836141+Acclorite@users.noreply.github.com> Date: Sun, 2 Mar 2025 13:09:10 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Storage=20Access=20Framework?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Per-folder storage access * Easily revoke per-folder access * No storage permission * Privacy focus (Milestone #5) Resolves: #38 --- app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 8 - .../book_story/data/parser/DocumentParser.kt | 257 ++++++++------- .../book_story/data/parser/FileParser.kt | 4 +- .../book_story/data/parser/FileParserImpl.kt | 35 +- .../book_story/data/parser/TextParser.kt | 5 +- .../book_story/data/parser/TextParserImpl.kt | 27 +- .../data/parser/epub/EpubFileParser.kt | 14 +- .../data/parser/epub/EpubTextParser.kt | 23 +- .../data/parser/fb2/Fb2FileParser.kt | 67 ++-- .../data/parser/fb2/Fb2TextParser.kt | 168 ---------- .../data/parser/html/HtmlFileParser.kt | 18 +- .../data/parser/html/HtmlTextParser.kt | 12 +- .../data/parser/pdf/PdfFileParser.kt | 12 +- .../data/parser/pdf/PdfTextParser.kt | 26 +- .../data/parser/txt/TxtFileParser.kt | 8 +- .../data/parser/txt/TxtTextParser.kt | 46 +-- .../data/repository/BookRepositoryImpl.kt | 88 +++-- .../repository/FileSystemRepositoryImpl.kt | 165 ++++------ .../repository/PermissionRepositoryImpl.kt | 106 ++---- .../domain/browse/SelectableFile.kt | 9 +- .../book_story/domain/file/CachedFile.kt | 311 ++++++++++++++++++ .../domain/file/CachedFileBuilder.kt | 18 + .../domain/file/CachedFileCompat.kt | 109 ++++++ .../domain/repository/BookRepository.kt | 2 +- .../domain/repository/FileSystemRepository.kt | 6 +- .../domain/repository/PermissionRepository.kt | 16 +- .../domain/use_case/book/InsertBook.kt | 4 +- .../use_case/file_system/GetBookFromFile.kt | 6 +- .../{GetFilesFromDevice.kt => GetFiles.kt} | 4 +- .../GrantPersistableUriPermission.kt | 20 ++ .../permission/GrantStoragePermission.kt | 29 -- .../ReleasePersistableUriPermission.kt | 20 ++ .../book_info/BookInfoDetailsBottomSheet.kt | 26 +- .../presentation/browse/BrowseContent.kt | 19 +- .../presentation/browse/BrowseDialog.kt | 15 - .../browse/BrowseEmptyPlaceholder.kt | 35 +- .../presentation/browse/BrowseGridFileItem.kt | 6 +- .../presentation/browse/BrowseGridLayout.kt | 2 +- .../presentation/browse/BrowseLayout.kt | 2 +- .../presentation/browse/BrowseListFileItem.kt | 6 +- .../presentation/browse/BrowseListLayout.kt | 2 +- .../browse/BrowsePermissionDialog.kt | 52 --- .../presentation/browse/BrowseScaffold.kt | 14 +- .../core/components/dialog/Dialog.kt | 7 +- .../settings/browse/BrowseSettingsCategory.kt | 2 + .../browse/scan/BrowseScanSubcategory.kt | 41 +++ .../scan/components/BrowseScanOption.kt | 203 ++++++++++++ .../scan/components/BrowseScanOptionNote.kt | 21 ++ .../components/SettingsSubcategoryNote.kt | 55 ++++ .../presentation/start/StartContent.kt | 10 - .../presentation/start/StartSettings.kt | 23 +- .../start/StartSettingsBottomBar.kt | 5 +- .../start/StartSettingsLayoutPermissions.kt | 68 ---- .../StartSettingsLayoutPermissionsItem.kt | 100 ------ .../start/StartSettingsLayoutScan.kt | 45 +++ .../start/StartSettingsScaffold.kt | 4 - .../book_story/ui/browse/BrowseEvent.kt | 18 - .../book_story/ui/browse/BrowseModel.kt | 143 +------- .../book_story/ui/browse/BrowseScreen.kt | 32 +- .../book_story/ui/browse/BrowseState.kt | 1 - .../book_story/ui/main/MainActivity.kt | 5 + .../book_story/ui/settings/SettingsEvent.kt | 12 +- .../book_story/ui/settings/SettingsModel.kt | 24 +- .../book_story/ui/start/StartEvent.kt | 26 -- .../book_story/ui/start/StartModel.kt | 92 ------ .../book_story/ui/start/StartScreen.kt | 31 +- .../book_story/ui/start/StartState.kt | 14 - app/src/main/res/values-ar/strings.xml | 11 - app/src/main/res/values-cs/strings.xml | 9 - app/src/main/res/values-de/strings.xml | 11 - app/src/main/res/values-es/strings.xml | 11 - app/src/main/res/values-fr/strings.xml | 11 - app/src/main/res/values-hi/strings.xml | 6 - app/src/main/res/values-it/strings.xml | 11 - app/src/main/res/values-pl/strings.xml | 11 - app/src/main/res/values-pt-rBR/strings.xml | 11 - app/src/main/res/values-ta/strings.xml | 11 - app/src/main/res/values-tr/strings.xml | 16 - app/src/main/res/values-uk/strings.xml | 31 +- app/src/main/res/values-zh-rCN/strings.xml | 11 - app/src/main/res/values/strings.xml | 43 +-- 82 files changed, 1424 insertions(+), 1588 deletions(-) delete mode 100644 app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2TextParser.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/file/CachedFile.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/file/CachedFileBuilder.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/file/CachedFileCompat.kt rename app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/{GetFilesFromDevice.kt => GetFiles.kt} (83%) create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/use_case/permission/GrantPersistableUriPermission.kt delete mode 100644 app/src/main/java/ua/acclorite/book_story/domain/use_case/permission/GrantStoragePermission.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/use_case/permission/ReleasePersistableUriPermission.kt delete mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowsePermissionDialog.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/scan/BrowseScanSubcategory.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/scan/components/BrowseScanOption.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/scan/components/BrowseScanOptionNote.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/settings/components/SettingsSubcategoryNote.kt delete mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutPermissions.kt delete mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutPermissionsItem.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutScan.kt delete mode 100644 app/src/main/java/ua/acclorite/book_story/ui/start/StartEvent.kt delete mode 100644 app/src/main/java/ua/acclorite/book_story/ui/start/StartModel.kt delete mode 100644 app/src/main/java/ua/acclorite/book_story/ui/start/StartState.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d3806401..e6c26922 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -127,8 +127,8 @@ dependencies { // Splash Screen API implementation("androidx.core:core-splashscreen:1.0.1") - // Permission handling - implementation("com.google.accompanist:accompanist-permissions:0.37.0") + // SAF + implementation("com.anggrayudi:storage:2.0.0") // PDF parser implementation("com.tom-roush:pdfbox-android:2.0.27.0") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a47bcc8d..38e42540 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,12 +1,6 @@ - - - - diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/DocumentParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/DocumentParser.kt index 8e54dbf1..13255e45 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/DocumentParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/DocumentParser.kt @@ -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? = null, includeChapter: Boolean = true @@ -40,148 +41,150 @@ class DocumentParser @Inject constructor( val readerText = mutableListOf() var chapterAdded = false - body().apply { - // Remove manual line breaks from all

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

, + 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 's title + select("title").remove() - // Image () - 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 () - 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 () + 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 () + 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() diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParser.kt index d7c4e462..1628a4f7 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParser.kt @@ -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? } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt index 8b1978f0..bf31dfc5 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt @@ -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( } } } -} - - - - - - - - +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt index 11f5e7bd..5b6c22f2 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt @@ -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 + + suspend fun parse(cachedFile: CachedFile): List } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt index 9b519816..d7ac5354 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt @@ -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 { - if (!file.exists()) { - Log.e(TEXT_PARSER, "File does not exist.") + + override suspend fun parse(cachedFile: CachedFile): List { + 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 -> { diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt index f2717a55..9d8f5b0d 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt @@ -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) ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt index 27701c1f..fc5c8309 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt @@ -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 { - Log.i(EPUB_TAG, "Started EPUB parsing: ${file.name}.") + override suspend fun parse(cachedFile: CachedFile): List { + Log.i(EPUB_TAG, "Started EPUB parsing: ${cachedFile.name}.") return try { yield() var readerText = listOf() + 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( diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2FileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2FileParser.kt index 6ae3191a..ffe4caf4 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2FileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2FileParser.kt @@ -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 - } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2TextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2TextParser.kt deleted file mode 100644 index 96b2f820..00000000 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/fb2/Fb2TextParser.kt +++ /dev/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 { - 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() - - val bodyNodes = document.getElementsByTagName("body") - if (bodyNodes.length == 0) { - return emptyList() - } - - yield() - - val unformattedLines = mutableListOf() - 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() - 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().isEmpty() || - readerText.filterIsInstance().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 { - val list = mutableListOf() - for (i in 0 until this.length) { - list.add(this.item(i) as Element) - } - return list - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlFileParser.kt index 856538ee..3aa8174e 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlFileParser.kt @@ -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 diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlTextParser.kt index 985f8989..7c6b0256 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlTextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/html/HtmlTextParser.kt @@ -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 { - Log.i(HTML_TAG, "Started HTML parsing: ${file.name}.") + override suspend fun parse(cachedFile: CachedFile): List { + 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().isEmpty() || readerText.filterIsInstance().isEmpty() ) { diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt index 1b6d35c3..f4d95ba3 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt @@ -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 diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt index 9ba7dfea..6ec5e951 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt @@ -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 { - Log.i(PDF_TAG, "Started PDF parsing: ${file.name}.") + override suspend fun parse(cachedFile: CachedFile): List { + 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 = "
" - PDDocument.load(file).use { + PDDocument.load(cachedFile.openInputStream()).use { oldText = pdfStripper.getText(it) .replace("\r", "") } @@ -160,20 +160,4 @@ class PdfTextParser @Inject constructor( emptyList() } } -} - - - - - - - - - - - - - - - - +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt index d3564f65..32a18202 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt @@ -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 diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt index dec6c109..9c7f025d 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt @@ -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 { - Log.i(TXT_TAG, "Started TXT parsing: ${file.name}.") + override suspend fun parse(cachedFile: CachedFile): List { + Log.i(TXT_TAG, "Started TXT parsing: ${cachedFile.name}.") return try { val readerText = mutableListOf() 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) - ) - ) + } } } } diff --git a/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt index 456d8f60..9541f657 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt @@ -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().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) diff --git a/app/src/main/java/ua/acclorite/book_story/data/repository/FileSystemRepositoryImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/repository/FileSystemRepositoryImpl.kt index de9d2ad1..2e005dfb 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/repository/FileSystemRepositoryImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/repository/FileSystemRepositoryImpl.kt @@ -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 { - Log.i(GET_FILES_FROM_DEVICE, "Getting files from device by query: \"$query\".") + override suspend fun getFiles(query: String): List { + 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 { - 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 { + val selectableFiles = mutableListOf() + + walk { file -> + if (!file.isValid()) return@walk + + selectableFiles.add( + SelectableFile( + data = file, + selected = false + ) + ) + } + + return selectableFiles + } + + fun getAllStorages(): List { + 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 { - val storageDirectories = mutableListOf() + return try { + val storages = getAllStorages() + val files = mutableListOf() - 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() - - 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) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/data/repository/PermissionRepositoryImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/repository/PermissionRepositoryImpl.kt index d3ea5f4b..198ba4a5 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/repository/PermissionRepositoryImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/repository/PermissionRepositoryImpl.kt @@ -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 } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/browse/SelectableFile.kt b/app/src/main/java/ua/acclorite/book_story/domain/browse/SelectableFile.kt index 4b190e14..d0550bb2 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/browse/SelectableFile.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/browse/SelectableFile.kt @@ -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 ) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/file/CachedFile.kt b/app/src/main/java/ua/acclorite/book_story/domain/file/CachedFile.kt new file mode 100644 index 00000000..a584aece --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/file/CachedFile.kt @@ -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 { + if (!isDirectory || !canAccess()) return emptyList() + + val cachedFiles = mutableListOf() + + 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 { + val cachedFiles = mutableListOf() + + 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().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() + + 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('/') ?: "" + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/file/CachedFileBuilder.kt b/app/src/main/java/ua/acclorite/book_story/domain/file/CachedFileBuilder.kt new file mode 100644 index 00000000..a12699c8 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/file/CachedFileBuilder.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/file/CachedFileCompat.kt b/app/src/main/java/ua/acclorite/book_story/domain/file/CachedFileCompat.kt new file mode 100644 index 00000000..146a54d2 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/file/CachedFileCompat.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt b/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt index a712c319..dd647021 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt @@ -27,7 +27,7 @@ interface BookRepository { suspend fun insertBook( bookWithCover: BookWithCover - ): Boolean + ) suspend fun updateBook( book: Book diff --git a/app/src/main/java/ua/acclorite/book_story/domain/repository/FileSystemRepository.kt b/app/src/main/java/ua/acclorite/book_story/domain/repository/FileSystemRepository.kt index 5033fae7..5e4a31eb 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/repository/FileSystemRepository.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/repository/FileSystemRepository.kt @@ -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 suspend fun getBookFromFile( - file: File + cachedFile: CachedFile ): NullableBook } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/repository/PermissionRepository.kt b/app/src/main/java/ua/acclorite/book_story/domain/repository/PermissionRepository.kt index f5e4e354..05014e9e 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/repository/PermissionRepository.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/repository/PermissionRepository.kt @@ -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 + ) } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/InsertBook.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/InsertBook.kt index cbedbe13..18af20f7 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/InsertBook.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/book/InsertBook.kt @@ -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) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/GetBookFromFile.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/GetBookFromFile.kt index f6a0df31..794f3bfa 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/GetBookFromFile.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/GetBookFromFile.kt @@ -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) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/GetFilesFromDevice.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/GetFiles.kt similarity index 83% rename from app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/GetFilesFromDevice.kt rename to app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/GetFiles.kt index 22eaec83..a108be05 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/GetFilesFromDevice.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/file_system/GetFiles.kt @@ -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 { - return repository.getFilesFromDevice(query) + return repository.getFiles(query) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/permission/GrantPersistableUriPermission.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/permission/GrantPersistableUriPermission.kt new file mode 100644 index 00000000..2d802f50 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/permission/GrantPersistableUriPermission.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/permission/GrantStoragePermission.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/permission/GrantStoragePermission.kt deleted file mode 100644 index f712675b..00000000 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/permission/GrantStoragePermission.kt +++ /dev/null @@ -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 - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/permission/ReleasePersistableUriPermission.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/permission/ReleasePersistableUriPermission.kt new file mode 100644 index 00000000..b7c7c106 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/permission/ReleasePersistableUriPermission.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDetailsBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDetailsBottomSheet.kt index d9dac45e..b0490d05 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDetailsBottomSheet.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/book_info/BookInfoDetailsBottomSheet.kt @@ -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) + } } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseContent.kt index 28b0cf23..730bde61 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseContent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseContent.kt @@ -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, selectedBooksAddDialog: List, 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( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseDialog.kt index 849de3a4..a83ded1f 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseDialog.kt @@ -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, 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, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseEmptyPlaceholder.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseEmptyPlaceholder.kt index ab41e7f0..66561103 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseEmptyPlaceholder.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseEmptyPlaceholder.kt @@ -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() } ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridFileItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridFileItem.kt index 47eb8d97..757f0496 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridFileItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridFileItem.kt @@ -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, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridLayout.kt index 9541adfe..1bfddbd7 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridLayout.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseGridLayout.kt @@ -50,7 +50,7 @@ fun BrowseGridLayout( items( group.files, - key = { it.path } + key = { it.data.path } ) { selectableFile -> Box(Modifier.animateItem()) { itemContent(selectableFile, group.files) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseLayout.kt index 37f254a5..070a9ffc 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseLayout.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseLayout.kt @@ -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, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListFileItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListFileItem.kt index 4bfa52c5..578ebcd1 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListFileItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListFileItem.kt @@ -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, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListLayout.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListLayout.kt index ff762dc1..0c3def2e 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListLayout.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseListLayout.kt @@ -44,7 +44,7 @@ fun BrowseListLayout( items( group.files, - key = { it.path } + key = { it.data.path } ) { selectableFile -> Box(Modifier.animateItem()) { itemContent(selectableFile, group.files) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowsePermissionDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowsePermissionDialog.kt deleted file mode 100644 index 6564c55f..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowsePermissionDialog.kt +++ /dev/null @@ -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 - ) -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseScaffold.kt index 75502ef0..d4d3ae91 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseScaffold.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/browse/BrowseScaffold.kt @@ -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, 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( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/Dialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/Dialog.kt index 52b6e7b9..8f91d055 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/Dialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/core/components/dialog/Dialog.kt @@ -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, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsCategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsCategory.kt index b3e9169c..c1524218 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsCategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/BrowseSettingsCategory.kt @@ -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( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/scan/BrowseScanSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/scan/BrowseScanSubcategory.kt new file mode 100644 index 00000000..20042ee9 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/scan/BrowseScanSubcategory.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/scan/components/BrowseScanOption.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/scan/components/BrowseScanOption.kt new file mode 100644 index 00000000..ed7aab7e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/scan/components/BrowseScanOption.kt @@ -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() + val context = LocalContext.current + + fun getPersistedUriPermissions(): List { + return context.contentResolver?.persistedUriPermissions.let { permissions -> + if (permissions.isNullOrEmpty()) return@let emptyList() + permissions.sortedBy { it.uri.path?.lowercase() } + } + } + + val persistedUriPermissions = remember { + mutableStateListOf().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 + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/scan/components/BrowseScanOptionNote.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/scan/components/BrowseScanOptionNote.kt new file mode 100644 index 00000000..b48c55e5 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/browse/scan/components/BrowseScanOptionNote.kt @@ -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 + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/settings/components/SettingsSubcategoryNote.kt b/app/src/main/java/ua/acclorite/book_story/presentation/settings/components/SettingsSubcategoryNote.kt new file mode 100644 index 00000000..fbbb36c9 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/settings/components/SettingsSubcategoryNote.kt @@ -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 + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartContent.kt index fc3b69d6..5e622a44 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartContent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartContent.kt @@ -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, 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 ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettings.kt index 9ba8c84f..531549df 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettings.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettings.kt @@ -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, 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() } } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsBottomBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsBottomBar.kt index a7a8898f..d6b8fd7d 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsBottomBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsBottomBar.kt @@ -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)) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutPermissions.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutPermissions.kt deleted file mode 100644 index a21c9a5f..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutPermissions.kt +++ /dev/null @@ -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)) - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutPermissionsItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutPermissionsItem.kt deleted file mode 100644 index 6987e0fd..00000000 --- a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutPermissionsItem.kt +++ /dev/null @@ -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) - ) - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutScan.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutScan.kt new file mode 100644 index 00000000..63e2da4d --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsLayoutScan.kt @@ -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)) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsScaffold.kt b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsScaffold.kt index d373e665..8c44b861 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsScaffold.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/start/StartSettingsScaffold.kt @@ -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 ) } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseEvent.kt b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseEvent.kt index 54dde992..5b07e12b 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseEvent.kt @@ -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() diff --git a/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseModel.kt index 9f537abb..bcb3681c 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseModel.kt @@ -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, 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 { 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 } } } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseScreen.kt index 4d7b10b5..148414d2 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseScreen.kt @@ -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) }, ) } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseState.kt b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseState.kt index 9c2405ba..5cee0b2e 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseState.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/browse/BrowseState.kt @@ -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, diff --git a/app/src/main/java/ua/acclorite/book_story/ui/main/MainActivity.kt b/app/src/main/java/ua/acclorite/book_story/ui/main/MainActivity.kt index d625025d..3ac1ce04 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/main/MainActivity.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/main/MainActivity.kt @@ -169,4 +169,9 @@ class MainActivity : AppCompatActivity() { } } } + + override fun onDestroy() { + cacheDir.deleteRecursively() + super.onDestroy() + } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsEvent.kt b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsEvent.kt index f8f256e4..de004a05 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsEvent.kt @@ -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() diff --git a/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsModel.kt index 3a958cbc..3c952aad 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/settings/SettingsModel.kt @@ -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() diff --git a/app/src/main/java/ua/acclorite/book_story/ui/start/StartEvent.kt b/app/src/main/java/ua/acclorite/book_story/ui/start/StartEvent.kt deleted file mode 100644 index 5111e65a..00000000 --- a/app/src/main/java/ua/acclorite/book_story/ui/start/StartEvent.kt +++ /dev/null @@ -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() -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/start/StartModel.kt b/app/src/main/java/ua/acclorite/book_story/ui/start/StartModel.kt deleted file mode 100644 index 04703f95..00000000 --- a/app/src/main/java/ua/acclorite/book_story/ui/start/StartModel.kt +++ /dev/null @@ -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 MutableStateFlow.update(function: (T) -> T) { - mutex.withLock { - yield() - this.value = function(this.value) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/ui/start/StartScreen.kt b/app/src/main/java/ua/acclorite/book_story/ui/start/StartScreen.kt index b94fae97..854298f6 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/start/StartScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/ui/start/StartScreen.kt @@ -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() val mainModel = hiltViewModel() - 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 } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/start/StartState.kt b/app/src/main/java/ua/acclorite/book_story/ui/start/StartState.kt deleted file mode 100644 index b2dede29..00000000 --- a/app/src/main/java/ua/acclorite/book_story/ui/start/StartState.kt +++ /dev/null @@ -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 -) \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index ccb4709e..386facf0 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -9,14 +9,12 @@ التاريخ يمكنك اختيار أي الكُتب تريد إضافتها. يمكنك تعديل العنوان و صورة غلاف الكتاب لاحقا. عملية التحميل يمكن أن تستغرق حتى بضع دقائق. - نحتاج إذن التخزين لفحص جهازك من أجل الكُتب. من دون هذا الإذن، لن تكون قادرا على إضافة كتاب. اختر الفئة التي تريد نقل هذا الكتاب إليها. سيتم نقل هذا الكتاب إلى الفئة المُحدّدة. المكتبة تصفح الإعدادات عن التطبيق مساعدة - منح الإذن؟ إضافة الكُتب؟ نقل الكُتب؟ حذف الكُتب؟ @@ -36,9 +34,6 @@ دع آخر كتاب قرأته يفتح صفحات قصة جديدة عظيمة من رحلة قرائتك غير قادر على إعادة ضبط صورة الغلاف. إلغاء - امنح - امنح الإذن - أحتاج المساعدة أضف كتابا˝ أبدا˝ تراجع @@ -51,7 +46,6 @@ لغة التطبيق سمة داكنة التاريخ - نحتاج إذن التخزين لفحص جهازك من أجل الكُتب لا يوجد وصف. قم بتوسيع مجموعة كتب مكتبتك باستخدام تنزيلاتك حُددت %1$s @@ -301,7 +295,6 @@ إلى عالم حيث الكلمات تسحر وتعلم وتقود إلى مغامرات لا تصدق! دعنا نرتب بعض الأمور! مرحبًا بك في Book\'s Story! احذف الكُتب المُحدّدة - مطلوب مسح جهازك والعثور على الكُتب احذف إعداد اللون المسبق أنشئ إعداد لون مسبق جديد اخلط الألوان @@ -333,7 +326,6 @@ ارجع إعدادات القارئ تطبيق الهامش على منطقة القطع - مطلوب خطأ لم يتم العثور غلاف صورة المزيد @@ -341,11 +333,8 @@ نجح تغيير مسار الملف. تراجع سهم - منحت - إذن التخزين أمس ترتيب الفرز - الأذونات اليوم مربّع تأشير ملف تعريف GitHub diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index d6579d0b..486d6e47 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -61,7 +61,6 @@ Pomozte překládat Filtr Čtečka - Potřebuji pomoc List Úplně tmavé Smazat knihu? @@ -80,7 +79,6 @@ Procházet Žádná aplikace pro sdílení nebyla nalezena. Procházet - Povolit oprávnění Velikost mřížky Smazat knihy? Normální @@ -137,11 +135,9 @@ O aplikaci Přidat knihy? Přesunout knihu? - Potřebujeme povolení, abychom mohli skenovat knihy na Vašem úložišti. Bez povolení si nebudete moci přidat jakoukoli knihu. Smaže to tuto knihu z databáze. Nesmaže to na vašem zařízení. Žádný popis. Něco se pokazilo. - Povolit Přidat knihu Kopírovat Přeložit @@ -195,7 +191,6 @@ Vlastní jas obrazovky Citlivost Nastavení - Vyžadováno, abychom mohli skenoval Vaše úložiště a hledat knihy Preference jazyka Preference témat Historie čtení @@ -203,7 +198,6 @@ Obálka Smazat vybrané knihy Přidat knihy - Povoleno Přesunout vybrané knihy Vítejte v Book\'s Story! Jak přesunu nebo smažu knihy? @@ -219,14 +213,11 @@ Jít zpět Pokračovat ve čtení Vaše knihovna - Vyžadován Nastavení čtečky Smazat celou historii Pojmenujete nastavení… Knihovna Ve světe, kde slova okouzlují, učí a vedou k neuvěřitelným dobrodružstvím! Pojďme nastavit pár věcí! - Oprávnění úložiště - Oprávnění Makemake A je to! Dnes diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4e882379..82763d08 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -28,9 +28,6 @@ %1$s ausgewählt Ausgewählt «%1$s» Abbrechen - Gewähren - Erteilen der Berechtigung - Ich brauche Hilfe Buch hinzufügen Verschieben Niemals @@ -120,9 +117,6 @@ Bibliothek Wie passe ich den Reader an? 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 - Gewährt - Speicherberechtigung - Erforderlich, um Ihr Gerät zu scannen und Bücher zu finden Alles ist erledigt! Sie haben die Grundeinstellungen der App erfolgreich abgeschlossen. Möchten Sie anhand des Hilfebildschirms erfahren, wie Sie die App verwenden? Heute @@ -147,12 +141,9 @@ App-Symbol 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 Hilfe - Berechtigung erteilen? Bücher hinzufügen? - Wir benötigen eine Speicherberechtigung, um Ihr Gerät nach Büchern zu durchsuchen. Ohne diese Berechtigung können Sie kein Buch hinzufügen. 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. 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. - Wir benötigen eine Speicherberechtigung, um Ihr Gerät nach Büchern zu durchsuchen Etwas ist schief gelaufen. Keine Übersetzungsapp gefunden. Keine Browserapp gefunden. @@ -214,13 +205,11 @@ In eine Welt, in der Worte verzaubern, lehren und zu unglaublichen Abenteuern führen! Lassen Sie uns ein paar Dinge vorbereiten! Spracheinstellungen Themeneinstellungen - Berechtigungen Lösche Verlaufselement Deine Bibliothek Leseverlauf Bücher hinzufügen Pfeil - Vorausgesetzt Startanleitung zurücksetzen Im Internet öffnen Farbvoreinstellung löschen diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 9bab7393..95ee706d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -13,7 +13,6 @@ Ajustes Acerca de la aplicación Ayuda - ¿Conceder permiso? ¿Añadir libros? ¿Mover libros? ¿Borrar libros? @@ -23,15 +22,12 @@ 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. Elija la categoría a la que quiere mover este libro. Este libro será trasladado a la categoría seleccionada. Eliminará todo el historial de la base de datos. Después de borrar todo el historial, el orden de los libros se restablecerá. - Necesitamos permiso de almacenamiento para escanear su dispositivo en busca de libros Sin descripción. Algo salió mal. No se encontró ninguna aplicación de navegador. Fuente cerrada %1$s seleccionado Cancelar - Conceder - Necesito ayuda Añadir un libro Mover Cambiar portada @@ -75,11 +71,9 @@ 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! Configuración del idioma Preferencias de tema - Permisos ¡Bienvenid@ a Book\'s Story! Arrastrar Borrar - Necesitamos permiso de almacenamiento para escanear su dispositivo en busca de libros. Sin este permino, no podrá añadir libros. Eliminará todos los libros seleccionados(%1$s) de la base de datos. No eliminará los libros seleccionados de su dispositivo. 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. Eliminará este libro de la base de datos. No eliminará este libro de su dispositivo. @@ -90,7 +84,6 @@ Lea sus libros favoritos y ordénelos en categorías Deje que su último libro leído abra las páginas de una nueva gran historia en su viaje de lectura Traducir - Conceder permiso Nunca Copiar Hecho @@ -179,11 +172,8 @@ Biblioteca ¿Cómo utilizo la traducción de doble clic? ¡Hacia un mundo donde las palabras cautivan, enseñan y conducen a increíbles aventuras! ¡Vamos a preparar algunas cosas! - Concedido ¡Todo está listo! - Permiso de almacenamiento 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. - Necesario para escanear tu dispositivo y encontrar los libros Ajustes Ir al Historial @@ -212,7 +202,6 @@ Eliminar elemento del historial Tu biblioteca Flecha - Necesario Abrir en la web Colores aleatorios Ajustes del lector diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 246b84ff..47459c2f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -13,7 +13,6 @@ Aide Supprimer des livres ? Déplacer des livres ? - Autoriser ? Supprimer le livre ? Déplacer le livre ? Effacer l\'historique de lecture ? @@ -21,7 +20,6 @@ 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. Cela supprimera ce livre de la base de données. Ceci ne le supprimera pas de votre appareil. Choisissez la catégorie dans laquelle vous souhaitez déplacer ce livre. Ce dernier sera déplacé vers la catégorie choisie. - Nous avons besoin de l\'autorisation de stockage pour scanner votre appareil à la recherche de livres Aucune application de traduction n\'a été trouvée. Aucune application de dictionnaire n\'a été trouvée. Aucune application pour partager n\'a été trouvée. @@ -178,10 +176,6 @@ Pour un monde où les mots charment, enseignent et mènent à des aventures incroyables! Mettons les choses au point! Préférences de langue Préférences de thèmes - Permissions - Autorisé - Permission de Stockage - Nécessaire pour scanner votre appareil et trouver des livres Tout est prêt! Hier Poursuivre la lecture @@ -206,7 +200,6 @@ Votre librairie Ajouter les livres Flèche - Requis Guide de réinitialisation Ouvrir en ligne Supprimer le préréglage de couleur @@ -214,7 +207,6 @@ Tryzub Profile Github Glisser - 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. 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. Ajouter des livres ? Pas de description. @@ -224,9 +216,6 @@ Le contenu du livre est vide. Veuillez vous assurer que le contenu du livre n\'est pas vide et de le mettre à jour. Laissez le dernier livre que vous avez lu ouvrir les pages d\'une grande et nouvelle histoire dans votre aventure de lecture Annuler - Accorder la permission - Autoriser - J\'ai besoin d\'aide Ajouter un livre Couleurs Paramètres généraux diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 4ae38388..44298d97 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -11,7 +11,6 @@ ऐप के बारे में जानकारी डिफ़ॉल्ट सहायता - अनुमति दें? किताबें शामिल करें ? किताबें स्थानांतरित करें? किताब हटाएं? @@ -25,9 +24,6 @@ %1$s चुना हुआ है «%1$s» चुना गया है रद्द करें - अनुमति दें - अनुदान - मुझे सहायता की आवश्यकता है एक किताब जोड़ें कभी नहीं पूर्ववत् करें @@ -81,12 +77,10 @@ सिस्टम फ़िल्टर आप चुन सकते हैं कि आप कौन सी किताबें जोड़ना चाहते हैं। आप बाद में किताब का शीर्षक और कवर छवि संपादित कर सकते हैं। लोडिंग प्रक्रिया में कई मिनट लग सकते हैं। - हमें आपकी डिवाइस को पुस्तकों के लिए स्कैन करने के लिए संग्रहण अनुमति की आवश्यकता है। इस अनुमति के बिना, आप कोई पुस्तक नहीं डाल सकेंगे । यह डेटाबेस से सभी चयनित पुस्तकों(%1$s) को हटा देगा। यह आपके डिवाइस से चयनित पुस्तकों को नहीं हटाएगा। यह इस पुस्तक को डेटाबेस से हटा देगा. यह आपकी डिवाइस से इस पुस्तक को नहीं हटाएगा। वह श्रेणी चुनें जिसमें आप इस पुस्तक को ले जाना चाहते हैं। यह पुस्तक चयनित श्रेणी में ले जाई जाएगी। यह डेटाबेस से पूरा इतिहास मिटा देगा। पूरा इतिहास मिटाने के बाद, किताबों का क्रम फिर से सेट हो जाएगा। - हमें किताबों के लिए आपके डिवाइस को स्कैन करने के लिए भंडारण अनुमति की आवश्यकता है कोई वर्णन नहीं। कोई शब्दकोश ऐप नहीं मिला । कोई ब्राउज़र ऐप नहीं मिला । diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 461dae9b..363d3a1c 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -13,7 +13,6 @@ Sposta Annulla Traduci - Concedi permesso Esplora Elimina immagine di copertina Testo @@ -52,18 +51,14 @@ Preferenze tema Indietro Spaziatura dei paragrafi - Concedi permesso? Corsivo Nessuna app traduttore trovata. - Permessi Cronologia lettura - Richiesto Versione app Tutti i libri selezionati sono stati spostati correttamente. Nessuna app browser trovata. Cancellare la cronologia di lettura? Nessuna app dizionario trovata. - 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. Fatto Condividi Sì, vai ad Aiuto @@ -98,7 +93,6 @@ Eliminerà tutti i libri selezionati (%1$s) dal database. Non eliminerà i libri selezionati dal tuo dispositivo. Nessuna descrizione. Qualcosa è andato storto. - Abbiamo bisogno del permesso di archiviazione per cercare i libri nel tuo dispositivo Leggi i tuoi libri preferiti e ordinali per categoria Ordina Colori @@ -124,7 +118,6 @@ Eliminare libri? Copia Generale - Concedi Aiuta a tradurre Venere Dizionario @@ -132,7 +125,6 @@ Contrasto tema Cerca Terra - Ho bisogno di aiuto Alto Autore Lista @@ -287,9 +279,6 @@ Filtro e seleziona la categoria Lettore per personalizzare le impostazioni generali oppure la categoria Aspetto per personalizzare le impostazioni Colori. Come si usa la traduzione con doppio clic? - Concesso - Autorizzazione di archiviazione - Necessario per scansionare il tuo dispositivo e trovare i libri Hai completato con successo le impostazioni di base dell\'app. Vuoi imparare a usare l\'app leggendo la schermata di Aiuto? Aggiungi file La tua libreria diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index c25c233e..4393ebc0 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -33,7 +33,6 @@ Biblioteka Przeglądaj Anuluj - Udziel Usuń Sortuj Czerwony @@ -58,7 +57,6 @@ Planowanie Szybka zmiana ustawienia koloru O aplikacji - Przyznać dostęp? Dodać książki? Usunąć książki? Usunąć książkę? @@ -67,7 +65,6 @@ Wybierz kategorię, do której chcesz przenieść wszystkie wybrane książki (%1$s). Wszystkie wybrane książki zostaną przeniesione do wybranej kategorii. To spowoduje usunięcie tej książki z bazy danych, ale nie usunie jej z twojego urządzenia. To spowoduje usuniecie całej historii z bazy danych. Po usunięciu całej historii kolejność książek zostanie zresetowana. - Potrzebujemy uprawnień do przechowywania, aby przeskanować Twoje urządzenie w poszukiwaniu książek Brak opisu. Coś poszło nie tak. Nie znaleziono aplikacji słownikowej. @@ -129,15 +126,12 @@ Porzucone Tryb wyświetlania 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. - Potrzebujemy uprawnień do przechowywania, aby przeskanować Twoje urządzenie w poszukiwaniu książek. Bez tego pozwolenia nie będziesz mógł dodać książki. Nie znaleziono aplikacji przeglądarki. Jowisz Saturn Wenus Uran Czytelnik - Udziel zgody - Potrzebuję pomocy Dodaj książkę Przetłumacz Motyw, kolory @@ -182,7 +176,6 @@ Wnieśli wkład Wczoraj Wybrane - Wymagane do przeskanowania urządzenia i znalezienia książek Zresetuj przewodnik startu Wysoki Obraz okładki został pomyślnie zmieniony. @@ -209,7 +202,6 @@ 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. Jak mogę tworzyć i modyfikować ustawienia kolorów? Preferencje języka - Uprawnienie do przechowywania Dzisiaj Wróć Obraz okładki @@ -219,7 +211,6 @@ Usuń element historii Twoja biblioteka Strzałka - Wymagane Otwórz w sieci Przeciągnij 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. @@ -227,8 +218,6 @@ 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ń ». 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. Ustawienia wyglądu - Uprawnienia - Przyznane Pomyślnie wykonałeś podstawowe ustawienie aplikacji. Czy chcesz dowiedzieć się, jak korzystać z aplikacji, czytając Pomoc? Plik Wybierz wszystkie pliki diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 7a21f2ab..43b52b34 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -24,7 +24,6 @@ Histórico Explorar Tela cheia - Permitir Feito Desabilitado Apagar livros? @@ -56,7 +55,6 @@ Miscelânea Desabilitado Adicionar livros? - 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. Apagar Desfazer Organizar @@ -69,7 +67,6 @@ Sistema Padrão Normal - Dar permissão? Apagar histórico de leitura? Autor Azul @@ -106,16 +103,13 @@ Arquivo Erro Pesquisar - Permissões Copiado - Requerido Justificar Escolha a categoria da qual você quer mover todos os livros selecionados(%1$s). Todos os livros selecionados serão movidos para essa categoria. Isto irá apagar todos os livros selecionados(%1$s) da base de dados. Isso não irá apagar os livros do seu dispositivo. Centro Configurações Reverter - Permitir Configurações Alfabeticamente Selecionado @@ -160,7 +154,6 @@ por linha Lido Tamanho da grade - Dar permissão Sem descrição. %1$s selecionado Código fechado @@ -183,7 +176,6 @@ Continuar a ler Voltar Prefêrencias linguísticas - Permissão de armazenamento O que há de novo Ajude a traduzir Está tudo feito! @@ -200,7 +192,6 @@ Perfil do Github Orientação da tela Algo deu errado. - Preciso d eajuda Trocar imagem da capa Fonte, texto, tradutor Geral, filtro, ordem @@ -223,7 +214,6 @@ Sensibilidade Desativar Book\'s Story v%1$s - Precisamos de permissão de armazenamento para escanear seu dispositivo em busca de livros Nenhum aplicativo de navegador encontrado. Nenhum aplicativo para compartilhar encontrado. Nenhum aplicativo de dicionário encontrado. @@ -306,7 +296,6 @@ Ceres Esse livro foi apagado com sucesso. Ir para - Necessário para escanear seu dispositivo e encontrar livros Ir para Redefinir guia de introdução Tamanho da fonte diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index eff84716..0ebfc2be 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -19,9 +19,6 @@ முன்னமைக்கப்பட்ட %1$s தேர்ந்தெடுக்கப்பட்ட «%1$s» ரத்துசெய் - மானியம் - இசைவு வழங்கவும் - எனக்கு உதவி தேவை நீக்கு அடுத்தது ஆம், உதவிக்குச் செல்லுங்கள் @@ -83,7 +80,6 @@ பூட்டப்பட்ட நிலப்பரப்பு தெரியவில்லை செல்லுங்கள் - சேமிப்பக இசைவு பயன்பாட்டு பதிப்பு திறந்த மூல உரிமங்கள் வரலாறு @@ -97,13 +93,11 @@ எழுத்துருக்கள் நீங்கள் படித்த உங்கள் கடைசி நூல் உங்கள் வாசிப்பு பயணத்தின் புதிய சிறந்த கதையின் பக்கங்களைத் திறக்கட்டும் பங்கு - அனுமதிகள் ஒருபோதும் காட்சி முறை கோப்பு வடிவம் இயல்புநிலை வாசிப்பு வரலாற்றை நீக்கவா? - புத்தகங்களுக்காக உங்கள் சாதனத்தை ச்கேன் செய்ய எங்களுக்கு சேமிப்பக இசைவு தேவை விளக்கம் இல்லை. %1$s தேர்ந்தெடுக்கப்பட்டன செயல்தவிர் @@ -161,7 +155,6 @@ இன்று பிழை புத்தகங்களைச் சேர்க்கவும் - தேவை வலையில் திறந்திருக்கும் வண்ண முன்னமைவை நீக்கு புதிய வண்ண முன்னமைவை உருவாக்கவும் @@ -179,12 +172,10 @@ அமைப்புகள் பயன்பாடு பற்றி உதவி - இசைவு வழங்கவா? புத்தகங்களை நகர்த்தவா? புத்தகங்களை நீக்கவா? புத்தகத்தை நீக்கவா? புத்தகத்தை நகர்த்தவா? - புத்தகங்களுக்காக உங்கள் சாதனத்தை ச்கேன் செய்ய எங்களுக்கு சேமிப்பக இசைவு தேவை. இந்த அனுமதியின்றி, நீங்கள் ஒரு புத்தகத்தைச் சேர்க்க முடியாது. நீங்கள் எந்த புத்தகங்களைச் சேர்க்க விரும்புகிறீர்கள் என்பதை நீங்கள் தேர்வு செய்யலாம். புத்தகத்தின் தலைப்பு மற்றும் படத்தை பின்னர் திருத்தலாம். ஏற்றுதல் செயல்முறை பல நிமிடங்கள் வரை ஆகலாம். தேர்ந்தெடுக்கப்பட்ட அனைத்து புத்தகங்களையும் (%1$s) நகர்த்த விரும்பும் வகையைத் தேர்வுசெய்க. தேர்ந்தெடுக்கப்பட்ட அனைத்து புத்தகங்களும் தேர்ந்தெடுக்கப்பட்ட வகைக்கு மாற்றப்படும். இது தரவுத்தளத்திலிருந்து தேர்ந்தெடுக்கப்பட்ட அனைத்து புத்தகங்களையும் (%1$s) நீக்கிவிடும். இது உங்கள் சாதனத்திலிருந்து தேர்ந்தெடுக்கப்பட்ட புத்தகங்களை நீக்காது. @@ -290,8 +281,6 @@ தோற்ற அமைப்புகளுக்குச் செல்லுங்கள் (பார்க்க: வாசகரை எவ்வாறு தனிப்பயனாக்குவது?). «வண்ண முன்னமைவு» கீழ் «வண்ணங்கள்» துணைப்பிரிவு ஆகியவற்றின் கீழ் பெட்டியில் உள்ள அனைத்து வண்ண முன்னமைவின் அமைப்புகளையும் நீங்கள் மாற்றலாம். புதிய வண்ண முன்னமைவை உருவாக்க, «சேர்» ஐகான்-பொன்ட்டனைக் சொடுக்கு செய்க. ஒரு வண்ண முன்னமைவை மாற்ற பல வழிகள் உள்ளன: உரை புலத்தைக் சொடுக்கு செய்வதன் மூலம் நீங்கள் வண்ண முன்னமைவின் பெயரை மாற்றலாம், ச்லைடர்களை நகர்த்துவதன் மூலம் வண்ண முன்னமைவின் வண்ணங்களை மாற்றலாம், «சஃபிள்» ஐகான்-பொத்தானைக் சொடுக்கு செய்வதன் மூலம் நீங்கள் அனைத்தையும் கலக்கலாம் நிறங்கள். நீங்கள் வண்ண முன்னமைவை நீக்க விரும்பினால், «நீக்கு» ஐகான்-பொத்தானைக் சொடுக்கு செய்க. புலனுணர்வு விரிவாக்கம் உங்களுக்கு விரைவாக படிக்க உதவக்கூடும். முதலில், அதை வாசகர் அமைப்புகளில் இயக்கவும் (பார்க்க: வாசகரை எவ்வாறு தனிப்பயனாக்குவது?). இயக்கப்பட்டதும், வாசகரின் இருபுறமும் இரண்டு செங்குத்து கோடுகள் தோன்றும். உங்கள் வாசிப்பு வேகத்தை மேலும் மேம்படுத்த, மையத்தைச் சுற்றி குறைந்த இடத்தை உருவாக்க திணிப்பை சரிசெய்யவும். இதைப் பயன்படுத்த, உங்கள் புற பார்வை பக்கங்களில் உரையை எடுக்க அனுமதிக்கும் போது திரையின் மையத்தில் கவனம் செலுத்துங்கள். நீங்கள் படிக்கும்போது, ஒவ்வொரு வரியுக்கும் பிறகு பக்கங்களை நேரடியாகப் பார்க்காமல் மையத்தில் உங்கள் கவனத்தை பராமரிக்கவும். கருப்பொருள் விருப்பத்தேர்வுகள் - வழங்கப்பட்டது - உங்கள் சாதனத்தை ச்கேன் செய்து புத்தகங்களைக் கண்டுபிடிக்க வேண்டும் பயன்பாட்டின் அடிப்படை அமைப்பை நீங்கள் வெற்றிகரமாக முடித்துவிட்டீர்கள். உதவித் திரையைப் படிப்பதன் மூலம் பயன்பாட்டை எவ்வாறு பயன்படுத்துவது என்பதை அறிய விரும்புகிறீர்களா? நேற்று தொடர்ந்து படிக்கவும் diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index b0a10a11..ac56a635 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -17,7 +17,6 @@ Yardım - İzin verilsin mi? Kitaplar eklensin mi? Kitaplar taşınsın mı? Kitaplar silinsin mi? @@ -27,10 +26,6 @@ Düzenle - - Cihazınızı kitaplar için tarayabilmemiz adına depolama iznine ihtiyacımız var. - Bu izin olmadan kitap ekleyemezsiniz. - 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. - - Cihazınızı kitaplar için tarayabilmemiz adına depolama iznine ihtiyacımız var - Açıklama yok. @@ -99,9 +91,6 @@ İptal - Ver - İzin ver - Yardıma ihtiyacım var Kitap ekle Taşı Sil @@ -539,10 +528,6 @@ Dil tercihleri Tema tercihleri - İzinler - Verildi - Depolama izni - Cihazınızı tarayıp kitapları bulmak için gereklidir Her şey tamam! Uygulamanın temel ayarlarını başarıyla tamamladınız. @@ -579,7 +564,6 @@ Okuma geçmişi Kitap ekle Ok - Gerekli Başlangıç rehberini sıfırla Web\'de aç Renk ön ayarını sil diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 2c332755..41a44706 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -17,7 +17,6 @@ Довідка - Надати Дозвіл? Додати книги? Перемістити книги? Видалити книги? @@ -27,15 +26,14 @@ Редагувати - Нам потрібен дозвіл на сховище, щоб просканувати Ваш Девайс на наявність книг. Без цього дозволу ви не зможете додати книгу. Ви можете вибрати книги, які хочете додати. Ви зможете відредагувати Назву та Обкладинку книги пізніше. Процес завантаження може зайняти декілька хвилин. Виберіть категорію, в яку ви хочете перемістити всі обрані книги(%1$s). Всі обрані книги будуть переміщені в обрану категорію. Це видалить всі обрані книги(%1$s) з бази даних. Це не видалить обрані книги з вашого пристрою. Це видалить цю книгу з бази даних. Це не видалить цю книгу з вашого пристрою. Виберіть категорію, в яку ви хочете перемістити цю книгу. Ця книга буде переміщена в обрану категорію. Це видалить всю історію з бази даних. Після видалення всієї історії порядок книг буде скинутий. + - Нам потрібен дозвіл на сховище, щоб просканувати Ваш Девайс на наявність книг Немає опису. Який жах! Щось пішло не так. Не знайдено жодного перекладача. @@ -43,12 +41,12 @@ Не знайдено жодного браузера. Не знайдено застосунка щоб поділитися. - Файл порожній, пошкоджений або відсутній. - Перевірте файл або змініть шлях і спробуйте ще раз. + Файл порожній, відсутній або недоступний. + Перевірте файл, налаштування сканування або змініть шлях і спробуйте ще раз. Не вдалося скинути обкладинку. Закритий код - Файла не існує + Файла не існує або він недоступний Пошук… @@ -63,9 +61,6 @@ Відмінити - Надати - Надати дозвіл - Мені потрібна допомога Додати книгу Перемістити Видалити @@ -82,6 +77,8 @@ Поділитися ОК Змінити шлях + Додати теку + Налаштувати сканування Невідомий @@ -128,10 +125,18 @@ Прогрес Налаштування теми Кольори + Сканування Головні Фільтр Сотрування + + + Ці теки використовуються для сканування та отримання файлів у Огляді. + Видалення теки тут не видалить фактичні файли чи книги з Бібліотеки. + Щоб відкликати доступ, натисніть відповідну кнопку. + + Мова застосунка @@ -407,10 +412,7 @@ У світ, де слова чарують, вчать та ведуть у неймовірні пригоди! Давайте дещо налаштуємо! Налаштування мови Налаштування теми - Дозволи - Надано - Дозвіл на сховище - Потрібний, щоб просканувати ваш пристрій та знайти книги + Налаштування сканування Все готово! Ви успішно закінчили базове налаштовування застосунку. Бажаєте дізнатися, як користуватися застосунком, прочитавши екран Довідки? @@ -444,7 +446,6 @@ Історія читання Додавайте книги Стрілка - Необхідно Скинути початковий посібник Відкрити в браузері Видалити пресет кольорів @@ -464,5 +465,7 @@ Редагувати поле Розгорнути Згорнути + Прибрати + Примітка \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2f912808..4f2aab88 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -21,14 +21,11 @@ 永不 预设 %1$s 取消 - 我们需要存储权限以扫描您的设备上的书 书库 历史 浏览 关于 app - 授予权限? 移动书籍? - 我们需要存储权限以扫描您的设备上的书。没有此权限,您将无法添加书。 所有已选择的书(%1$s)将会从数据库中删除,但不会从您的设备上删除。 这本书将会从数据库中删除,但不会从您的设备上删除。 选择你想将这本书移动到的分类。这本书将会被移动到指定的分类。 @@ -44,8 +41,6 @@ 阅读您喜欢的书籍并对其进行分类 让您读完的最后一本书为您打开阅读之旅的新篇章 已选择 «%1$s» - 授予 - 授予权限 撤销 下一个 未知 @@ -55,7 +50,6 @@ 已阅读 添加书籍? 未发现浏览器应用。 - 我需要帮助 添加一本书 移动 删除 @@ -260,11 +254,8 @@ 昨天 继续阅读 封面图片 - 权限 设置完成! 错误 - 已授权 - 存储权限 你已经成功完成了应用基本设置。要参阅帮助以获取更多使用技巧吗? 未找到封面图片 语言偏好 @@ -293,7 +284,6 @@ 阅读历史 添加书籍 箭头 - 必填 重置引导页面 删除所选书籍 撤销 @@ -301,7 +291,6 @@ 复选框 添加文件 选择所有文件 - 需要扫描您的设备来查找书籍 删除全部历史记录 在网页中打开 创建新颜色预设 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cfd97bca..65d1f61b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,7 +19,6 @@ Help - Grant Permission? Add books? Move books? Delete books? @@ -29,10 +28,6 @@ Edit - - We need storage permission to scan Your Device for books. - Without this permission, you will not be able to add a book. - You can choose which books you want to add. You can edit Title and Cover image of the book later. @@ -60,26 +55,19 @@ - - We need storage permission to scan Your Device for books - - - No description. - - - Something went wrong. - + No description. + Something went wrong. No translator app found. No dictionary app found. No browser app found. No app to share found. - 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. Could not reset cover image. Closed source - File does not exist + File does not exist or inaccessible @@ -102,9 +90,6 @@ Cancel - Grant - Grant permission - I need help Add a book Move Delete @@ -121,6 +106,8 @@ Share OK Change path + Add folder + Set up scanning Unknown @@ -167,10 +154,18 @@ Progress Theme preferences Colors + Scan General Filter Sort + + + 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. + + App language @@ -545,10 +540,7 @@ Language preferences Theme preferences - Permissions - Granted - Storage permission - Required to scan your device and find books + Scan preferences Everything\'s done! You\'re successfully done basic setting of the app. @@ -585,7 +577,6 @@ Reading history Add books Arrow - Required Reset start guide Open in web Delete color preset @@ -608,5 +599,7 @@ Edit field Expand Collapse + Remove + Note \ No newline at end of file