From 9f7ec888a69dc242df15ed456aafa45b32ed3a45 Mon Sep 17 00:00:00 2001 From: Isira Seneviratne Date: Sat, 22 Jul 2023 08:30:05 +0530 Subject: [PATCH] Remove uses of File#listFiles() --- .../kotatsu/bookmarks/domain/Bookmark.kt | 13 +-- .../koitharu/kotatsu/core/util/ext/File.kt | 35 -------- .../koitharu/kotatsu/core/util/ext/Path.kt | 18 ++++ .../koitharu/kotatsu/core/zip/ZipOutput.kt | 18 ++-- .../kotatsu/details/ui/DetailsViewModel.kt | 10 +-- .../koitharu/kotatsu/local/data/CbzFilter.kt | 32 ++----- .../kotatsu/local/data/ImageFileFilter.kt | 31 ++----- .../local/data/LocalMangaRepository.kt | 87 +++++++++---------- .../kotatsu/local/data/LocalStorageManager.kt | 26 +++--- .../koitharu/kotatsu/local/data/MangaIndex.kt | 4 +- .../kotatsu/local/data/TempFileFilter.kt | 11 --- .../data/importer/SingleMangaImporter.kt | 4 +- .../local/data/input/LocalMangaDirInput.kt | 53 +++++++---- .../local/data/input/LocalMangaInput.kt | 11 ++- .../local/data/input/LocalMangaZipInput.kt | 7 +- .../kotatsu/reader/domain/PageLoader.kt | 4 +- .../reader/ui/thumbnails/MangaPageFetcher.kt | 4 +- 17 files changed, 158 insertions(+), 210 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Path.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/local/data/TempFileFilter.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt index 258a53ec8..cb98dde66 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt @@ -1,7 +1,8 @@ package org.koitharu.kotatsu.bookmarks.domain import org.koitharu.kotatsu.list.ui.model.ListModel -import org.koitharu.kotatsu.local.data.ImageFileFilter +import org.koitharu.kotatsu.local.data.hasImageExtension +import org.koitharu.kotatsu.local.data.isImageExtension import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaPage import java.util.Date @@ -17,11 +18,8 @@ data class Bookmark( val percent: Float, ) : ListModel { - val directImageUrl: String? - get() = if (isImageUrlDirect()) imageUrl else null - val imageLoadData: Any - get() = if (isImageUrlDirect()) imageUrl else toMangaPage() + get() = if (hasImageExtension(imageUrl)) imageUrl else toMangaPage() override fun areItemsTheSame(other: ListModel): Boolean { return other is Bookmark && @@ -36,9 +34,4 @@ data class Bookmark( preview = null, source = manga.source, ) - - private fun isImageUrlDirect(): Boolean { - val extension = imageUrl.substringAfterLast('.') - return extension.isNotEmpty() && ImageFileFilter().isExtensionValid(extension) - } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt index 79877887b..04776a3be 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt @@ -7,14 +7,11 @@ import android.os.Build import android.os.Environment import android.os.storage.StorageManager import android.provider.OpenableColumns -import androidx.annotation.WorkerThread import androidx.core.database.getStringOrNull import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withContext import org.koitharu.kotatsu.R import java.io.File -import java.io.FileFilter import java.util.zip.ZipEntry import java.util.zip.ZipFile @@ -46,8 +43,6 @@ fun File.getStorageName(context: Context): String = runCatching { } }.getOrNull() ?: context.getString(R.string.other_storage) -fun Uri.toFileOrNull() = if (scheme == "file") path?.let(::File) else null - suspend fun File.deleteAwait() = withContext(Dispatchers.IO) { delete() || deleteRecursively() } @@ -66,33 +61,3 @@ fun ContentResolver.resolveName(uri: Uri): String? { } return fallback } - -suspend fun File.computeSize(): Long = runInterruptible(Dispatchers.IO) { - computeSizeInternal(this) -} - -@WorkerThread -private fun computeSizeInternal(file: File): Long { - if (file.isDirectory) { - val files = file.listFiles() ?: return 0L - return files.sumOf { computeSizeInternal(it) } - } else { - return file.length() - } -} - -fun File.listFilesRecursive(filter: FileFilter? = null): Sequence = sequence { - listFilesRecursiveImpl(this@listFilesRecursive, filter) -} - -private suspend fun SequenceScope.listFilesRecursiveImpl(root: File, filter: FileFilter?) { - val ss = root.list() ?: return - for (s in ss) { - val f = File(root, s) - if (f.isDirectory) { - listFilesRecursiveImpl(f, filter) - } else if (filter == null || filter.accept(f)) { - yield(f) - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Path.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Path.kt new file mode 100644 index 000000000..bc05ed293 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Path.kt @@ -0,0 +1,18 @@ +package org.koitharu.kotatsu.core.util.ext + +import android.net.Uri +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runInterruptible +import java.nio.file.Path +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.Path +import kotlin.io.path.fileSize +import kotlin.io.path.walk + +@OptIn(ExperimentalPathApi::class) +suspend fun Path.computeSize(): Long = runInterruptible(Dispatchers.IO) { + // Directories are not included by default + walk().sumOf { it.fileSize() } +} + +fun Uri.toPathOrNull() = if (scheme == "file") path?.let { Path(it) } else null diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt index fd38f1c04..3bbe12875 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt @@ -4,11 +4,15 @@ import androidx.annotation.WorkerThread import androidx.collection.ArraySet import okio.Closeable import java.io.File -import java.io.FileInputStream +import java.nio.file.Path import java.util.zip.Deflater import java.util.zip.ZipEntry import java.util.zip.ZipFile import java.util.zip.ZipOutputStream +import kotlin.io.path.forEachDirectoryEntry +import kotlin.io.path.inputStream +import kotlin.io.path.isDirectory +import kotlin.io.path.name class ZipOutput( val file: File, @@ -23,7 +27,7 @@ class ZipOutput( @WorkerThread fun put(name: String, file: File): Boolean { - return output.appendFile(file, name) + return output.appendFile(file.toPath(), name) } @WorkerThread @@ -78,8 +82,8 @@ class ZipOutput( } @WorkerThread - private fun ZipOutputStream.appendFile(fileToZip: File, name: String): Boolean { - if (fileToZip.isDirectory) { + private fun ZipOutputStream.appendFile(fileToZip: Path, name: String): Boolean { + if (fileToZip.isDirectory()) { val entry = if (name.endsWith("/")) { ZipEntry(name) } else { @@ -90,11 +94,11 @@ class ZipOutput( } putNextEntry(entry) closeEntry() - fileToZip.listFiles()?.forEach { childFile -> - appendFile(childFile, "$name/${childFile.name}") + fileToZip.forEachDirectoryEntry { + appendFile(it, "$name/${it.name}") } } else { - FileInputStream(fileToZip).use { fis -> + fileToZip.inputStream().use { fis -> if (!entryNames.add(name)) { return false } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index c0e82cb6f..d30244cb6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -44,7 +44,7 @@ import org.koitharu.kotatsu.core.util.ext.combine import org.koitharu.kotatsu.core.util.ext.computeSize import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.core.util.ext.sanitize -import org.koitharu.kotatsu.core.util.ext.toFileOrNull +import org.koitharu.kotatsu.core.util.ext.toPathOrNull import org.koitharu.kotatsu.details.domain.BranchComparator import org.koitharu.kotatsu.details.domain.DetailsInteractor import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase @@ -135,13 +135,7 @@ class DetailsViewModel @Inject constructor( val localSize = doubleManga .map { - val local = it?.local - if (local != null) { - val file = local.url.toUri().toFileOrNull() - file?.computeSize() ?: 0L - } else { - 0L - } + it?.local?.url?.toUri()?.toPathOrNull()?.computeSize() ?: 0L }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(), 0) val description = manga diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFilter.kt index 24e9f8a2d..6f1779fdd 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/CbzFilter.kt @@ -1,31 +1,13 @@ package org.koitharu.kotatsu.local.data import android.net.Uri -import java.io.File -import java.io.FileFilter -import java.io.FilenameFilter -import java.util.Locale -class CbzFilter : FileFilter, FilenameFilter { - - override fun accept(dir: File, name: String): Boolean { - return isFileSupported(name) - } - - override fun accept(pathname: File?): Boolean { - return isFileSupported(pathname?.name ?: return false) - } - - companion object { - - fun isFileSupported(name: String): Boolean { - val ext = name.substringAfterLast('.', "").lowercase(Locale.ROOT) - return ext == "cbz" || ext == "zip" - } +fun isCbzExtension(string: String): Boolean { + return string.equals("cbz", ignoreCase = true) || string.equals("zip", ignoreCase = true) +} - fun isUriSupported(uri: Uri): Boolean { - val scheme = uri.scheme?.lowercase(Locale.ROOT) - return scheme != null && scheme == "cbz" || scheme == "zip" - } - } +fun hasCbzExtension(name: String): Boolean { + return isCbzExtension(name.substringAfterLast('.', "")) } + +fun hasCbzExtension(uri: Uri) = uri.scheme?.let { isCbzExtension(it) } ?: false diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/ImageFileFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/ImageFileFilter.kt index 4dc76c9f6..044508d60 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/ImageFileFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/ImageFileFilter.kt @@ -1,29 +1,16 @@ package org.koitharu.kotatsu.local.data -import java.io.File -import java.io.FileFilter -import java.io.FilenameFilter -import java.util.Locale import java.util.zip.ZipEntry -class ImageFileFilter : FilenameFilter, FileFilter { - - override fun accept(dir: File, name: String): Boolean { - val ext = name.substringAfterLast('.', "").lowercase(Locale.ROOT) - return isExtensionValid(ext) - } - - override fun accept(pathname: File?): Boolean { - val ext = pathname?.extension?.lowercase(Locale.ROOT) ?: return false - return isExtensionValid(ext) - } +fun isImageExtension(string: String): Boolean { + return string.equals("png", ignoreCase = true) || string.equals("jpg", ignoreCase = true) + || string.equals("jpeg", ignoreCase = true) || string.equals("webp", ignoreCase = true) +} - fun accept(entry: ZipEntry): Boolean { - val ext = entry.name.substringAfterLast('.', "").lowercase(Locale.ROOT) - return isExtensionValid(ext) - } +fun hasImageExtension(name: String): Boolean { + return isImageExtension(name.substringAfterLast('.', "")) +} - fun isExtensionValid(ext: String): Boolean { - return ext == "png" || ext == "jpg" || ext == "jpeg" || ext == "webp" - } +fun hasImageExtension(entry: ZipEntry): Boolean { + return hasImageExtension(entry.name) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt index 7950f4575..2033812f3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.runInterruptible import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.AlphanumComparator import org.koitharu.kotatsu.core.util.CompositeMutex import org.koitharu.kotatsu.core.util.ext.deleteAwait import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug @@ -32,6 +33,9 @@ import java.io.File import java.util.EnumSet import javax.inject.Inject import javax.inject.Singleton +import kotlin.io.path.deleteExisting +import kotlin.io.path.forEachDirectoryEntry +import kotlin.io.path.useDirectoryEntries private const val MAX_PARALLELISM = 4 @@ -57,10 +61,7 @@ class LocalMangaRepository @Inject constructor( if (offset > 0) { return emptyList() } - val list = getRawList() - if (query.isNotEmpty()) { - list.retainAll { x -> x.isMatchesQuery(query) } - } + val list = getRawList().filter { query.isEmpty() || it.isMatchesQuery(query) } return list.unwrap() } @@ -69,18 +70,17 @@ class LocalMangaRepository @Inject constructor( return emptyList() } val list = getRawList() - if (!tags.isNullOrEmpty()) { - list.retainAll { x -> x.containsTags(tags) } - } - when (sortOrder) { - SortOrder.ALPHABETICAL -> list.sortWith(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.manga.title }) - SortOrder.RATING -> list.sortByDescending { it.manga.rating } - SortOrder.NEWEST, - SortOrder.UPDATED, - -> list.sortByDescending { it.createdAt } - - else -> Unit - } + .filter { tags.isNullOrEmpty() || it.containsTags(tags) } + .apply { + when (sortOrder) { + SortOrder.ALPHABETICAL -> sortedWith(compareBy(AlphanumComparator()) { x -> x.manga.title }) + SortOrder.RATING -> sortedByDescending { it.manga.rating } + SortOrder.NEWEST, SortOrder.UPDATED -> sortedByDescending { it.createdAt } + else -> { + } + } + } + return list.unwrap() } @@ -127,21 +127,19 @@ class LocalMangaRepository @Inject constructor( } suspend fun findSavedManga(remoteManga: Manga): LocalManga? { - val files = getAllFiles() - if (files.isEmpty()) { - return null - } return channelFlow { - for (file in files) { - launch { - val mangaInput = LocalMangaInput.of(file) - runCatchingCancellable { - val mangaInfo = mangaInput.getMangaInfo() - if (mangaInfo != null && mangaInfo.id == remoteManga.id) { - send(mangaInput) + storageManager.getReadableDirs().forEach { dir -> + dir.forEachDirectoryEntry { entry -> + launch { + val mangaInput = LocalMangaInput.of(entry.toFile()) + runCatchingCancellable { + val mangaInfo = mangaInput.getMangaInfo() + if (mangaInfo != null && mangaInfo.id == remoteManga.id) { + send(mangaInput) + } + }.onFailure { + it.printStackTraceDebug() } - }.onFailure { - it.printStackTraceDebug() } } } @@ -171,10 +169,10 @@ class LocalMangaRepository @Inject constructor( } val dirs = storageManager.getWriteableDirs() runInterruptible(Dispatchers.IO) { - dirs.flatMap { dir -> - dir.listFiles(TempFileFilter())?.toList().orEmpty() - }.forEach { file -> - file.deleteRecursively() + dirs.forEach { dir -> + dir.toPath().forEachDirectoryEntry("*.tmp") { + it.deleteExisting() + } } } return true @@ -188,20 +186,21 @@ class LocalMangaRepository @Inject constructor( locks.unlock(id) } - private suspend fun getRawList(): ArrayList { - val files = getAllFiles() + private suspend fun getRawList(): List { return coroutineScope { val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM) - files.map { file -> - async(dispatcher) { - runCatchingCancellable { LocalMangaInput.ofOrNull(file)?.getManga() }.getOrNull() + storageManager.getReadableDirs().flatMap { dir -> + dir.useDirectoryEntries { entries -> + entries.map { + async(dispatcher) { + runCatchingCancellable { LocalMangaInput.ofOrNull(it)?.getManga() }.getOrNull() + } + }.toList() // Using toList() allows the directory stream to be closed. } - }.awaitAll() - }.filterNotNullTo(ArrayList(files.size)) - } - - private suspend fun getAllFiles() = storageManager.getReadableDirs().flatMap { dir -> - dir.listFiles()?.toList().orEmpty() + } + .awaitAll() + .filterNotNull() + } } private fun Collection.unwrap(): List = map { it.manga } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalStorageManager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalStorageManager.kt index 37170b478..293972b78 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalStorageManager.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalStorageManager.kt @@ -17,10 +17,11 @@ import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.ext.computeSize import org.koitharu.kotatsu.core.util.ext.getStorageName import org.koitharu.kotatsu.core.util.ext.resolveFile -import org.koitharu.kotatsu.core.util.ext.toFileOrNull import org.koitharu.kotatsu.parsers.util.mapToSet import java.io.File +import java.nio.file.Path import javax.inject.Inject +import kotlin.io.path.isReadable private const val DIR_NAME = "manga" private const val NOMEDIA = ".nomedia" @@ -46,15 +47,15 @@ class LocalStorageManager @Inject constructor( } suspend fun computeCacheSize(cache: CacheDir) = withContext(Dispatchers.IO) { - getCacheDirs(cache.dir).sumOf { it.computeSize() } + getCacheDirs(cache.dir).sumOf { it.toPath().computeSize() } } suspend fun computeCacheSize() = withContext(Dispatchers.IO) { - getCacheDirs().sumOf { it.computeSize() } + getCacheDirs().sumOf { it.toPath().computeSize() } } suspend fun computeStorageSize() = withContext(Dispatchers.IO) { - getAvailableStorageDirs().sumOf { it.computeSize() } + getAvailableStorageDirs().sumOf { it.toPath().computeSize() } } suspend fun computeAvailableSize() = runInterruptible(Dispatchers.IO) { @@ -65,19 +66,20 @@ class LocalStorageManager @Inject constructor( getCacheDirs(cache.dir).forEach { it.deleteRecursively() } } - suspend fun getReadableDirs(): List = runInterruptible(Dispatchers.IO) { + suspend fun getReadableDirs(): List = runInterruptible(Dispatchers.IO) { getConfiguredStorageDirs() + .map { it.toPath() } .filter { it.isReadable() } } suspend fun getWriteableDirs(): List = runInterruptible(Dispatchers.IO) { getConfiguredStorageDirs() - .filter { it.isWriteable() } + .filter { it.isWritable() } } suspend fun getDefaultWriteableDir(): File? = runInterruptible(Dispatchers.IO) { - val preferredDir = settings.mangaStorageDir?.takeIf { it.isWriteable() } - preferredDir ?: getFallbackStorageDir()?.takeIf { it.isWriteable() } + val preferredDir = settings.mangaStorageDir?.takeIf { it.isWritable() } + preferredDir ?: getFallbackStorageDir()?.takeIf { it.isWritable() } } suspend fun getApplicationStorageDirs(): Set = runInterruptible(Dispatchers.IO) { @@ -146,7 +148,7 @@ class LocalStorageManager @Inject constructor( } @WorkerThread - private fun getCacheDirs(): MutableSet { + private fun getCacheDirs(): Set { val result = LinkedHashSet() result += context.cacheDir context.externalCacheDirs.filterNotNullTo(result) @@ -163,11 +165,7 @@ class LocalStorageManager @Inject constructor( } } - private fun File.isReadable() = runCatching { - canRead() - }.getOrDefault(false) - - private fun File.isWriteable() = runCatching { + private fun File.isWritable() = runCatching { canWrite() }.getOrDefault(false) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt index a6a1c4bdc..5ebf2b22a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/MangaIndex.kt @@ -4,6 +4,7 @@ import androidx.annotation.WorkerThread import org.json.JSONArray import org.json.JSONObject import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.core.util.AlphanumComparator import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource @@ -126,8 +127,7 @@ class MangaIndex(source: String?) { item.put("id", id) list.add(item) } - val comparator = org.koitharu.kotatsu.core.util.AlphanumComparator() - list.sortWith(compareBy(comparator) { it.getString("name") }) + list.sortWith(compareBy(AlphanumComparator()) { it.getString("name") }) val newJo = JSONObject() list.forEachIndexed { i, obj -> obj.put("number", i + 1) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/TempFileFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/TempFileFilter.kt deleted file mode 100644 index 696a433b2..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/TempFileFilter.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.koitharu.kotatsu.local.data - -import java.io.File -import java.io.FilenameFilter - -class TempFileFilter : FilenameFilter { - - override fun accept(dir: File, name: String): Boolean { - return name.endsWith(".tmp", ignoreCase = true) - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/importer/SingleMangaImporter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/importer/SingleMangaImporter.kt index 9221a4f08..b7442c380 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/importer/SingleMangaImporter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/importer/SingleMangaImporter.kt @@ -15,9 +15,9 @@ import okio.source import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.util.ext.resolveName import org.koitharu.kotatsu.core.util.ext.writeAllCancellable -import org.koitharu.kotatsu.local.data.CbzFilter import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.LocalStorageManager +import org.koitharu.kotatsu.local.data.hasCbzExtension import org.koitharu.kotatsu.local.data.input.LocalMangaInput import org.koitharu.kotatsu.local.domain.model.LocalManga import java.io.File @@ -46,7 +46,7 @@ class SingleMangaImporter @Inject constructor( private suspend fun importFile(uri: Uri): LocalManga = withContext(Dispatchers.IO) { val contentResolver = storageManager.contentResolver val name = contentResolver.resolveName(uri) ?: throw IOException("Cannot fetch name from uri: $uri") - if (!CbzFilter.isFileSupported(name)) { + if (!hasCbzExtension(name)) { throw UnsupportedFileException("Unsupported file on $uri") } val dest = File(getOutputDir(), name) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt index c2cac2c60..153adbdf5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt @@ -4,12 +4,13 @@ import androidx.core.net.toFile import androidx.core.net.toUri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible -import org.koitharu.kotatsu.core.util.ext.listFilesRecursive +import org.koitharu.kotatsu.core.util.AlphanumComparator import org.koitharu.kotatsu.core.util.ext.longHashCode import org.koitharu.kotatsu.core.util.ext.toListSorted -import org.koitharu.kotatsu.local.data.CbzFilter -import org.koitharu.kotatsu.local.data.ImageFileFilter import org.koitharu.kotatsu.local.data.MangaIndex +import org.koitharu.kotatsu.local.data.hasImageExtension +import org.koitharu.kotatsu.local.data.isCbzExtension +import org.koitharu.kotatsu.local.data.isImageExtension import org.koitharu.kotatsu.local.data.output.LocalMangaOutput import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.model.Manga @@ -18,7 +19,14 @@ import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.toCamelCase import java.io.File +import java.nio.file.Path import java.util.zip.ZipFile +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.extension +import kotlin.io.path.getLastModifiedTime +import kotlin.io.path.name +import kotlin.io.path.nameWithoutExtension +import kotlin.io.path.walk /** * Manga {Folder} @@ -61,7 +69,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { name = f.nameWithoutExtension.toHumanReadable(), number = i + 1, source = MangaSource.LOCAL, - uploadDate = f.lastModified(), + uploadDate = f.getLastModifiedTime().toMillis(), url = f.toUri().toString(), scanlator = null, branch = null, @@ -84,11 +92,13 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { index?.getMangaInfo() } + @OptIn(ExperimentalPathApi::class) override suspend fun getPages(chapter: MangaChapter): List = runInterruptible(Dispatchers.IO) { val file = chapter.url.toUri().toFile() if (file.isDirectory) { - file.listFilesRecursive(ImageFileFilter()) - .toListSorted(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name }) + file.toPath().walk() + .filter { isImageExtension(it.extension) } + .toListSorted(compareBy(AlphanumComparator()) { x -> x.name }) .map { val pageUri = it.toUri().toString() MangaPage( @@ -104,7 +114,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { .asSequence() .filter { x -> !x.isDirectory } .map { it.name } - .toListSorted(org.koitharu.kotatsu.core.util.AlphanumComparator()) + .toListSorted(AlphanumComparator()) .map { val pageUri = zipUri(file, it) MangaPage( @@ -120,20 +130,25 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { private fun String.toHumanReadable() = replace("_", " ").toCamelCase() - private fun getChaptersFiles(): List = root.listFilesRecursive(CbzFilter()) - .toListSorted(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name }) + @OptIn(ExperimentalPathApi::class) + private fun getChaptersFiles(): List = root.toPath().walk() + .filter { isCbzExtension(it.extension) } + .toListSorted(compareBy(AlphanumComparator()) { x -> x.name }) + @OptIn(ExperimentalPathApi::class) private fun findFirstImageEntry(): String? { - val filter = ImageFileFilter() - root.listFilesRecursive(filter).firstOrNull()?.let { - return it.toUri().toString() - } - val cbz = root.listFilesRecursive(CbzFilter()).firstOrNull() ?: return null - return ZipFile(cbz).use { zip -> - zip.entries().asSequence() - .firstOrNull { x -> !x.isDirectory && filter.accept(x) } - ?.let { entry -> zipUri(cbz, entry.name) } - } + val rootPath = root.toPath() + return rootPath.walk() + .filter { isImageExtension(it.extension) } + .firstOrNull()?.toUri()?.toString() + ?: run { + val cbz = rootPath.walk().filter { isCbzExtension(it.extension) }.firstOrNull()?.toFile() ?: return null + return ZipFile(cbz).use { zip -> + zip.entries().asSequence() + .firstOrNull { x -> !x.isDirectory && hasImageExtension(x) } + ?.let { entry -> zipUri(cbz, entry.name) } + } + } } private fun fileUri(base: File, name: String): String { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaInput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaInput.kt index c88b0af05..bc4198e61 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaInput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaInput.kt @@ -2,13 +2,16 @@ package org.koitharu.kotatsu.local.data.input import android.net.Uri import androidx.core.net.toFile -import org.koitharu.kotatsu.local.data.CbzFilter +import org.koitharu.kotatsu.local.data.isCbzExtension import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource import java.io.File +import java.nio.file.Path +import kotlin.io.path.extension +import kotlin.io.path.isDirectory sealed class LocalMangaInput( protected val root: File, @@ -31,9 +34,9 @@ sealed class LocalMangaInput( else -> LocalMangaZipInput(file) } - fun ofOrNull(file: File): LocalMangaInput? = when { - file.isDirectory -> LocalMangaDirInput(file) - CbzFilter.isFileSupported(file.name) -> LocalMangaZipInput(file) + fun ofOrNull(path: Path): LocalMangaInput? = when { + path.isDirectory() -> LocalMangaDirInput(path.toFile()) + isCbzExtension(path.extension) -> LocalMangaZipInput(path.toFile()) else -> null } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaZipInput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaZipInput.kt index f468c4647..bf706eff9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaZipInput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaZipInput.kt @@ -7,6 +7,7 @@ import androidx.core.net.toFile import androidx.core.net.toUri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible +import org.koitharu.kotatsu.core.util.AlphanumComparator import org.koitharu.kotatsu.core.util.ext.longHashCode import org.koitharu.kotatsu.core.util.ext.readText import org.koitharu.kotatsu.core.util.ext.toListSorted @@ -70,7 +71,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) { publicUrl = fileUri, source = MangaSource.LOCAL, coverUrl = zipUri(root, findFirstImageEntry(zip.entries())?.name.orEmpty()), - chapters = chapters.sortedWith(org.koitharu.kotatsu.core.util.AlphanumComparator()) + chapters = chapters.sortedWith(AlphanumComparator()) .mapIndexed { i, s -> MangaChapter( id = "$i$s".longHashCode(), @@ -125,7 +126,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) { } } entries - .toListSorted(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name }) + .toListSorted(compareBy(AlphanumComparator()) { x -> x.name }) .map { x -> val entryUri = zipUri(file, x.name) MangaPage( @@ -141,7 +142,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) { private fun findFirstImageEntry(entries: Enumeration): ZipEntry? { val list = entries.toList() .filterNot { it.isDirectory } - .sortedWith(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name }) + .sortedWith(compareBy(AlphanumComparator()) { x -> x.name }) val map = MimeTypeMap.getSingleton() return list.firstOrNull { map.getMimeTypeFromExtension(it.name.substringAfterLast('.')) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt index aa49d0786..2b9c023df 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt @@ -41,8 +41,8 @@ import org.koitharu.kotatsu.core.util.ext.ramAvailable import org.koitharu.kotatsu.core.util.ext.withProgress import org.koitharu.kotatsu.core.util.progress.ProgressDeferred import org.koitharu.kotatsu.core.zip.ZipPool -import org.koitharu.kotatsu.local.data.CbzFilter import org.koitharu.kotatsu.local.data.PagesCache +import org.koitharu.kotatsu.local.data.hasCbzExtension import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.reader.ui.pager.ReaderPage @@ -198,7 +198,7 @@ class PageLoader @Inject constructor( val pageUrl = getPageUrl(page) check(pageUrl.isNotBlank()) { "Cannot obtain full image url" } val uri = Uri.parse(pageUrl) - return if (CbzFilter.isUriSupported(uri)) { + return if (hasCbzExtension(uri)) { runInterruptible(Dispatchers.IO) { zipPool[uri] }.use { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/MangaPageFetcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/MangaPageFetcher.kt index 393f6398d..ddf1113fa 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/MangaPageFetcher.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/thumbnails/MangaPageFetcher.kt @@ -19,8 +19,8 @@ import okio.source import org.koitharu.kotatsu.core.network.ImageProxyInterceptor import org.koitharu.kotatsu.core.network.MangaHttpClient import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.local.data.CbzFilter import org.koitharu.kotatsu.local.data.PagesCache +import org.koitharu.kotatsu.local.data.hasCbzExtension import org.koitharu.kotatsu.local.data.util.withExtraCloseable import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.util.mimeType @@ -56,7 +56,7 @@ class MangaPageFetcher( private suspend fun loadPage(pageUrl: String): SourceResult { val uri = pageUrl.toUri() - return if (CbzFilter.isUriSupported(uri)) { + return if (hasCbzExtension(uri)) { val zip = runInterruptible(Dispatchers.IO) { ZipFile(uri.schemeSpecificPart) } val entry = runInterruptible(Dispatchers.IO) { zip.getEntry(uri.fragment) } return SourceResult(