Browse Source

Use UniFile for local source file handling

arkon 1 year ago
parent
commit
ca54984344
20 changed files with 110 additions and 95 deletions
  1. 2 2
      app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt
  2. 1 1
      app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt
  3. 5 6
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt
  4. 1 2
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt
  5. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt
  6. 4 3
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt
  7. 5 4
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt
  8. 4 2
      core/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt
  9. 1 1
      core/src/main/java/tachiyomi/core/storage/AndroidStorageFolderProvider.kt
  10. 1 1
      core/src/main/java/tachiyomi/core/storage/FolderProvider.kt
  11. 3 0
      core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt
  12. 3 1
      core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt
  13. 1 1
      domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt
  14. 38 30
      source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt
  15. 8 14
      source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt
  16. 12 8
      source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt
  17. 3 3
      source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt
  18. 4 3
      source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt
  19. 7 6
      source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt
  20. 5 5
      source-local/src/commonMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt

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

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt

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

+ 5 - 6
app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt

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

+ 1 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt

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

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt

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

+ 4 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt

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

+ 5 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt

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

+ 4 - 2
core/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt

@@ -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 - 1
core/src/main/java/tachiyomi/core/provider/AndroidStorageFolderProvider.kt → core/src/main/java/tachiyomi/core/storage/AndroidStorageFolderProvider.kt

@@ -1,4 +1,4 @@
-package tachiyomi.core.provider
+package tachiyomi.core.storage
 
 import android.content.Context
 import android.os.Environment

+ 1 - 1
core/src/main/java/tachiyomi/core/provider/FolderProvider.kt → core/src/main/java/tachiyomi/core/storage/FolderProvider.kt

@@ -1,4 +1,4 @@
-package tachiyomi.core.provider
+package tachiyomi.core.storage
 
 import java.io.File
 

+ 3 - 0
core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt

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

+ 3 - 1
core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt

@@ -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 - 1
domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt

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

+ 38 - 30
source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt

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

+ 8 - 14
source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt

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

+ 12 - 8
source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt

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

+ 3 - 3
source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt

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

+ 4 - 3
source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt

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

+ 7 - 6
source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt

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

+ 5 - 5
source-local/src/commonMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt

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