Remove uses of File#listFiles #449
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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('.'))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user