Remove uses of File#listFiles #449

Closed
Isira-Seneviratne wants to merge 2 commits from Remove_listFiles into devel
18 changed files with 165 additions and 212 deletions

View File

@@ -33,7 +33,7 @@ android {
applicationIdSuffix = '.debug'
}
release {
multiDexEnabled false
multiDexEnabled true
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
@@ -48,11 +48,14 @@ android {
main.java.srcDirs += 'src/main/kotlin/'
}
compileOptions {
// Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
jvmTarget = JavaVersion.VERSION_17
freeCompilerArgs += [
'-opt-in=kotlin.ExperimentalStdlibApi',
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
@@ -81,6 +84,8 @@ afterEvaluate {
}
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.3'
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:08eb9552d6') {
exclude group: 'org.json', module: 'json'

View File

@@ -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)
}
}

View File

@@ -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<File> = sequence {
listFilesRecursiveImpl(this@listFilesRecursive, filter)
}
private suspend fun SequenceScope<File>.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)
}
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 isUriSupported(uri: Uri): Boolean {
val scheme = uri.scheme?.lowercase(Locale.ROOT)
return scheme != null && scheme == "cbz" || scheme == "zip"
}
}
fun isCbzExtension(string: String): Boolean {
return string.equals("cbz", ignoreCase = true) || string.equals("zip", ignoreCase = true)
}
fun hasCbzExtension(name: String): Boolean {
return isCbzExtension(name.substringAfterLast('.', ""))
}
fun hasCbzExtension(uri: Uri) = uri.scheme?.let { isCbzExtension(it) } ?: false

View File

@@ -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 {
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)
}
override fun accept(dir: File, name: String): Boolean {
val ext = name.substringAfterLast('.', "").lowercase(Locale.ROOT)
return isExtensionValid(ext)
}
fun hasImageExtension(name: String): Boolean {
return isImageExtension(name.substringAfterLast('.', ""))
}
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(entry: ZipEntry): Boolean {
return hasImageExtension(entry.name)
}

View File

@@ -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 }
.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 -> {
}
}
}
else -> Unit
}
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<LocalManga> {
val files = getAllFiles()
private suspend fun getRawList(): List<LocalManga> {
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<LocalManga>.unwrap(): List<Manga> = map { it.manga }

View File

@@ -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<File> = runInterruptible(Dispatchers.IO) {
suspend fun getReadableDirs(): List<Path> = runInterruptible(Dispatchers.IO) {
getConfiguredStorageDirs()
.map { it.toPath() }
.filter { it.isReadable() }
}
suspend fun getWriteableDirs(): List<File> = 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<File> = runInterruptible(Dispatchers.IO) {
@@ -146,7 +148,7 @@ class LocalStorageManager @Inject constructor(
}
@WorkerThread
private fun getCacheDirs(): MutableSet<File> {
private fun getCacheDirs(): Set<File> {
val result = LinkedHashSet<File>()
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)
}

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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<MangaPage> = 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<File> = root.listFilesRecursive(CbzFilter())
.toListSorted(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name })
@OptIn(ExperimentalPathApi::class)
private fun getChaptersFiles(): List<Path> = 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 {

View File

@@ -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
}

View File

@@ -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<out ZipEntry>): 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('.'))

View File

@@ -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 {

View File

@@ -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(