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..f9b74f99f 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,7 @@ 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.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaPage import java.util.Date @@ -38,7 +38,6 @@ data class Bookmark( ) private fun isImageUrlDirect(): Boolean { - val extension = imageUrl.substringAfterLast('.') - return extension.isNotEmpty() && ImageFileFilter().isExtensionValid(extension) + return hasImageExtension(imageUrl) } } 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 92741f1d5..444cdc101 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,7 +7,6 @@ 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 @@ -19,6 +18,8 @@ import java.io.FileFilter import java.nio.file.attribute.BasicFileAttributes import java.util.zip.ZipEntry import java.util.zip.ZipFile +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.walk import kotlin.io.path.readAttributes fun File.subdir(name: String) = File(this, name).also { @@ -71,31 +72,7 @@ fun ContentResolver.resolveName(uri: Uri): String? { } suspend fun File.computeSize(): Long = runInterruptible(Dispatchers.IO) { - computeSizeInternal(this) -} - -@WorkerThread -private fun computeSizeInternal(file: File): Long { - return if (file.isDirectory) { - file.children().sumOf { computeSizeInternal(it) } - } else { - 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.children() - for (f in ss) { - if (f.isDirectory) { - listFilesRecursiveImpl(f, filter) - } else if (filter == null || filter.accept(f)) { - yield(f) - } - } + walkCompat().sumOf { it.length() } } fun File.children() = FileSequence(this) @@ -108,3 +85,12 @@ val File.creationTime } else { lastModified() } + +@OptIn(ExperimentalPathApi::class) +fun File.walkCompat() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Use lazy loading on Android 8.0 and later + toPath().walk().map { it.toFile() } +} else { + // Directories are excluded by default in Path.walk(), so do it here as well + walk().filter { it.isFile } +} 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..dd044a9f9 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 @@ -2,30 +2,16 @@ 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) - } +private fun isCbzExtension(ext: String?): Boolean { + return ext.equals("cbz", ignoreCase = true) || ext.equals("zip", ignoreCase = true) +} - companion object { +fun hasCbzExtension(string: String): Boolean { + val ext = string.substringAfterLast('.', "") + return isCbzExtension(ext) +} - fun isFileSupported(name: String): Boolean { - val ext = name.substringAfterLast('.', "").lowercase(Locale.ROOT) - return ext == "cbz" || ext == "zip" - } +fun hasCbzExtension(file: File) = isCbzExtension(file.name) - fun isUriSupported(uri: Uri): Boolean { - val scheme = uri.scheme?.lowercase(Locale.ROOT) - return scheme != null && scheme == "cbz" || scheme == "zip" - } - } -} +fun isCbzUri(uri: Uri) = isCbzExtension(uri.scheme) 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..06a48f4eb 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,11 @@ 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 accept(entry: ZipEntry): Boolean { - val ext = entry.name.substringAfterLast('.', "").lowercase(Locale.ROOT) - return isExtensionValid(ext) - } - - fun isExtensionValid(ext: String): Boolean { - return ext == "png" || ext == "jpg" || ext == "jpeg" || ext == "webp" - } +fun hasImageExtension(string: String): Boolean { + val ext = string.substringAfterLast('.', "") + return ext.equals("png", ignoreCase = true) || ext.equals("jpg", ignoreCase = true) + || ext.equals("jpeg", ignoreCase = true) || ext.equals("webp", ignoreCase = true) } + +fun hasImageExtension(file: File) = hasImageExtension(file.name) 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 d626a66d6..43739b74b 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 @@ -6,12 +6,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import org.koitharu.kotatsu.core.util.AlphanumComparator import org.koitharu.kotatsu.core.util.ext.creationTime -import org.koitharu.kotatsu.core.util.ext.listFilesRecursive 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.core.util.ext.walkCompat import org.koitharu.kotatsu.local.data.MangaIndex +import org.koitharu.kotatsu.local.data.hasCbzExtension +import org.koitharu.kotatsu.local.data.hasImageExtension import org.koitharu.kotatsu.local.data.output.LocalMangaOutput import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.model.Manga @@ -91,16 +91,12 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { override suspend fun getPages(chapter: MangaChapter): List = runInterruptible(Dispatchers.IO) { val file = chapter.url.toUri().toFile() if (file.isDirectory) { - file.listFilesRecursive(ImageFileFilter()) + file.walkCompat() + .filter { hasImageExtension(it) } .toListSorted(compareBy(AlphanumComparator()) { x -> x.name }) .map { val pageUri = it.toUri().toString() - MangaPage( - id = pageUri.longHashCode(), - url = pageUri, - preview = null, - source = MangaSource.LOCAL, - ) + MangaPage(pageUri.longHashCode(), pageUri, null, MangaSource.LOCAL) } } else { ZipFile(file).use { zip -> @@ -124,20 +120,20 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { private fun String.toHumanReadable() = replace("_", " ").toCamelCase() - private fun getChaptersFiles(): List = root.listFilesRecursive(CbzFilter()) - .toListSorted(compareBy(AlphanumComparator()) { x -> x.name }) + private fun getChaptersFiles(): List = root.walkCompat() + .filter { hasCbzExtension(it) } + .toListSorted(compareBy(AlphanumComparator()) { it.name }) 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) } - } + return root.walkCompat().firstOrNull { hasImageExtension(it) }?.toUri()?.toString() + ?: run { + val cbz = root.walkCompat().firstOrNull { hasCbzExtension(it) } ?: return null + ZipFile(cbz).use { zip -> + zip.entries().asSequence() + .firstOrNull { !it.isDirectory && hasImageExtension(it.name) } + ?.let { zipUri(cbz, it.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 96b1e0691..851ae7b04 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 @@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch -import org.koitharu.kotatsu.local.data.CbzFilter +import org.koitharu.kotatsu.local.data.hasCbzExtension import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter @@ -39,7 +39,7 @@ sealed class LocalMangaInput( fun ofOrNull(file: File): LocalMangaInput? = when { file.isDirectory -> LocalMangaDirInput(file) - CbzFilter.isFileSupported(file.name) -> LocalMangaZipInput(file) + hasCbzExtension(file.name) -> LocalMangaZipInput(file) else -> null } 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 0381a40df..1f4fa7761 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 @@ -42,8 +42,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.isCbzUri import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.reader.ui.pager.ReaderPage @@ -199,7 +199,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 (isCbzUri(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 812df6484..fc16164e6 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.isCbzUri 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 (isCbzUri(uri)) { runInterruptible(Dispatchers.IO) { val zip = ZipFile(uri.schemeSpecificPart) val entry = zip.getEntry(uri.fragment)