mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	Use UniFile for local source file handling
This commit is contained in:
		@@ -25,7 +25,7 @@ import kotlinx.serialization.json.Json
 | 
			
		||||
import nl.adaptivity.xmlutil.XmlDeclMode
 | 
			
		||||
import nl.adaptivity.xmlutil.core.XmlVersion
 | 
			
		||||
import nl.adaptivity.xmlutil.serialization.XML
 | 
			
		||||
import tachiyomi.core.provider.AndroidStorageFolderProvider
 | 
			
		||||
import tachiyomi.core.storage.AndroidStorageFolderProvider
 | 
			
		||||
import tachiyomi.data.AndroidDatabaseHandler
 | 
			
		||||
import tachiyomi.data.Database
 | 
			
		||||
import tachiyomi.data.DatabaseHandler
 | 
			
		||||
@@ -125,7 +125,7 @@ class AppModule(val app: Application) : InjektModule {
 | 
			
		||||
        addSingletonFactory { ImageSaver(app) }
 | 
			
		||||
 | 
			
		||||
        addSingletonFactory { AndroidStorageFolderProvider(app) }
 | 
			
		||||
        addSingletonFactory { LocalSourceFileSystem(get<AndroidStorageFolderProvider>()) }
 | 
			
		||||
        addSingletonFactory { LocalSourceFileSystem(app, get<AndroidStorageFolderProvider>()) }
 | 
			
		||||
        addSingletonFactory { LocalCoverManager(app, get()) }
 | 
			
		||||
 | 
			
		||||
        // Asynchronously init expensive components for a faster cold start
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
 | 
			
		||||
import tachiyomi.core.preference.AndroidPreferenceStore
 | 
			
		||||
import tachiyomi.core.preference.PreferenceStore
 | 
			
		||||
import tachiyomi.core.provider.AndroidStorageFolderProvider
 | 
			
		||||
import tachiyomi.core.storage.AndroidStorageFolderProvider
 | 
			
		||||
import tachiyomi.domain.backup.service.BackupPreferences
 | 
			
		||||
import tachiyomi.domain.download.service.DownloadPreferences
 | 
			
		||||
import tachiyomi.domain.library.service.LibraryPreferences
 | 
			
		||||
 
 | 
			
		||||
@@ -1,25 +1,24 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.reader.loader
 | 
			
		||||
 | 
			
		||||
import com.hippo.unifile.UniFile
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Page
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
 | 
			
		||||
import tachiyomi.core.util.system.ImageUtil
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.FileInputStream
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Loader used to load a chapter from a directory given on [file].
 | 
			
		||||
 */
 | 
			
		||||
internal class DirectoryPageLoader(val file: File) : PageLoader() {
 | 
			
		||||
internal class DirectoryPageLoader(val file: UniFile) : PageLoader() {
 | 
			
		||||
 | 
			
		||||
    override var isLocal: Boolean = true
 | 
			
		||||
 | 
			
		||||
    override suspend fun getPages(): List<ReaderPage> {
 | 
			
		||||
        return file.listFiles()
 | 
			
		||||
            ?.filter { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
 | 
			
		||||
            ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
 | 
			
		||||
            ?.filter { !it.isDirectory && ImageUtil.isImage(it.name) { it.openInputStream() } }
 | 
			
		||||
            ?.sortedWith { f1, f2 -> f1.name.orEmpty().compareToCaseInsensitiveNaturalOrder(f2.name.orEmpty()) }
 | 
			
		||||
            ?.mapIndexed { i, file ->
 | 
			
		||||
                val streamFn = { FileInputStream(file) }
 | 
			
		||||
                val streamFn = { file.openInputStream() }
 | 
			
		||||
                ReaderPage(i).apply {
 | 
			
		||||
                    stream = streamFn
 | 
			
		||||
                    status = Page.State.READY
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
 | 
			
		||||
import tachiyomi.domain.manga.model.Manga
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.io.File
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Loader used to load a chapter from the downloaded chapters.
 | 
			
		||||
@@ -47,7 +46,7 @@ internal class DownloadPageLoader(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun getPagesFromArchive(chapterPath: UniFile): List<ReaderPage> {
 | 
			
		||||
        val loader = ZipPageLoader(File(chapterPath.filePath!!)).also { zipPageLoader = it }
 | 
			
		||||
        val loader = ZipPageLoader(chapterPath).also { zipPageLoader = it }
 | 
			
		||||
        return loader.getPages()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,14 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.reader.loader
 | 
			
		||||
 | 
			
		||||
import com.hippo.unifile.UniFile
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Page
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
 | 
			
		||||
import eu.kanade.tachiyomi.util.storage.EpubFile
 | 
			
		||||
import java.io.File
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Loader used to load a chapter from a .epub file.
 | 
			
		||||
 */
 | 
			
		||||
internal class EpubPageLoader(file: File) : PageLoader() {
 | 
			
		||||
internal class EpubPageLoader(file: UniFile) : PageLoader() {
 | 
			
		||||
 | 
			
		||||
    private val epub = EpubFile(file)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,12 @@ package eu.kanade.tachiyomi.ui.reader.loader
 | 
			
		||||
 | 
			
		||||
import com.github.junrar.Archive
 | 
			
		||||
import com.github.junrar.rarfile.FileHeader
 | 
			
		||||
import com.hippo.unifile.UniFile
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Page
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
 | 
			
		||||
import tachiyomi.core.storage.toFile
 | 
			
		||||
import tachiyomi.core.util.system.ImageUtil
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.InputStream
 | 
			
		||||
import java.io.PipedInputStream
 | 
			
		||||
import java.io.PipedOutputStream
 | 
			
		||||
@@ -14,9 +15,9 @@ import java.io.PipedOutputStream
 | 
			
		||||
/**
 | 
			
		||||
 * Loader used to load a chapter from a .rar or .cbr file.
 | 
			
		||||
 */
 | 
			
		||||
internal class RarPageLoader(file: File) : PageLoader() {
 | 
			
		||||
internal class RarPageLoader(file: UniFile) : PageLoader() {
 | 
			
		||||
 | 
			
		||||
    private val rar = Archive(file)
 | 
			
		||||
    private val rar = Archive(file.toFile())
 | 
			
		||||
 | 
			
		||||
    override var isLocal: Boolean = true
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,24 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.reader.loader
 | 
			
		||||
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import com.hippo.unifile.UniFile
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Page
 | 
			
		||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
 | 
			
		||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
 | 
			
		||||
import tachiyomi.core.storage.toFile
 | 
			
		||||
import tachiyomi.core.util.system.ImageUtil
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.nio.charset.StandardCharsets
 | 
			
		||||
import java.util.zip.ZipFile
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Loader used to load a chapter from a .zip or .cbz file.
 | 
			
		||||
 */
 | 
			
		||||
internal class ZipPageLoader(file: File) : PageLoader() {
 | 
			
		||||
internal class ZipPageLoader(file: UniFile) : PageLoader() {
 | 
			
		||||
 | 
			
		||||
    private val zip = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 | 
			
		||||
        ZipFile(file, StandardCharsets.ISO_8859_1)
 | 
			
		||||
        ZipFile(file.toFile(), StandardCharsets.ISO_8859_1)
 | 
			
		||||
    } else {
 | 
			
		||||
        ZipFile(file)
 | 
			
		||||
        ZipFile(file.toFile())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override var isLocal: Boolean = true
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
package eu.kanade.tachiyomi.util.storage
 | 
			
		||||
 | 
			
		||||
import com.hippo.unifile.UniFile
 | 
			
		||||
import org.jsoup.Jsoup
 | 
			
		||||
import org.jsoup.nodes.Document
 | 
			
		||||
import tachiyomi.core.storage.toFile
 | 
			
		||||
import java.io.Closeable
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.InputStream
 | 
			
		||||
@@ -11,12 +13,12 @@ import java.util.zip.ZipFile
 | 
			
		||||
/**
 | 
			
		||||
 * Wrapper over ZipFile to load files in epub format.
 | 
			
		||||
 */
 | 
			
		||||
class EpubFile(file: File) : Closeable {
 | 
			
		||||
class EpubFile(file: UniFile) : Closeable {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Zip file of this epub.
 | 
			
		||||
     */
 | 
			
		||||
    private val zip = ZipFile(file)
 | 
			
		||||
    private val zip = ZipFile(file.toFile())
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Path separator used by this epub.
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package tachiyomi.core.provider
 | 
			
		||||
package tachiyomi.core.storage
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.os.Environment
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
package tachiyomi.core.provider
 | 
			
		||||
package tachiyomi.core.storage
 | 
			
		||||
 | 
			
		||||
import java.io.File
 | 
			
		||||
 | 
			
		||||
@@ -1,9 +1,12 @@
 | 
			
		||||
package tachiyomi.core.storage
 | 
			
		||||
 | 
			
		||||
import com.hippo.unifile.UniFile
 | 
			
		||||
import java.io.File
 | 
			
		||||
 | 
			
		||||
val UniFile.extension: String?
 | 
			
		||||
    get() = name?.substringAfterLast('.')
 | 
			
		||||
 | 
			
		||||
val UniFile.nameWithoutExtension: String?
 | 
			
		||||
    get() = name?.substringBeforeLast('.')
 | 
			
		||||
 | 
			
		||||
fun UniFile.toFile(): File? = filePath?.let { File(it) }
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,9 @@ import kotlin.math.min
 | 
			
		||||
 | 
			
		||||
object ImageUtil {
 | 
			
		||||
 | 
			
		||||
    fun isImage(name: String, openStream: (() -> InputStream)? = null): Boolean {
 | 
			
		||||
    fun isImage(name: String?, openStream: (() -> InputStream)? = null): Boolean {
 | 
			
		||||
        if (name == null) return false
 | 
			
		||||
 | 
			
		||||
        val contentType = try {
 | 
			
		||||
            URLConnection.guessContentTypeFromName(name)
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
package tachiyomi.domain.storage.service
 | 
			
		||||
 | 
			
		||||
import tachiyomi.core.preference.PreferenceStore
 | 
			
		||||
import tachiyomi.core.provider.FolderProvider
 | 
			
		||||
import tachiyomi.core.storage.FolderProvider
 | 
			
		||||
 | 
			
		||||
class StoragePreferences(
 | 
			
		||||
    private val folderProvider: FolderProvider,
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,9 @@ import tachiyomi.core.metadata.comicinfo.ComicInfo
 | 
			
		||||
import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
 | 
			
		||||
import tachiyomi.core.metadata.comicinfo.getComicInfo
 | 
			
		||||
import tachiyomi.core.metadata.tachiyomi.MangaDetails
 | 
			
		||||
import tachiyomi.core.storage.extension
 | 
			
		||||
import tachiyomi.core.storage.nameWithoutExtension
 | 
			
		||||
import tachiyomi.core.storage.toFile
 | 
			
		||||
import tachiyomi.core.util.lang.withIOContext
 | 
			
		||||
import tachiyomi.core.util.system.ImageUtil
 | 
			
		||||
import tachiyomi.core.util.system.logcat
 | 
			
		||||
@@ -37,7 +40,6 @@ import tachiyomi.source.local.metadata.fillChapterMetadata
 | 
			
		||||
import tachiyomi.source.local.metadata.fillMangaMetadata
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.FileInputStream
 | 
			
		||||
import java.io.InputStream
 | 
			
		||||
import java.nio.charset.StandardCharsets
 | 
			
		||||
import java.util.zip.ZipFile
 | 
			
		||||
@@ -83,11 +85,11 @@ actual class LocalSource(
 | 
			
		||||
        }
 | 
			
		||||
        var mangaDirs = baseDirFiles
 | 
			
		||||
            // Filter out files that are hidden and is not a folder
 | 
			
		||||
            .filter { it.isDirectory && !it.name.startsWith('.') }
 | 
			
		||||
            .filter { it.isDirectory && !it.name.orEmpty().startsWith('.') }
 | 
			
		||||
            .distinctBy { it.name }
 | 
			
		||||
            .filter { // Filter by query or last modified
 | 
			
		||||
                if (lastModifiedLimit == 0L) {
 | 
			
		||||
                    it.name.contains(query, ignoreCase = true)
 | 
			
		||||
                    it.name.orEmpty().contains(query, ignoreCase = true)
 | 
			
		||||
                } else {
 | 
			
		||||
                    it.lastModified() >= lastModifiedLimit
 | 
			
		||||
                }
 | 
			
		||||
@@ -97,16 +99,16 @@ actual class LocalSource(
 | 
			
		||||
            when (filter) {
 | 
			
		||||
                is OrderBy.Popular -> {
 | 
			
		||||
                    mangaDirs = if (filter.state!!.ascending) {
 | 
			
		||||
                        mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
 | 
			
		||||
                        mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() })
 | 
			
		||||
                    } else {
 | 
			
		||||
                        mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
 | 
			
		||||
                        mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() })
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                is OrderBy.Latest -> {
 | 
			
		||||
                    mangaDirs = if (filter.state!!.ascending) {
 | 
			
		||||
                        mangaDirs.sortedBy(File::lastModified)
 | 
			
		||||
                        mangaDirs.sortedBy(UniFile::lastModified)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        mangaDirs.sortedByDescending(File::lastModified)
 | 
			
		||||
                        mangaDirs.sortedByDescending(UniFile::lastModified)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@@ -119,13 +121,13 @@ actual class LocalSource(
 | 
			
		||||
        // Transform mangaDirs to list of SManga
 | 
			
		||||
        val mangas = mangaDirs.map { mangaDir ->
 | 
			
		||||
            SManga.create().apply {
 | 
			
		||||
                title = mangaDir.name
 | 
			
		||||
                url = mangaDir.name
 | 
			
		||||
                title = mangaDir.name.orEmpty()
 | 
			
		||||
                url = mangaDir.name.orEmpty()
 | 
			
		||||
 | 
			
		||||
                // Try to find the cover
 | 
			
		||||
                coverManager.find(mangaDir.name)
 | 
			
		||||
                    ?.takeIf(File::exists)
 | 
			
		||||
                    ?.let { thumbnail_url = it.absolutePath }
 | 
			
		||||
                coverManager.find(mangaDir.name.orEmpty())
 | 
			
		||||
                    ?.takeIf(UniFile::exists)
 | 
			
		||||
                    ?.let { thumbnail_url = it.uri.toString() }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -155,7 +157,7 @@ actual class LocalSource(
 | 
			
		||||
    // Manga details related
 | 
			
		||||
    override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext {
 | 
			
		||||
        coverManager.find(manga.url)?.let {
 | 
			
		||||
            manga.thumbnail_url = it.absolutePath
 | 
			
		||||
            manga.thumbnail_url = it.uri.toString()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Augment manga details based on metadata files
 | 
			
		||||
@@ -174,13 +176,13 @@ actual class LocalSource(
 | 
			
		||||
                // Top level ComicInfo.xml
 | 
			
		||||
                comicInfoFile != null -> {
 | 
			
		||||
                    noXmlFile?.delete()
 | 
			
		||||
                    setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga)
 | 
			
		||||
                    setMangaDetailsFromComicInfoFile(comicInfoFile.openInputStream(), manga)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Old custom JSON format
 | 
			
		||||
                // TODO: remove support for this entirely after a while
 | 
			
		||||
                legacyJsonDetailsFile != null -> {
 | 
			
		||||
                    json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.inputStream()).run {
 | 
			
		||||
                    json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.openInputStream()).run {
 | 
			
		||||
                        title?.let { manga.title = it }
 | 
			
		||||
                        author?.let { manga.author = it }
 | 
			
		||||
                        artist?.let { manga.artist = it }
 | 
			
		||||
@@ -190,7 +192,7 @@ actual class LocalSource(
 | 
			
		||||
                    }
 | 
			
		||||
                    // Replace with ComicInfo.xml file
 | 
			
		||||
                    val comicInfo = manga.getComicInfo()
 | 
			
		||||
                    UniFile.fromFile(mangaDir)
 | 
			
		||||
                    mangaDir
 | 
			
		||||
                        ?.createFile(COMIC_INFO_FILE)
 | 
			
		||||
                        ?.openOutputStream()
 | 
			
		||||
                        ?.use {
 | 
			
		||||
@@ -206,7 +208,7 @@ actual class LocalSource(
 | 
			
		||||
                        .filter(Archive::isSupported)
 | 
			
		||||
                        .toList()
 | 
			
		||||
 | 
			
		||||
                    val folderPath = mangaDir?.absolutePath
 | 
			
		||||
                    val folderPath = mangaDir?.filePath
 | 
			
		||||
 | 
			
		||||
                    val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
 | 
			
		||||
                    if (copiedFile != null) {
 | 
			
		||||
@@ -224,11 +226,11 @@ actual class LocalSource(
 | 
			
		||||
        return@withIOContext manga
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun copyComicInfoFileFromArchive(chapterArchives: List<File>, folderPath: String?): File? {
 | 
			
		||||
    private fun copyComicInfoFileFromArchive(chapterArchives: List<UniFile>, folderPath: String?): File? {
 | 
			
		||||
        for (chapter in chapterArchives) {
 | 
			
		||||
            when (Format.valueOf(chapter)) {
 | 
			
		||||
                is Format.Zip -> {
 | 
			
		||||
                    ZipFile(chapter).use { zip: ZipFile ->
 | 
			
		||||
                    ZipFile(chapter.toFile()).use { zip: ZipFile ->
 | 
			
		||||
                        zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile ->
 | 
			
		||||
                            zip.getInputStream(comicInfoFile).buffered().use { stream ->
 | 
			
		||||
                                return copyComicInfoFile(stream, folderPath)
 | 
			
		||||
@@ -237,7 +239,7 @@ actual class LocalSource(
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                is Format.Rar -> {
 | 
			
		||||
                    JunrarArchive(chapter).use { rar ->
 | 
			
		||||
                    JunrarArchive(chapter.toFile()).use { rar ->
 | 
			
		||||
                        rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
 | 
			
		||||
                            rar.getInputStream(comicInfoFile).buffered().use { stream ->
 | 
			
		||||
                                return copyComicInfoFile(stream, folderPath)
 | 
			
		||||
@@ -276,9 +278,9 @@ actual class LocalSource(
 | 
			
		||||
                SChapter.create().apply {
 | 
			
		||||
                    url = "${manga.url}/${chapterFile.name}"
 | 
			
		||||
                    name = if (chapterFile.isDirectory) {
 | 
			
		||||
                        chapterFile.name
 | 
			
		||||
                        chapterFile.name.orEmpty()
 | 
			
		||||
                    } else {
 | 
			
		||||
                        chapterFile.nameWithoutExtension
 | 
			
		||||
                        chapterFile.nameWithoutExtension.orEmpty()
 | 
			
		||||
                    }
 | 
			
		||||
                    date_upload = chapterFile.lastModified()
 | 
			
		||||
                    chapter_number = ChapterRecognition
 | 
			
		||||
@@ -308,8 +310,8 @@ actual class LocalSource(
 | 
			
		||||
 | 
			
		||||
    fun getFormat(chapter: SChapter): Format {
 | 
			
		||||
        try {
 | 
			
		||||
            return File(fileSystem.getBaseDirectory(), chapter.url)
 | 
			
		||||
                .takeIf { it.exists() }
 | 
			
		||||
            return fileSystem.getBaseDirectory()
 | 
			
		||||
                ?.findFile(chapter.url)
 | 
			
		||||
                ?.let(Format.Companion::valueOf)
 | 
			
		||||
                ?: throw Exception(context.stringResource(MR.strings.chapter_not_found))
 | 
			
		||||
        } catch (e: Format.UnknownFormatException) {
 | 
			
		||||
@@ -319,18 +321,24 @@ actual class LocalSource(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun updateCover(chapter: SChapter, manga: SManga): File? {
 | 
			
		||||
    private fun updateCover(chapter: SChapter, manga: SManga): UniFile? {
 | 
			
		||||
        return try {
 | 
			
		||||
            when (val format = getFormat(chapter)) {
 | 
			
		||||
                is Format.Directory -> {
 | 
			
		||||
                    val entry = format.file.listFiles()
 | 
			
		||||
                        ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
 | 
			
		||||
                        ?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
 | 
			
		||||
                        ?.sortedWith { f1, f2 ->
 | 
			
		||||
                            f1.name.orEmpty().compareToCaseInsensitiveNaturalOrder(
 | 
			
		||||
                                f2.name.orEmpty(),
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                        ?.find {
 | 
			
		||||
                            !it.isDirectory && ImageUtil.isImage(it.name) { it.openInputStream() }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                    entry?.let { coverManager.update(manga, it.inputStream()) }
 | 
			
		||||
                    entry?.let { coverManager.update(manga, it.openInputStream()) }
 | 
			
		||||
                }
 | 
			
		||||
                is Format.Zip -> {
 | 
			
		||||
                    ZipFile(format.file).use { zip ->
 | 
			
		||||
                    ZipFile(format.file.toFile()).use { zip ->
 | 
			
		||||
                        val entry = zip.entries().toList()
 | 
			
		||||
                            .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
 | 
			
		||||
                            .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
 | 
			
		||||
@@ -339,7 +347,7 @@ actual class LocalSource(
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                is Format.Rar -> {
 | 
			
		||||
                    JunrarArchive(format.file).use { archive ->
 | 
			
		||||
                    JunrarArchive(format.file.toFile()).use { archive ->
 | 
			
		||||
                        val entry = archive.fileHeaders
 | 
			
		||||
                            .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
 | 
			
		||||
                            .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,9 @@ import android.content.Context
 | 
			
		||||
import com.hippo.unifile.UniFile
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
 | 
			
		||||
import tachiyomi.core.storage.nameWithoutExtension
 | 
			
		||||
import tachiyomi.core.util.system.ImageUtil
 | 
			
		||||
import tachiyomi.source.local.io.LocalSourceFileSystem
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.InputStream
 | 
			
		||||
 | 
			
		||||
private const val DEFAULT_COVER_NAME = "cover.jpg"
 | 
			
		||||
@@ -16,43 +16,37 @@ actual class LocalCoverManager(
 | 
			
		||||
    private val fileSystem: LocalSourceFileSystem,
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
    actual fun find(mangaUrl: String): File? {
 | 
			
		||||
    actual fun find(mangaUrl: String): UniFile? {
 | 
			
		||||
        return fileSystem.getFilesInMangaDirectory(mangaUrl)
 | 
			
		||||
            // Get all file whose names start with "cover"
 | 
			
		||||
            .filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
 | 
			
		||||
            // Get the first actual image
 | 
			
		||||
            .firstOrNull {
 | 
			
		||||
                ImageUtil.isImage(it.name) { it.inputStream() }
 | 
			
		||||
                ImageUtil.isImage(it.name) { it.openInputStream() }
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    actual fun update(
 | 
			
		||||
        manga: SManga,
 | 
			
		||||
        inputStream: InputStream,
 | 
			
		||||
    ): File? {
 | 
			
		||||
    ): UniFile? {
 | 
			
		||||
        val directory = fileSystem.getMangaDirectory(manga.url)
 | 
			
		||||
        if (directory == null) {
 | 
			
		||||
            inputStream.close()
 | 
			
		||||
            return null
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var targetFile = find(manga.url)
 | 
			
		||||
        if (targetFile == null) {
 | 
			
		||||
            targetFile = File(directory.absolutePath, DEFAULT_COVER_NAME)
 | 
			
		||||
            targetFile.createNewFile()
 | 
			
		||||
        }
 | 
			
		||||
        val targetFile = find(manga.url) ?: directory.createFile(DEFAULT_COVER_NAME)
 | 
			
		||||
 | 
			
		||||
        // It might not exist at this point
 | 
			
		||||
        targetFile.parentFile?.mkdirs()
 | 
			
		||||
        inputStream.use { input ->
 | 
			
		||||
            targetFile.outputStream().use { output ->
 | 
			
		||||
            targetFile.openOutputStream().use { output ->
 | 
			
		||||
                input.copyTo(output)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        DiskUtil.createNoMediaFile(UniFile.fromFile(directory), context)
 | 
			
		||||
        DiskUtil.createNoMediaFile(directory, context)
 | 
			
		||||
 | 
			
		||||
        manga.thumbnail_url = targetFile.absolutePath
 | 
			
		||||
        manga.thumbnail_url = targetFile.uri.toString()
 | 
			
		||||
        return targetFile
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,27 +1,31 @@
 | 
			
		||||
package tachiyomi.source.local.io
 | 
			
		||||
 | 
			
		||||
import tachiyomi.core.provider.FolderProvider
 | 
			
		||||
import java.io.File
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import androidx.core.net.toUri
 | 
			
		||||
import com.hippo.unifile.UniFile
 | 
			
		||||
import tachiyomi.core.storage.FolderProvider
 | 
			
		||||
 | 
			
		||||
actual class LocalSourceFileSystem(
 | 
			
		||||
    private val context: Context,
 | 
			
		||||
    private val folderProvider: FolderProvider,
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
    actual fun getBaseDirectory(): File {
 | 
			
		||||
        return File(folderProvider.directory(), "local")
 | 
			
		||||
    actual fun getBaseDirectory(): UniFile? {
 | 
			
		||||
        return UniFile.fromUri(context, folderProvider.path().toUri())
 | 
			
		||||
            ?.createDirectory("local")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    actual fun getFilesInBaseDirectory(): List<File> {
 | 
			
		||||
        return getBaseDirectory().listFiles().orEmpty().toList()
 | 
			
		||||
    actual fun getFilesInBaseDirectory(): List<UniFile> {
 | 
			
		||||
        return getBaseDirectory()?.listFiles().orEmpty().toList()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    actual fun getMangaDirectory(name: String): File? {
 | 
			
		||||
    actual fun getMangaDirectory(name: String): UniFile? {
 | 
			
		||||
        return getFilesInBaseDirectory()
 | 
			
		||||
            // Get the first mangaDir or null
 | 
			
		||||
            .firstOrNull { it.isDirectory && it.name == name }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    actual fun getFilesInMangaDirectory(name: String): List<File> {
 | 
			
		||||
    actual fun getFilesInMangaDirectory(name: String): List<UniFile> {
 | 
			
		||||
        return getFilesInBaseDirectory()
 | 
			
		||||
            // Filter out ones that are not related to the manga and is not a directory
 | 
			
		||||
            .filter { it.isDirectory && it.name == name }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
package tachiyomi.source.local.image
 | 
			
		||||
 | 
			
		||||
import com.hippo.unifile.UniFile
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.InputStream
 | 
			
		||||
 | 
			
		||||
expect class LocalCoverManager {
 | 
			
		||||
 | 
			
		||||
    fun find(mangaUrl: String): File?
 | 
			
		||||
    fun find(mangaUrl: String): UniFile?
 | 
			
		||||
 | 
			
		||||
    fun update(manga: SManga, inputStream: InputStream): File?
 | 
			
		||||
    fun update(manga: SManga, inputStream: InputStream): UniFile?
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,13 @@
 | 
			
		||||
package tachiyomi.source.local.io
 | 
			
		||||
 | 
			
		||||
import java.io.File
 | 
			
		||||
import com.hippo.unifile.UniFile
 | 
			
		||||
import tachiyomi.core.storage.extension
 | 
			
		||||
 | 
			
		||||
object Archive {
 | 
			
		||||
 | 
			
		||||
    private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
 | 
			
		||||
 | 
			
		||||
    fun isSupported(file: File): Boolean = with(file) {
 | 
			
		||||
        return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
 | 
			
		||||
    fun isSupported(file: UniFile): Boolean {
 | 
			
		||||
        return file.extension in SUPPORTED_ARCHIVE_TYPES
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,19 @@
 | 
			
		||||
package tachiyomi.source.local.io
 | 
			
		||||
 | 
			
		||||
import java.io.File
 | 
			
		||||
import com.hippo.unifile.UniFile
 | 
			
		||||
import tachiyomi.core.storage.extension
 | 
			
		||||
 | 
			
		||||
sealed interface Format {
 | 
			
		||||
    data class Directory(val file: File) : Format
 | 
			
		||||
    data class Zip(val file: File) : Format
 | 
			
		||||
    data class Rar(val file: File) : Format
 | 
			
		||||
    data class Epub(val file: File) : Format
 | 
			
		||||
    data class Directory(val file: UniFile) : Format
 | 
			
		||||
    data class Zip(val file: UniFile) : Format
 | 
			
		||||
    data class Rar(val file: UniFile) : Format
 | 
			
		||||
    data class Epub(val file: UniFile) : Format
 | 
			
		||||
 | 
			
		||||
    class UnknownFormatException : Exception()
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
 | 
			
		||||
        fun valueOf(file: File) = with(file) {
 | 
			
		||||
        fun valueOf(file: UniFile) = with(file) {
 | 
			
		||||
            when {
 | 
			
		||||
                isDirectory -> Directory(this)
 | 
			
		||||
                extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,14 @@
 | 
			
		||||
package tachiyomi.source.local.io
 | 
			
		||||
 | 
			
		||||
import java.io.File
 | 
			
		||||
import com.hippo.unifile.UniFile
 | 
			
		||||
 | 
			
		||||
expect class LocalSourceFileSystem {
 | 
			
		||||
 | 
			
		||||
    fun getBaseDirectory(): File
 | 
			
		||||
    fun getBaseDirectory(): UniFile?
 | 
			
		||||
 | 
			
		||||
    fun getFilesInBaseDirectory(): List<File>
 | 
			
		||||
    fun getFilesInBaseDirectory(): List<UniFile>
 | 
			
		||||
 | 
			
		||||
    fun getMangaDirectory(name: String): File?
 | 
			
		||||
    fun getMangaDirectory(name: String): UniFile?
 | 
			
		||||
 | 
			
		||||
    fun getFilesInMangaDirectory(name: String): List<File>
 | 
			
		||||
    fun getFilesInMangaDirectory(name: String): List<UniFile>
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user