implement mihonapp/mihon#326 (#1104)

* implement mihonapp/mihon#326

Archives are now being read from channels

Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>

* disable parallelisms for loading into memory

* switched to mutex

* detekt changes

* more detekt baseline changes

---------

Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
This commit is contained in:
Shamicen 2024-03-16 16:59:00 +01:00 committed by GitHub
parent 45711cd394
commit 6719f22eff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 513 additions and 398 deletions

View File

@ -215,7 +215,7 @@ dependencies {
// Disk // Disk
implementation(libs.disklrucache) implementation(libs.disklrucache)
implementation(libs.unifile) implementation(libs.unifile)
implementation(libs.junrar) implementation(libs.bundles.archive)
// SY --> // SY -->
implementation(libs.zip4j) implementation(libs.zip4j)
// SY <-- // SY <--

View File

@ -122,6 +122,9 @@
# XmlUtil # XmlUtil
-keep public enum nl.adaptivity.xmlutil.EventType { *; } -keep public enum nl.adaptivity.xmlutil.EventType { *; }
# Apache Commons Compress
-keep class * extends org.apache.commons.compress.archivers.zip.ZipExtraField { <init>(); }
# Firebase # Firebase
-keep class com.google.firebase.installations.** { *; } -keep class com.google.firebase.installations.** { *; }
-keep interface com.google.firebase.installations.** { *; } -keep interface com.google.firebase.installations.** { *; }

View File

@ -570,10 +570,14 @@ object SettingsReaderScreen : SearchableSettings {
.toMap() .toMap()
.toImmutableMap(), .toImmutableMap(),
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.ListPreference(
pref = readerPreferences.cacheArchiveMangaOnDisk(), pref = readerPreferences.archiveReaderMode(),
title = stringResource(SYMR.strings.cache_archived_manga_to_disk), title = stringResource(SYMR.strings.pref_archive_reader_mode),
subtitle = stringResource(SYMR.strings.cache_archived_manga_to_disk_subtitle), subtitle = stringResource(SYMR.strings.pref_archive_reader_mode_summary),
entries = ReaderPreferences.archiveModeTypes
.mapIndexed { index, it -> index to stringResource(it) }
.toMap()
.toImmutableMap(),
), ),
), ),
) )

View File

@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.storage.CbzCrypto import eu.kanade.tachiyomi.util.storage.CbzCrypto
import eu.kanade.tachiyomi.util.storage.CbzCrypto.addFilesToZip
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
import eu.kanade.tachiyomi.util.storage.saveTo import eu.kanade.tachiyomi.util.storage.saveTo
@ -46,6 +45,7 @@ import logcat.LogPriority
import nl.adaptivity.xmlutil.serialization.XML import nl.adaptivity.xmlutil.serialization.XML
import okhttp3.Response import okhttp3.Response
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.addFilesToZip
import tachiyomi.core.common.storage.extension import tachiyomi.core.common.storage.extension
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNow import tachiyomi.core.common.util.lang.launchNow
@ -572,10 +572,6 @@ class Downloader(
tmpDir, tmpDir,
imageFile, imageFile,
filenamePrefix, filenamePrefix,
// SY -->
zip4jFile = null,
zip4jEntry = null,
// SY <--
) )
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to split downloaded image" } logcat(LogPriority.ERROR, e) { "Failed to split downloaded image" }

View File

@ -389,7 +389,6 @@ class ReaderViewModel @JvmOverloads constructor(
context = context, context = context,
downloadManager = downloadManager, downloadManager = downloadManager,
downloadProvider = downloadProvider, downloadProvider = downloadProvider,
tempFileManager = tempFileManager,
manga = manga, manga = manga,
source = source, /* SY --> */ source = source, /* SY --> */
sourceManager = sourceManager, sourceManager = sourceManager,

View File

@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.UniFileTempFileManager
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
@ -28,7 +27,6 @@ class ChapterLoader(
private val context: Context, private val context: Context,
private val downloadManager: DownloadManager, private val downloadManager: DownloadManager,
private val downloadProvider: DownloadProvider, private val downloadProvider: DownloadProvider,
private val tempFileManager: UniFileTempFileManager,
private val manga: Manga, private val manga: Manga,
private val source: Source, private val source: Source,
// SY --> // SY -->
@ -121,36 +119,39 @@ class ChapterLoader(
source = source, source = source,
downloadManager = downloadManager, downloadManager = downloadManager,
downloadProvider = downloadProvider, downloadProvider = downloadProvider,
tempFileManager = tempFileManager,
) )
source is HttpSource -> HttpPageLoader(chapter, source) source is HttpSource -> HttpPageLoader(chapter, source)
source is LocalSource -> source.getFormat(chapter.chapter).let { format -> source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
when (format) { when (format) {
is Format.Directory -> DirectoryPageLoader(format.file) is Format.Directory -> DirectoryPageLoader(format.file)
is Format.Zip -> ZipPageLoader(tempFileManager.createTempFile(format.file)) is Format.Zip -> ZipPageLoader(format.file, context)
is Format.Rar -> try { is Format.Rar -> try {
RarPageLoader(tempFileManager.createTempFile(format.file)) RarPageLoader(format.file)
} catch (e: UnsupportedRarV5Exception) { } catch (e: UnsupportedRarV5Exception) {
error(context.stringResource(MR.strings.loader_rar5_error)) error(context.stringResource(MR.strings.loader_rar5_error))
} }
is Format.Epub -> EpubPageLoader(tempFileManager.createTempFile(format.file)) is Format.Epub -> EpubPageLoader(format.file, context)
} }
} }
else -> error(context.stringResource(MR.strings.loader_not_implemented_error)) else -> error(context.stringResource(MR.strings.loader_not_implemented_error))
} }
} }
// SY <-- // SY <--
isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager, downloadProvider, tempFileManager) isDownloaded -> DownloadPageLoader(chapter, manga, source, downloadManager, downloadProvider)
source is LocalSource -> source.getFormat(chapter.chapter).let { format -> source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
when (format) { when (format) {
is Format.Directory -> DirectoryPageLoader(format.file) is Format.Directory -> DirectoryPageLoader(format.file)
is Format.Zip -> ZipPageLoader(tempFileManager.createTempFile(format.file)) // SY -->
is Format.Zip -> ZipPageLoader(format.file, context)
is Format.Rar -> try { is Format.Rar -> try {
RarPageLoader(tempFileManager.createTempFile(format.file)) RarPageLoader(format.file)
// SY <--
} catch (e: UnsupportedRarV5Exception) { } catch (e: UnsupportedRarV5Exception) {
error(context.stringResource(MR.strings.loader_rar5_error)) error(context.stringResource(MR.strings.loader_rar5_error))
} }
is Format.Epub -> EpubPageLoader(tempFileManager.createTempFile(format.file)) // SY -->
is Format.Epub -> EpubPageLoader(format.file, context)
// SY <--
} }
} }
source is HttpSource -> HttpPageLoader(chapter, source) source is HttpSource -> HttpPageLoader(chapter, source)

View File

@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import tachiyomi.core.common.storage.UniFileTempFileManager
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -23,7 +22,6 @@ internal class DownloadPageLoader(
private val source: Source, private val source: Source,
private val downloadManager: DownloadManager, private val downloadManager: DownloadManager,
private val downloadProvider: DownloadProvider, private val downloadProvider: DownloadProvider,
private val tempFileManager: UniFileTempFileManager,
) : PageLoader() { ) : PageLoader() {
private val context: Application by injectLazy() private val context: Application by injectLazy()
@ -48,7 +46,9 @@ internal class DownloadPageLoader(
} }
private suspend fun getPagesFromArchive(file: UniFile): List<ReaderPage> { private suspend fun getPagesFromArchive(file: UniFile): List<ReaderPage> {
val loader = ZipPageLoader(tempFileManager.createTempFile(file)).also { zipPageLoader = it } // SY -->
val loader = ZipPageLoader(file, context).also { zipPageLoader = it }
// SY <--
return loader.getPages() return loader.getPages()
} }

View File

@ -1,16 +1,19 @@
package eu.kanade.tachiyomi.ui.reader.loader package eu.kanade.tachiyomi.ui.reader.loader
import android.content.Context
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.storage.EpubFile import eu.kanade.tachiyomi.util.storage.EpubFile
import java.io.File
/** /**
* Loader used to load a chapter from a .epub file. * Loader used to load a chapter from a .epub file.
*/ */
internal class EpubPageLoader(file: File) : PageLoader() { // SY -->
internal class EpubPageLoader(file: UniFile, context: Context) : PageLoader() {
private val epub = EpubFile(file) private val epub = EpubFile(file, context)
// SY <--
override var isLocal: Boolean = true override var isLocal: Boolean = true

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.reader.loader package eu.kanade.tachiyomi.ui.reader.loader
import android.app.Application import android.app.Application
import android.os.Build
import com.github.junrar.Archive import com.github.junrar.Archive
import com.github.junrar.rarfile.FileHeader import com.github.junrar.rarfile.FileHeader
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
@ -8,6 +9,14 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import tachiyomi.core.common.storage.UniFileTempFileManager
import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.ImageUtil
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
@ -19,11 +28,17 @@ import java.util.concurrent.Executors
/** /**
* Loader used to load a chapter from a .rar or .cbr file. * 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)
// SY --> // SY -->
private val tempFileManager: UniFileTempFileManager by injectLazy()
private val rar = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Archive(tempFileManager.createTempFile(file))
} else {
Archive(file.openInputStream())
}
private val context: Application by injectLazy() private val context: Application by injectLazy()
private val readerPreferences: ReaderPreferences by injectLazy() private val readerPreferences: ReaderPreferences by injectLazy()
private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also { private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also {
@ -31,20 +46,21 @@ internal class RarPageLoader(file: File) : PageLoader() {
} }
init { init {
if (readerPreferences.cacheArchiveMangaOnDisk().get()) { if (readerPreferences.archiveReaderMode().get() == ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK) {
tmpDir.mkdirs() tmpDir.mkdirs()
Archive(file).use { rar -> rar.fileHeaders.asSequence()
rar.fileHeaders.asSequence() .filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } }
.filterNot { it.isDirectory } .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.forEach { header -> .forEach { header ->
val pageOutputStream = File(tmpDir, header.fileName.substringAfterLast("/")) File(tmpDir, header.fileName.substringAfterLast("/"))
.also { it.createNewFile() } .also { it.createNewFile() }
.outputStream() .outputStream()
getStream(header).use { .use { output ->
it.copyTo(pageOutputStream) rar.getInputStream(header).use { input ->
input.copyTo(output)
}
} }
} }
}
} }
} }
// SY <-- // SY <--
@ -58,16 +74,37 @@ internal class RarPageLoader(file: File) : PageLoader() {
override suspend fun getPages(): List<ReaderPage> { override suspend fun getPages(): List<ReaderPage> {
// SY --> // SY -->
if (readerPreferences.cacheArchiveMangaOnDisk().get()) { if (readerPreferences.archiveReaderMode().get() == ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK) {
return DirectoryPageLoader(UniFile.fromFile(tmpDir)!!).getPages() return DirectoryPageLoader(UniFile.fromFile(tmpDir)!!).getPages()
} }
val mutex = Mutex()
// SY <-- // SY <--
return rar.fileHeaders.asSequence() return rar.fileHeaders.asSequence()
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } } .filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } }
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.mapIndexed { i, header -> .mapIndexed { i, header ->
// SY -->
val imageBytesDeferred: Deferred<ByteArray>? =
when (readerPreferences.archiveReaderMode().get()) {
ReaderPreferences.ArchiveReaderMode.LOAD_INTO_MEMORY -> {
CoroutineScope(Dispatchers.IO).async {
mutex.withLock {
getStream(header).buffered().use { stream ->
stream.readBytes()
}
}
}
}
else -> null
}
val imageBytes by lazy { runBlocking { imageBytesDeferred?.await() } }
// SY <--
ReaderPage(i).apply { ReaderPage(i).apply {
stream = { getStream(header) } // SY -->
stream = { imageBytes?.copyOf()?.inputStream() ?: getStream(header) }
// SY <--
status = Page.State.READY status = Page.State.READY
} }
} }

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.loader package eu.kanade.tachiyomi.ui.reader.loader
import android.app.Application import android.content.Context
import android.os.Build import android.os.Build
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -8,107 +8,155 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.CbzCrypto import eu.kanade.tachiyomi.util.storage.CbzCrypto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.apache.commons.compress.archivers.zip.ZipFile
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.UniFileTempFileManager
import tachiyomi.core.common.storage.isEncryptedZip
import tachiyomi.core.common.storage.openReadOnlyChannel
import tachiyomi.core.common.storage.testCbzPassword
import tachiyomi.core.common.storage.unzip
import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.i18n.sy.SYMR import tachiyomi.i18n.sy.SYMR
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.nio.charset.StandardCharsets import java.nio.channels.SeekableByteChannel
import java.util.zip.ZipFile
import net.lingala.zip4j.ZipFile as Zip4jFile import net.lingala.zip4j.ZipFile as Zip4jFile
/** /**
* Loader used to load a chapter from a .zip or .cbz file. * Loader used to load a chapter from a .zip or .cbz file.
*/ */
internal class ZipPageLoader(file: File) : PageLoader() { internal class ZipPageLoader(file: UniFile, context: Context) : PageLoader() {
// SY --> // SY -->
private val context: Application by injectLazy() private val channel: SeekableByteChannel = file.openReadOnlyChannel(context)
private val tempFileManager: UniFileTempFileManager by injectLazy()
private val readerPreferences: ReaderPreferences by injectLazy() private val readerPreferences: ReaderPreferences by injectLazy()
private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also { private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also {
it.deleteRecursively() it.deleteRecursively()
} }
private val zip4j: Zip4jFile = Zip4jFile(file)
private val zip: ZipFile? = private val apacheZip: ZipFile? = if (!file.isEncryptedZip() && Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { ZipFile(channel)
if (!zip4j.isEncrypted) ZipFile(file, StandardCharsets.ISO_8859_1) else null } else {
null
}
private val tmpFile =
if (
apacheZip == null &&
readerPreferences.archiveReaderMode().get() != ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK
) {
tempFileManager.createTempFile(file)
} else { } else {
if (!zip4j.isEncrypted) ZipFile(file) else null null
}
private val zip4j =
if (apacheZip == null && tmpFile != null) {
Zip4jFile(tmpFile)
} else {
null
} }
init { init {
Zip4jFile(file).use { zip -> if (file.isEncryptedZip()) {
if (zip.isEncrypted) { if (!file.testCbzPassword()) {
if (!CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())) { this.recycle()
this.recycle() throw IllegalStateException(context.stringResource(SYMR.strings.wrong_cbz_archive_password))
throw IllegalStateException(context.stringResource(SYMR.strings.wrong_cbz_archive_password))
}
zip4j.setPassword(CbzCrypto.getDecryptedPasswordCbz())
if (readerPreferences.cacheArchiveMangaOnDisk().get()) {
unzip()
}
} else {
if (readerPreferences.cacheArchiveMangaOnDisk().get()) {
unzip()
}
} }
zip4j?.setPassword(CbzCrypto.getDecryptedPasswordCbz())
}
if (readerPreferences.archiveReaderMode().get() == ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK) {
file.unzip(tmpDir, onlyCopyImages = true)
} }
} }
// SY <-- // SY <--
override fun recycle() { override fun recycle() {
super.recycle() super.recycle()
zip?.close() apacheZip?.close()
// SY --> // SY -->
zip4j.close() zip4j?.close()
tmpDir.deleteRecursively() tmpDir.deleteRecursively()
} }
private fun unzip() {
tmpDir.mkdirs()
zip4j.fileHeaders.asSequence()
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { zip4j.getInputStream(it) } }
.forEach { entry ->
zip4j.extractFile(entry, tmpDir.absolutePath)
}
}
override var isLocal: Boolean = true override var isLocal: Boolean = true
override suspend fun getPages(): List<ReaderPage> { override suspend fun getPages(): List<ReaderPage> {
if (readerPreferences.cacheArchiveMangaOnDisk().get()) { if (readerPreferences.archiveReaderMode().get() == ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK) {
return DirectoryPageLoader(UniFile.fromFile(tmpDir)!!).getPages() return DirectoryPageLoader(UniFile.fromFile(tmpDir)!!).getPages()
} }
return if (apacheZip == null) {
if (zip == null) { loadZip4j()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
zip4j.charset = StandardCharsets.ISO_8859_1
}
return zip4j.fileHeaders.asSequence()
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { zip4j.getInputStream(it) } }
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.mapIndexed { i, entry ->
ReaderPage(i).apply {
stream = { zip4j.getInputStream(entry) }
status = Page.State.READY
zip4jFile = zip4j
zip4jEntry = entry
}
}.toList()
} else { } else {
// SY <-- loadApacheZip(apacheZip)
return zip.entries().asSequence()
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.mapIndexed { i, entry ->
ReaderPage(i).apply {
stream = { zip.getInputStream(entry) }
status = Page.State.READY
}
}.toList()
} }
} }
private fun loadZip4j(): List<ReaderPage> {
val mutex = Mutex()
return zip4j!!.fileHeaders.asSequence()
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { zip4j.getInputStream(it) } }
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.mapIndexed { i, entry ->
val imageBytesDeferred: Deferred<ByteArray>? =
when (readerPreferences.archiveReaderMode().get()) {
ReaderPreferences.ArchiveReaderMode.LOAD_INTO_MEMORY -> {
CoroutineScope(Dispatchers.IO).async {
mutex.withLock {
zip4j.getInputStream(entry).buffered().use { stream ->
stream.readBytes()
}
}
}
}
else -> null
}
val imageBytes by lazy { runBlocking { imageBytesDeferred?.await() } }
ReaderPage(i).apply {
stream = { imageBytes?.copyOf()?.inputStream() ?: zip4j.getInputStream(entry) }
status = Page.State.READY
}
}.toList()
}
private fun loadApacheZip(zip: ZipFile): List<ReaderPage> {
val mutex = Mutex()
return zip.entries.asSequence()
.filter { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.mapIndexed { i, entry ->
val imageBytesDeferred: Deferred<ByteArray>? =
when (readerPreferences.archiveReaderMode().get()) {
ReaderPreferences.ArchiveReaderMode.LOAD_INTO_MEMORY -> {
CoroutineScope(Dispatchers.IO).async {
mutex.withLock {
zip.getInputStream(entry).buffered().use { stream ->
stream.readBytes()
}
}
}
}
else -> null
}
val imageBytes by lazy { runBlocking { imageBytesDeferred?.await() } }
ReaderPage(i).apply {
stream = { imageBytes?.copyOf()?.inputStream() ?: zip.getInputStream(entry) }
status = Page.State.READY
}
}.toList()
}
// SY <--
/** /**
* No additional action required to load the page * No additional action required to load the page
*/ */

View File

@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.model package eu.kanade.tachiyomi.ui.reader.model
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.model.FileHeader
import java.io.InputStream import java.io.InputStream
open class ReaderPage( open class ReaderPage(
@ -10,9 +8,6 @@ open class ReaderPage(
url: String = "", url: String = "",
imageUrl: String? = null, imageUrl: String? = null,
// SY --> // SY -->
/** zip4j inputStreams do not support mark() and release(), so they must be passed to ImageUtil */
var zip4jFile: ZipFile? = null,
var zip4jEntry: FileHeader? = null,
/** Value to check if this page is used to as if it was too wide */ /** Value to check if this page is used to as if it was too wide */
var shiftedPage: Boolean = false, var shiftedPage: Boolean = false,
/** Value to check if a page is can be doubled up, but can't because the next page is too wide */ /** Value to check if a page is can be doubled up, but can't because the next page is too wide */

View File

@ -180,9 +180,9 @@ class ReaderPreferences(
fun centerMarginType() = preferenceStore.getInt("center_margin_type", PagerConfig.CenterMarginType.NONE) fun centerMarginType() = preferenceStore.getInt("center_margin_type", PagerConfig.CenterMarginType.NONE)
fun cacheArchiveMangaOnDisk() = preferenceStore.getBoolean("cache_archive_manga_on_disk", false) fun archiveReaderMode() = preferenceStore.getInt("archive_reader_mode", ArchiveReaderMode.LOAD_FROM_FILE)
fun markReadDupe() = preferenceStore.getBoolean("mark_read_dupe", false) fun markReadDupe() = preferenceStore.getBoolean("mark_read_dupe", false)
// SY <-- // SY <--
enum class TappingInvertMode( enum class TappingInvertMode(
@ -203,6 +203,12 @@ class ReaderPreferences(
LOWEST(47), LOWEST(47),
} }
object ArchiveReaderMode {
const val LOAD_FROM_FILE = 0
const val LOAD_INTO_MEMORY = 1
const val CACHE_TO_DISK = 2
}
companion object { companion object {
const val WEBTOON_PADDING_MIN = 0 const val WEBTOON_PADDING_MIN = 0
const val WEBTOON_PADDING_MAX = 25 const val WEBTOON_PADDING_MAX = 25
@ -264,6 +270,12 @@ class ReaderPreferences(
SYMR.strings.center_margin_wide_page, SYMR.strings.center_margin_wide_page,
SYMR.strings.center_margin_double_and_wide_page, SYMR.strings.center_margin_double_and_wide_page,
) )
val archiveModeTypes = listOf(
SYMR.strings.archive_mode_load_from_file,
SYMR.strings.archive_mode_load_into_memory,
SYMR.strings.archive_mode_cache_to_disk
)
// SY <-- // SY <--
} }
} }

View File

@ -230,13 +230,7 @@ class PagerPageHolder(
return splitInHalf(imageStream) return splitInHalf(imageStream)
} }
val isDoublePage = ImageUtil.isWideImage( val isDoublePage = ImageUtil.isWideImage(imageStream)
imageStream,
// SY -->
page.zip4jFile,
page.zip4jEntry,
// SY <--
)
if (!isDoublePage) { if (!isDoublePage) {
return imageStream return imageStream
} }
@ -247,13 +241,7 @@ class PagerPageHolder(
} }
private fun rotateDualPage(imageStream: BufferedInputStream): InputStream { private fun rotateDualPage(imageStream: BufferedInputStream): InputStream {
val isDoublePage = ImageUtil.isWideImage( val isDoublePage = ImageUtil.isWideImage(imageStream)
imageStream,
// SY -->
page.zip4jFile,
page.zip4jEntry,
// SY <--
)
return if (isDoublePage) { return if (isDoublePage) {
val rotation = if (viewer.config.dualPageRotateToFitInvert) -90f else 90f val rotation = if (viewer.config.dualPageRotateToFitInvert) -90f else 90f
ImageUtil.rotateImage(imageStream, rotation) ImageUtil.rotateImage(imageStream, rotation)
@ -267,13 +255,7 @@ class PagerPageHolder(
if (imageStream2 == null) { if (imageStream2 == null) {
return if (imageStream is BufferedInputStream && return if (imageStream is BufferedInputStream &&
!ImageUtil.isAnimatedAndSupported(imageStream) && !ImageUtil.isAnimatedAndSupported(imageStream) &&
ImageUtil.isWideImage( ImageUtil.isWideImage(imageStream) &&
imageStream,
// SY -->
page.zip4jFile,
page.zip4jEntry,
// SY <--
) &&
viewer.config.centerMarginType and PagerConfig.CenterMarginType.WIDE_PAGE_CENTER_MARGIN > 0 && viewer.config.centerMarginType and PagerConfig.CenterMarginType.WIDE_PAGE_CENTER_MARGIN > 0 &&
!viewer.config.imageCropBorders !viewer.config.imageCropBorders
) { ) {

View File

@ -224,13 +224,7 @@ class WebtoonPageHolder(
} }
if (viewer.config.dualPageSplit) { if (viewer.config.dualPageSplit) {
val isDoublePage = ImageUtil.isWideImage( val isDoublePage = ImageUtil.isWideImage(imageStream)
imageStream,
// SY -->
page?.zip4jFile,
page?.zip4jEntry,
// SY <--
)
if (isDoublePage) { if (isDoublePage) {
val upperSide = if (viewer.config.dualPageInvert) ImageUtil.Side.LEFT else ImageUtil.Side.RIGHT val upperSide = if (viewer.config.dualPageInvert) ImageUtil.Side.LEFT else ImageUtil.Side.RIGHT
return ImageUtil.splitAndMerge(imageStream, upperSide) return ImageUtil.splitAndMerge(imageStream, upperSide)
@ -241,13 +235,7 @@ class WebtoonPageHolder(
} }
private fun rotateDualPage(imageStream: BufferedInputStream): InputStream { private fun rotateDualPage(imageStream: BufferedInputStream): InputStream {
val isDoublePage = ImageUtil.isWideImage( val isDoublePage = ImageUtil.isWideImage(imageStream)
imageStream,
// SY -->
page?.zip4jFile,
page?.zip4jEntry,
// SY <--
)
return if (isDoublePage) { return if (isDoublePage) {
val rotation = if (viewer.config.dualPageRotateToFitInvert) -90f else 90f val rotation = if (viewer.config.dualPageRotateToFitInvert) -90f else 90f
ImageUtil.rotateImage(imageStream, rotation) ImageUtil.rotateImage(imageStream, rotation)

View File

@ -656,6 +656,12 @@ object EXHMigrations {
remove(Preference.appStateKey("trusted_signatures")) remove(Preference.appStateKey("trusted_signatures"))
} }
} }
if (oldVersion under 66) {
val cacheImagesToDisk = prefs.getBoolean("cache_archive_manga_on_disk", false)
if (cacheImagesToDisk) {
readerPreferences.archiveReaderMode().set(ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK)
}
}
if (oldVersion under 66) { if (oldVersion under 66) {
if (prefs.getBoolean(Preference.privateKey("encrypt_database"), false)) { if (prefs.getBoolean(Preference.privateKey("encrypt_database"), false)) {

View File

@ -187,7 +187,6 @@ class InterceptActivity : BaseActivity() {
lifecycleScope.launchIO { lifecycleScope.launchIO {
loadGalleryEnd(gallery, sources[index]) loadGalleryEnd(gallery, sources[index])
} }
} }
.show() .show()
} else { } else {

View File

@ -172,7 +172,7 @@
<ID>ComplexCondition:LibraryUpdateJob.kt$LibraryUpdateJob$group == LibraryGroup.BY_DEFAULT || groupLibraryUpdateType == GroupLibraryMode.GLOBAL || (groupLibraryUpdateType == GroupLibraryMode.ALL_BUT_UNGROUPED &amp;&amp; group == LibraryGroup.UNGROUPED)</ID> <ID>ComplexCondition:LibraryUpdateJob.kt$LibraryUpdateJob$group == LibraryGroup.BY_DEFAULT || groupLibraryUpdateType == GroupLibraryMode.GLOBAL || (groupLibraryUpdateType == GroupLibraryMode.ALL_BUT_UNGROUPED &amp;&amp; group == LibraryGroup.UNGROUPED)</ID>
<ID>ComplexCondition:MangaRestorer.kt$MangaRestorer$customTitle != null || customArtist != null || customAuthor != null || customThumbnailUrl != null || customDescription != null || customGenre != null || customStatus != 0</ID> <ID>ComplexCondition:MangaRestorer.kt$MangaRestorer$customTitle != null || customArtist != null || customAuthor != null || customThumbnailUrl != null || customDescription != null || customGenre != null || customStatus != 0</ID>
<ID>ComplexCondition:MangaScreenModel.kt$MangaScreenModel$(selectedItem.selected &amp;&amp; selected) || (!selectedItem.selected &amp;&amp; !selected)</ID> <ID>ComplexCondition:MangaScreenModel.kt$MangaScreenModel$(selectedItem.selected &amp;&amp; selected) || (!selectedItem.selected &amp;&amp; !selected)</ID>
<ID>ComplexCondition:PagerPageHolder.kt$PagerPageHolder$imageStream is BufferedInputStream &amp;&amp; !ImageUtil.isAnimatedAndSupported(imageStream) &amp;&amp; ImageUtil.isWideImage( imageStream, // SY --&gt; page.zip4jFile, page.zip4jEntry, // SY &lt;-- ) &amp;&amp; viewer.config.centerMarginType and PagerConfig.CenterMarginType.WIDE_PAGE_CENTER_MARGIN &gt; 0 &amp;&amp; !viewer.config.imageCropBorders</ID> <ID>ComplexCondition:PagerPageHolder.kt$PagerPageHolder$imageStream is BufferedInputStream &amp;&amp; !ImageUtil.isAnimatedAndSupported(imageStream) &amp;&amp; ImageUtil.isWideImage(imageStream) &amp;&amp; viewer.config.centerMarginType and PagerConfig.CenterMarginType.WIDE_PAGE_CENTER_MARGIN &gt; 0 &amp;&amp; !viewer.config.imageCropBorders</ID>
<ID>ComplexCondition:PagerViewerAdapter.kt$PagerViewerAdapter$items[itemIndex]?.fullPage == true &amp;&amp; itemIndex &gt; 0 &amp;&amp; items[itemIndex - 1] != null &amp;&amp; (itemIndex - 1) % 2 == 0</ID> <ID>ComplexCondition:PagerViewerAdapter.kt$PagerViewerAdapter$items[itemIndex]?.fullPage == true &amp;&amp; itemIndex &gt; 0 &amp;&amp; items[itemIndex - 1] != null &amp;&amp; (itemIndex - 1) % 2 == 0</ID>
<ID>ComplexCondition:ReaderActivity.kt$ReaderActivity$readerPreferences.useAutoWebtoon().get() &amp;&amp; (manga?.readingMode?.toInt() ?: ReadingMode.DEFAULT.flagValue) == ReadingMode.DEFAULT.flagValue &amp;&amp; defaultReaderType != null &amp;&amp; defaultReaderType == ReadingMode.WEBTOON.flagValue</ID> <ID>ComplexCondition:ReaderActivity.kt$ReaderActivity$readerPreferences.useAutoWebtoon().get() &amp;&amp; (manga?.readingMode?.toInt() ?: ReadingMode.DEFAULT.flagValue) == ReadingMode.DEFAULT.flagValue &amp;&amp; defaultReaderType != null &amp;&amp; defaultReaderType == ReadingMode.WEBTOON.flagValue</ID>
<ID>ComplexCondition:ReaderNavigationOverlayView.kt$ReaderNavigationOverlayView$isVisible || (!showOnStart &amp;&amp; firstLaunch) || navigation is DisabledNavigation</ID> <ID>ComplexCondition:ReaderNavigationOverlayView.kt$ReaderNavigationOverlayView$isVisible || (!showOnStart &amp;&amp; firstLaunch) || navigation is DisabledNavigation</ID>
@ -539,8 +539,8 @@
<ID>LongMethod:UpdatesQuery.kt$UpdatesQuery$override fun &lt;R&gt; execute(mapper: (SqlCursor) -&gt; QueryResult&lt;R&gt;): QueryResult&lt;R&gt;</ID> <ID>LongMethod:UpdatesQuery.kt$UpdatesQuery$override fun &lt;R&gt; execute(mapper: (SqlCursor) -&gt; QueryResult&lt;R&gt;): QueryResult&lt;R&gt;</ID>
<ID>LongMethod:UpdatesUiItem.kt$internal fun LazyListScope.updatesUiItems( uiModels: List&lt;UpdatesUiModel&gt;, selectionMode: Boolean, // SY --&gt; preserveReadingPosition: Boolean, // SY &lt;-- onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -&gt; Unit, onClickCover: (UpdatesItem) -&gt; Unit, onClickUpdate: (UpdatesItem) -&gt; Unit, onDownloadChapter: (List&lt;UpdatesItem&gt;, ChapterDownloadAction) -&gt; Unit, )</ID> <ID>LongMethod:UpdatesUiItem.kt$internal fun LazyListScope.updatesUiItems( uiModels: List&lt;UpdatesUiModel&gt;, selectionMode: Boolean, // SY --&gt; preserveReadingPosition: Boolean, // SY &lt;-- onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -&gt; Unit, onClickCover: (UpdatesItem) -&gt; Unit, onClickUpdate: (UpdatesItem) -&gt; Unit, onDownloadChapter: (List&lt;UpdatesItem&gt;, ChapterDownloadAction) -&gt; Unit, )</ID>
<ID>LongMethod:WebtoonRecyclerView.kt$WebtoonRecyclerView.Detector$override fun onTouchEvent(ev: MotionEvent): Boolean</ID> <ID>LongMethod:WebtoonRecyclerView.kt$WebtoonRecyclerView.Detector$override fun onTouchEvent(ev: MotionEvent): Boolean</ID>
<ID>LongParameterList:ChapterLoader.kt$ChapterLoader$( private val context: Context, private val downloadManager: DownloadManager, private val downloadProvider: DownloadProvider, private val tempFileManager: UniFileTempFileManager, private val manga: Manga, private val source: Source, // SY --&gt; private val sourceManager: SourceManager, private val readerPrefs: ReaderPreferences, private val mergedReferences: List&lt;MergedMangaReference&gt;, private val mergedManga: Map&lt;Long, Manga&gt;, // SY &lt;-- )</ID> <ID>LongParameterList:ChapterLoader.kt$ChapterLoader$( private val context: Context, private val downloadManager: DownloadManager, private val downloadProvider: DownloadProvider, private val manga: Manga, private val source: Source, // SY --&gt; private val sourceManager: SourceManager, private val readerPrefs: ReaderPreferences, private val mergedReferences: List&lt;MergedMangaReference&gt;, private val mergedManga: Map&lt;Long, Manga&gt;, // SY &lt;-- )</ID>
<ID>LongParameterList:ChapterMapper.kt$ChapterMapper$( id: Long, mangaId: Long, url: String, name: String, scanlator: String?, read: Boolean, bookmark: Boolean, lastPageRead: Long, chapterNumber: Double, sourceOrder: Long, dateFetch: Long, dateUpload: Long, lastModifiedAt: Long, )</ID> <ID>LongParameterList:ChapterMapper.kt$ChapterMapper$( id: Long, mangaId: Long, url: String, name: String, scanlator: String?, read: Boolean, bookmark: Boolean, lastPageRead: Long, chapterNumber: Double, sourceOrder: Long, dateFetch: Long, dateUpload: Long, lastModifiedAt: Long, version: Long, @Suppress("UNUSED_PARAMETER") isSyncing: Long, )</ID>
<ID>LongParameterList:Chip.kt$ChipColors$( private val containerColor: Color, private val labelColor: Color, private val leadingIconContentColor: Color, private val trailingIconContentColor: Color, private val disabledContainerColor: Color, private val disabledLabelColor: Color, private val disabledLeadingIconContentColor: Color, private val disabledTrailingIconContentColor: Color, )</ID> <ID>LongParameterList:Chip.kt$ChipColors$( private val containerColor: Color, private val labelColor: Color, private val leadingIconContentColor: Color, private val trailingIconContentColor: Color, private val disabledContainerColor: Color, private val disabledLabelColor: Color, private val disabledLeadingIconContentColor: Color, private val disabledTrailingIconContentColor: Color, )</ID>
<ID>LongParameterList:EXHMigrations.kt$EXHMigrations$( context: Context, preferenceStore: PreferenceStore, basePreferences: BasePreferences, uiPreferences: UiPreferences, networkPreferences: NetworkPreferences, sourcePreferences: SourcePreferences, securityPreferences: SecurityPreferences, libraryPreferences: LibraryPreferences, readerPreferences: ReaderPreferences, backupPreferences: BackupPreferences, trackerManager: TrackerManager, pagePreviewCache: PagePreviewCache, )</ID> <ID>LongParameterList:EXHMigrations.kt$EXHMigrations$( context: Context, preferenceStore: PreferenceStore, basePreferences: BasePreferences, uiPreferences: UiPreferences, networkPreferences: NetworkPreferences, sourcePreferences: SourcePreferences, securityPreferences: SecurityPreferences, libraryPreferences: LibraryPreferences, readerPreferences: ReaderPreferences, backupPreferences: BackupPreferences, trackerManager: TrackerManager, pagePreviewCache: PagePreviewCache, )</ID>
<ID>LongParameterList:FavoritesEntryRepositoryImpl.kt$FavoritesEntryRepositoryImpl$( gid: String, token: String, title: String, category: Long, otherGid: String?, otherToken: String?, )</ID> <ID>LongParameterList:FavoritesEntryRepositoryImpl.kt$FavoritesEntryRepositoryImpl$( gid: String, token: String, title: String, category: Long, otherGid: String?, otherToken: String?, )</ID>
@ -806,6 +806,7 @@
<ID>MagicNumber:EXHMigrations.kt$EXHMigrations$59</ID> <ID>MagicNumber:EXHMigrations.kt$EXHMigrations$59</ID>
<ID>MagicNumber:EXHMigrations.kt$EXHMigrations$6</ID> <ID>MagicNumber:EXHMigrations.kt$EXHMigrations$6</ID>
<ID>MagicNumber:EXHMigrations.kt$EXHMigrations$60</ID> <ID>MagicNumber:EXHMigrations.kt$EXHMigrations$60</ID>
<ID>MagicNumber:EXHMigrations.kt$EXHMigrations$66</ID>
<ID>MagicNumber:EXHMigrations.kt$EXHMigrations$6907</ID> <ID>MagicNumber:EXHMigrations.kt$EXHMigrations$6907</ID>
<ID>MagicNumber:EXHMigrations.kt$EXHMigrations$6907L</ID> <ID>MagicNumber:EXHMigrations.kt$EXHMigrations$6907L</ID>
<ID>MagicNumber:EXHMigrations.kt$EXHMigrations$6909L</ID> <ID>MagicNumber:EXHMigrations.kt$EXHMigrations$6909L</ID>
@ -1630,6 +1631,7 @@
<ID>NestedBlockDepth:SearchEngine.kt$SearchEngine$fun queryToSql(q: List&lt;QueryComponent&gt;): Pair&lt;String, List&lt;String&gt;&gt;</ID> <ID>NestedBlockDepth:SearchEngine.kt$SearchEngine$fun queryToSql(q: List&lt;QueryComponent&gt;): Pair&lt;String, List&lt;String&gt;&gt;</ID>
<ID>NestedBlockDepth:SmartSearchEngine.kt$SmartSearchEngine$private fun removeTextInBrackets(text: String, readForward: Boolean): String</ID> <ID>NestedBlockDepth:SmartSearchEngine.kt$SmartSearchEngine$private fun removeTextInBrackets(text: String, readForward: Boolean): String</ID>
<ID>NestedBlockDepth:SyncChaptersWithSource.kt$SyncChaptersWithSource$suspend fun await( rawSourceChapters: List&lt;SChapter&gt;, manga: Manga, source: Source, manualFetch: Boolean = false, fetchWindow: Pair&lt;Long, Long&gt; = Pair(0, 0), ): List&lt;Chapter&gt;</ID> <ID>NestedBlockDepth:SyncChaptersWithSource.kt$SyncChaptersWithSource$suspend fun await( rawSourceChapters: List&lt;SChapter&gt;, manga: Manga, source: Source, manualFetch: Boolean = false, fetchWindow: Pair&lt;Long, Long&gt; = Pair(0, 0), ): List&lt;Chapter&gt;</ID>
<ID>NestedBlockDepth:UniFileExtensions.kt$fun UniFile.unzip(destination: File, onlyCopyImages: Boolean = false)</ID>
<ID>NestedBlockDepth:UniFileTempFileManager.kt$UniFileTempFileManager$fun createTempFile(file: UniFile): File</ID> <ID>NestedBlockDepth:UniFileTempFileManager.kt$UniFileTempFileManager$fun createTempFile(file: UniFile): File</ID>
<ID>NestedBlockDepth:WebtoonRecyclerView.kt$WebtoonRecyclerView.Detector$override fun onTouchEvent(ev: MotionEvent): Boolean</ID> <ID>NestedBlockDepth:WebtoonRecyclerView.kt$WebtoonRecyclerView.Detector$override fun onTouchEvent(ev: MotionEvent): Boolean</ID>
<ID>NestedBlockDepth:WebtoonViewer.kt$WebtoonViewer$fun scrollDown()</ID> <ID>NestedBlockDepth:WebtoonViewer.kt$WebtoonViewer$fun scrollDown()</ID>
@ -2105,6 +2107,7 @@
<ID>TopLevelPropertyNaming:CbzCrypto.kt$private const val CRYPTO_SETTINGS = "$ALGORITHM/$BLOCK_MODE/$PADDING"</ID> <ID>TopLevelPropertyNaming:CbzCrypto.kt$private const val CRYPTO_SETTINGS = "$ALGORITHM/$BLOCK_MODE/$PADDING"</ID>
<ID>TopLevelPropertyNaming:CbzCrypto.kt$private const val IV_SIZE = 16</ID> <ID>TopLevelPropertyNaming:CbzCrypto.kt$private const val IV_SIZE = 16</ID>
<ID>TopLevelPropertyNaming:CbzCrypto.kt$private const val KEY_SIZE = 256</ID> <ID>TopLevelPropertyNaming:CbzCrypto.kt$private const val KEY_SIZE = 256</ID>
<ID>TopLevelPropertyNaming:CbzCrypto.kt$private const val SQL_PASSWORD_LENGTH = 32</ID>
<ID>TopLevelPropertyNaming:ChapterCache.kt$/** Application cache version. */ private const val PARAMETER_APP_VERSION = 1</ID> <ID>TopLevelPropertyNaming:ChapterCache.kt$/** Application cache version. */ private const val PARAMETER_APP_VERSION = 1</ID>
<ID>TopLevelPropertyNaming:ChapterCache.kt$/** The maximum number of bytes this cache should use to store. */ private const val PARAMETER_CACHE_SIZE = 100L * 1024 * 1024</ID> <ID>TopLevelPropertyNaming:ChapterCache.kt$/** The maximum number of bytes this cache should use to store. */ private const val PARAMETER_CACHE_SIZE = 100L * 1024 * 1024</ID>
<ID>TopLevelPropertyNaming:ChapterCache.kt$/** The number of values per cache entry. Must be positive. */ private const val PARAMETER_VALUE_COUNT = 1</ID> <ID>TopLevelPropertyNaming:ChapterCache.kt$/** The number of values per cache entry. Must be positive. */ private const val PARAMETER_VALUE_COUNT = 1</ID>

View File

@ -36,6 +36,7 @@ dependencies {
implementation(libs.image.decoder) implementation(libs.image.decoder)
implementation(libs.unifile) implementation(libs.unifile)
implementation(libs.bundles.archive)
api(kotlinx.coroutines.core) api(kotlinx.coroutines.core)
api(kotlinx.serialization.json) api(kotlinx.serialization.json)

View File

@ -3,25 +3,15 @@ package eu.kanade.tachiyomi.util.storage
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Base64 import android.util.Base64
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.core.security.SecurityPreferences import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import logcat.LogPriority
import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.exception.ZipException
import net.lingala.zip4j.io.inputstream.ZipInputStream
import net.lingala.zip4j.io.outputstream.ZipOutputStream
import net.lingala.zip4j.model.LocalFileHeader
import net.lingala.zip4j.model.ZipParameters import net.lingala.zip4j.model.ZipParameters
import net.lingala.zip4j.model.enums.AesKeyStrength import net.lingala.zip4j.model.enums.AesKeyStrength
import net.lingala.zip4j.model.enums.EncryptionMethod import net.lingala.zip4j.model.enums.EncryptionMethod
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
@ -136,12 +126,15 @@ object CbzCrypto {
} }
fun getDecryptedPasswordCbz(): CharArray { fun getDecryptedPasswordCbz(): CharArray {
return decrypt(securityPreferences.cbzPassword().get(), ALIAS_CBZ).toCharArray() val encryptedPassword = securityPreferences.cbzPassword().get()
if (encryptedPassword.isBlank()) error("This archive is encrypted please set a password")
return decrypt(encryptedPassword, ALIAS_CBZ).toCharArray()
} }
private fun generateAndEncryptSqlPw() { private fun generateAndEncryptSqlPw() {
val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9') val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
val password = (1..32).map { val password = (1..SQL_PASSWORD_LENGTH).map {
charPool[SecureRandom().nextInt(charPool.size)] charPool[SecureRandom().nextInt(charPool.size)]
}.joinToString("", transform = { it.toString() }) }.joinToString("", transform = { it.toString() })
securityPreferences.sqlPassword().set(encrypt(password, encryptionCipherSql)) securityPreferences.sqlPassword().set(encrypt(password, encryptionCipherSql))
@ -152,27 +145,6 @@ object CbzCrypto {
return decrypt(securityPreferences.sqlPassword().get(), ALIAS_SQL).toByteArray() return decrypt(securityPreferences.sqlPassword().get(), ALIAS_SQL).toByteArray()
} }
/**
* Function that returns true when the supplied password
* can Successfully decrypt the supplied zip archive
* not very elegant but this is the solution recommended by the maintainer for checking passwords
* a real password check will likely be implemented in the future though
*/
fun checkCbzPassword(zip4j: ZipFile, password: CharArray): Boolean {
try {
zip4j.setPassword(password)
zip4j.use { zip ->
zip.getInputStream(zip.fileHeaders.firstOrNull())
}
return true
} catch (e: Exception) {
logcat(LogPriority.WARN) {
"Wrong CBZ archive password for: ${zip4j.file.name} in: ${zip4j.file.parentFile?.name}"
}
}
return false
}
fun isPasswordSet(): Boolean { fun isPasswordSet(): Boolean {
return securityPreferences.cbzPassword().get().isNotEmpty() return securityPreferences.cbzPassword().get().isNotEmpty()
} }
@ -228,133 +200,6 @@ object CbzCrypto {
} }
return String(bytes).contains(DEFAULT_COVER_NAME, ignoreCase = true) return String(bytes).contains(DEFAULT_COVER_NAME, ignoreCase = true)
} }
fun UniFile.isEncryptedZip(): Boolean {
return try {
val stream = ZipInputStream(this.openInputStream())
stream.nextEntry
stream.close()
false
} catch (zipException: ZipException) {
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
true
} else throw zipException
}
}
fun UniFile.testCbzPassword(): Boolean {
return try {
val stream = ZipInputStream(this.openInputStream())
stream.setPassword(getDecryptedPasswordCbz())
stream.nextEntry
stream.close()
true
} catch (zipException: ZipException) {
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
false
} else throw zipException
}
}
fun UniFile.addStreamToZip(inputStream: InputStream, filename: String, password: CharArray? = null) {
val zipOutputStream =
if (password != null) ZipOutputStream(this.openOutputStream(), password)
else ZipOutputStream(this.openOutputStream())
val zipParameters = ZipParameters()
zipParameters.fileNameInZip = filename
if (password != null) setZipParametersEncrypted(zipParameters)
zipOutputStream.putNextEntry(zipParameters)
zipOutputStream.use { output ->
inputStream.use { input ->
input.copyTo(output)
}
}
}
fun UniFile.addFilesToZip(files: List<UniFile>, password: CharArray? = null) {
val zipOutputStream =
if (password != null) ZipOutputStream(this.openOutputStream(), password)
else ZipOutputStream(this.openOutputStream())
files.forEach {
val zipParameters = ZipParameters()
if (password != null) setZipParametersEncrypted(zipParameters)
zipParameters.fileNameInZip = it.name
zipOutputStream.putNextEntry(zipParameters)
it.openInputStream().use { input ->
input.copyTo(zipOutputStream)
}
zipOutputStream.closeEntry()
}
zipOutputStream.close()
}
fun UniFile.getZipInputStream(filename: String): InputStream? {
val zipInputStream = ZipInputStream(this.openInputStream())
var fileHeader: LocalFileHeader?
if (this.isEncryptedZip()) zipInputStream.setPassword(getDecryptedPasswordCbz())
try {
while (run {
fileHeader = zipInputStream.nextEntry
fileHeader != null
}) {
if (fileHeader?.fileName == filename) return zipInputStream
}
} catch (zipException: ZipException) {
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
logcat(LogPriority.WARN) {
"Wrong CBZ archive password for: ${this.name} in: ${this.parentFile?.name}"
}
} else throw zipException
}
return null
}
fun UniFile.getCoverStreamFromZip(): InputStream? {
val zipInputStream = ZipInputStream(this.openInputStream())
var fileHeader: LocalFileHeader?
val fileHeaderList: MutableList<LocalFileHeader?> = mutableListOf()
if (this.isEncryptedZip()) zipInputStream.setPassword(getDecryptedPasswordCbz())
try {
while (run {
fileHeader = zipInputStream.nextEntry
fileHeader != null
}) {
fileHeaderList.add(fileHeader)
}
var coverHeader = fileHeaderList
.mapNotNull { it }
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) }
val coverStream = coverHeader?.fileName?.let { this.getZipInputStream(it) }
if (coverStream != null) {
if (!ImageUtil.isImage(coverHeader?.fileName) { coverStream }) coverHeader = null
}
return coverHeader?.fileName?.let { getZipInputStream(it) }
} catch (zipException: ZipException) {
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
logcat(LogPriority.WARN) {
"Wrong CBZ archive password for: ${this.name} in: ${this.parentFile?.name}"
}
return null
} else throw zipException
}
}
} }
private const val BUFFER_SIZE = 2048 private const val BUFFER_SIZE = 2048
@ -369,4 +214,6 @@ private const val CRYPTO_SETTINGS = "$ALGORITHM/$BLOCK_MODE/$PADDING"
private const val KEYSTORE = "AndroidKeyStore" private const val KEYSTORE = "AndroidKeyStore"
private const val ALIAS_CBZ = "cbzPw" private const val ALIAS_CBZ = "cbzPw"
private const val ALIAS_SQL = "sqlPw" private const val ALIAS_SQL = "sqlPw"
private const val SQL_PASSWORD_LENGTH = 32
// SY <-- // SY <--

View File

@ -1,22 +1,36 @@
package eu.kanade.tachiyomi.util.storage package eu.kanade.tachiyomi.util.storage
import android.content.Context
import android.os.Build
import com.hippo.unifile.UniFile
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
import org.apache.commons.compress.archivers.zip.ZipFile
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import tachiyomi.core.common.storage.UniFileTempFileManager
import tachiyomi.core.common.storage.openReadOnlyChannel
import uy.kohesive.injekt.injectLazy
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
/** /**
* Wrapper over ZipFile to load files in epub format. * Wrapper over ZipFile to load files in epub format.
*/ */
class EpubFile(file: File) : Closeable { // SY -->
class EpubFile(file: UniFile, context: Context) : Closeable {
private val tempFileManager: UniFileTempFileManager by injectLazy()
/** /**
* Zip file of this epub. * Zip file of this epub.
*/ */
private val zip = ZipFile(file) private val zip = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
ZipFile(tempFileManager.createTempFile(file))
} else {
ZipFile(file.openReadOnlyChannel(context))
}
// SY <--
/** /**
* Path separator used by this epub. * Path separator used by this epub.
@ -33,14 +47,14 @@ class EpubFile(file: File) : Closeable {
/** /**
* Returns an input stream for reading the contents of the specified zip file entry. * Returns an input stream for reading the contents of the specified zip file entry.
*/ */
fun getInputStream(entry: ZipEntry): InputStream { fun getInputStream(entry: ZipArchiveEntry): InputStream {
return zip.getInputStream(entry) return zip.getInputStream(entry)
} }
/** /**
* Returns the zip file entry for the specified name, or null if not found. * Returns the zip file entry for the specified name, or null if not found.
*/ */
fun getEntry(name: String): ZipEntry? { fun getEntry(name: String): ZipArchiveEntry? {
return zip.getEntry(name) return zip.getEntry(name)
} }

View File

@ -1,6 +1,21 @@
package tachiyomi.core.common.storage package tachiyomi.core.common.storage
import android.content.Context
import android.os.ParcelFileDescriptor
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.CbzCrypto
import logcat.LogPriority
import net.lingala.zip4j.exception.ZipException
import net.lingala.zip4j.io.inputstream.ZipInputStream
import net.lingala.zip4j.io.outputstream.ZipOutputStream
import net.lingala.zip4j.model.LocalFileHeader
import net.lingala.zip4j.model.ZipParameters
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat
import java.io.File
import java.io.InputStream
import java.nio.channels.FileChannel
val UniFile.extension: String? val UniFile.extension: String?
get() = name?.substringAfterLast('.') get() = name?.substringAfterLast('.')
@ -10,3 +25,201 @@ val UniFile.nameWithoutExtension: String?
val UniFile.displayablePath: String val UniFile.displayablePath: String
get() = filePath ?: uri.toString() get() = filePath ?: uri.toString()
fun UniFile.openReadOnlyChannel(context: Context): FileChannel {
return ParcelFileDescriptor.AutoCloseInputStream(context.contentResolver.openFileDescriptor(uri, "r")).channel
// SY -->
}
fun UniFile.isEncryptedZip(): Boolean {
return try {
val stream = ZipInputStream(this.openInputStream())
stream.nextEntry
stream.close()
false
} catch (zipException: ZipException) {
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
true
} else {
throw zipException
}
}
}
fun UniFile.testCbzPassword(): Boolean {
return try {
val stream = ZipInputStream(this.openInputStream())
stream.setPassword(CbzCrypto.getDecryptedPasswordCbz())
stream.nextEntry
stream.close()
true
} catch (zipException: ZipException) {
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
false
} else {
throw zipException
}
}
}
fun UniFile.addStreamToZip(inputStream: InputStream, filename: String, password: CharArray? = null) {
val zipOutputStream =
if (password != null) {
ZipOutputStream(this.openOutputStream(), password)
} else {
ZipOutputStream(this.openOutputStream())
}
val zipParameters = ZipParameters()
zipParameters.fileNameInZip = filename
if (password != null) CbzCrypto.setZipParametersEncrypted(zipParameters)
zipOutputStream.putNextEntry(zipParameters)
zipOutputStream.use { output ->
inputStream.use { input ->
input.copyTo(output)
}
}
}
/**
* Unzips encrypted or unencrypted zip files using zip4j.
* The caller is responsible to ensure, that the file this is called from is a zip archive
*/
fun UniFile.unzip(destination: File, onlyCopyImages: Boolean = false) {
destination.mkdirs()
if (!destination.isDirectory) return
val zipInputStream = ZipInputStream(this.openInputStream())
var fileHeader: LocalFileHeader?
if (this.isEncryptedZip()) {
zipInputStream.setPassword(CbzCrypto.getDecryptedPasswordCbz())
}
try {
while (
run {
fileHeader = zipInputStream.nextEntry
fileHeader != null
}
) {
val tmpFile = File("${destination.absolutePath}/${fileHeader!!.fileName}")
if (onlyCopyImages) {
if (!fileHeader!!.isDirectory && ImageUtil.isImage(fileHeader!!.fileName)) {
tmpFile.createNewFile()
tmpFile.outputStream().buffered().use { tmpOut ->
zipInputStream.buffered().copyTo(tmpOut)
}
}
} else {
if (!fileHeader!!.isDirectory && ImageUtil.isImage(fileHeader!!.fileName)) {
tmpFile.createNewFile()
tmpFile
.outputStream()
.buffered()
.use { zipInputStream.buffered().copyTo(it) }
}
}
}
zipInputStream.close()
} catch (zipException: ZipException) {
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
logcat(LogPriority.WARN) {
"Wrong CBZ archive password for: ${this.name} in: ${this.parentFile?.name}"
}
} else {
throw zipException
}
}
}
fun UniFile.addFilesToZip(files: List<UniFile>, password: CharArray? = null) {
val zipOutputStream =
if (password != null) {
ZipOutputStream(this.openOutputStream(), password)
} else {
ZipOutputStream(this.openOutputStream())
}
files.forEach {
val zipParameters = ZipParameters()
if (password != null) CbzCrypto.setZipParametersEncrypted(zipParameters)
zipParameters.fileNameInZip = it.name
zipOutputStream.putNextEntry(zipParameters)
it.openInputStream().use { input ->
input.copyTo(zipOutputStream)
}
zipOutputStream.closeEntry()
}
zipOutputStream.close()
}
fun UniFile.getZipInputStream(filename: String): InputStream? {
val zipInputStream = ZipInputStream(this.openInputStream())
var fileHeader: LocalFileHeader?
if (this.isEncryptedZip()) zipInputStream.setPassword(CbzCrypto.getDecryptedPasswordCbz())
try {
while (
run {
fileHeader = zipInputStream.nextEntry
fileHeader != null
}
) {
if (fileHeader?.fileName == filename) return zipInputStream
}
} catch (zipException: ZipException) {
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
logcat(LogPriority.WARN) {
"Wrong CBZ archive password for: ${this.name} in: ${this.parentFile?.name}"
}
} else {
throw zipException
}
}
return null
}
fun UniFile.getCoverStreamFromZip(): InputStream? {
val zipInputStream = ZipInputStream(this.openInputStream())
var fileHeader: LocalFileHeader?
val fileHeaderList: MutableList<LocalFileHeader?> = mutableListOf()
if (this.isEncryptedZip()) zipInputStream.setPassword(CbzCrypto.getDecryptedPasswordCbz())
try {
while (
run {
fileHeader = zipInputStream.nextEntry
fileHeader != null
}
) {
fileHeaderList.add(fileHeader)
}
var coverHeader = fileHeaderList
.mapNotNull { it }
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) }
val coverStream = coverHeader?.fileName?.let { this.getZipInputStream(it) }
if (coverStream != null) {
if (!ImageUtil.isImage(coverHeader?.fileName) { coverStream }) coverHeader = null
}
return coverHeader?.fileName?.let { getZipInputStream(it) }
} catch (zipException: ZipException) {
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
logcat(LogPriority.WARN) {
"Wrong CBZ archive password for: ${this.name} in: ${this.parentFile?.name}"
}
return null
} else {
throw zipException
}
}
}
// SY <--

View File

@ -26,8 +26,6 @@ import androidx.core.graphics.red
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import logcat.LogPriority import logcat.LogPriority
import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.model.FileHeader
import tachiyomi.decoder.Format import tachiyomi.decoder.Format
import tachiyomi.decoder.ImageDecoder import tachiyomi.decoder.ImageDecoder
import java.io.BufferedInputStream import java.io.BufferedInputStream
@ -133,20 +131,8 @@ object ImageUtil {
* *
* @return true if the width is greater than the height * @return true if the width is greater than the height
*/ */
fun isWideImage( fun isWideImage(imageStream: BufferedInputStream): Boolean {
imageStream: BufferedInputStream, val options = extractImageOptions(imageStream)
// SY -->
zip4jFile: ZipFile?,
zip4jEntry: FileHeader?,
// SY <--
): Boolean {
val options = extractImageOptions(
imageStream,
// SY -->
zip4jFile,
zip4jEntry,
// SY <--
)
return options.outWidth > options.outHeight return options.outWidth > options.outHeight
} }
@ -271,19 +257,9 @@ object ImageUtil {
* *
* @return true if the height:width ratio is greater than 3. * @return true if the height:width ratio is greater than 3.
*/ */
private fun isTallImage( private fun isTallImage(imageStream: InputStream): Boolean {
imageStream: InputStream,
// SY -->
zip4jFile: ZipFile?,
zip4jEntry: FileHeader?,
// SY <--
): Boolean {
val options = extractImageOptions( val options = extractImageOptions(
imageStream, imageStream,
// SY -->
zip4jFile,
zip4jEntry,
// SY <--
resetAfterExtraction = false, resetAfterExtraction = false,
) )
@ -297,18 +273,9 @@ object ImageUtil {
tmpDir: UniFile, tmpDir: UniFile,
imageFile: UniFile, imageFile: UniFile,
filenamePrefix: String, filenamePrefix: String,
// SY -->
zip4jFile: ZipFile?,
zip4jEntry: FileHeader?,
// SY <--
): Boolean { ): Boolean {
if (isAnimatedAndSupported(imageFile.openInputStream()) || !isTallImage( if (isAnimatedAndSupported(imageFile.openInputStream()) ||
imageFile.openInputStream(), !isTallImage(imageFile.openInputStream())
// SY -->
zip4jFile,
zip4jEntry,
// SY <--
)
) { ) {
return true return true
} }
@ -321,10 +288,6 @@ object ImageUtil {
val options = extractImageOptions( val options = extractImageOptions(
imageFile.openInputStream(), imageFile.openInputStream(),
// SY -->
zip4jFile,
zip4jEntry,
// SY <--
resetAfterExtraction = false, resetAfterExtraction = false,
).apply { ).apply {
inJustDecodeBounds = false inJustDecodeBounds = false
@ -641,17 +604,8 @@ object ImageUtil {
*/ */
private fun extractImageOptions( private fun extractImageOptions(
imageStream: InputStream, imageStream: InputStream,
// SY -->
zip4jFile: ZipFile?,
zip4jEntry: FileHeader?,
// SY <--
resetAfterExtraction: Boolean = true, resetAfterExtraction: Boolean = true,
): BitmapFactory.Options { ): BitmapFactory.Options {
// SY -->
// zip4j does currently not support mark() and reset()
if (zip4jFile != null && zip4jEntry != null) return extractImageOptionsZip4j(zip4jFile, zip4jEntry)
// SY <--
imageStream.mark(Int.MAX_VALUE) imageStream.mark(Int.MAX_VALUE)
val imageBytes = imageStream.readBytes() val imageBytes = imageStream.readBytes()
@ -661,16 +615,6 @@ object ImageUtil {
return options return options
} }
// SY -->
private fun extractImageOptionsZip4j(zip4jFile: ZipFile?, zip4jEntry: FileHeader?): BitmapFactory.Options {
zip4jFile?.getInputStream(zip4jEntry).use { imageStream ->
val imageBytes = imageStream?.readBytes()
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
imageBytes?.size?.let { BitmapFactory.decodeByteArray(imageBytes, 0, it, options) }
return options
}
}
/** /**
* Creates random exif metadata used as padding to make * Creates random exif metadata used as padding to make
* the size of files inside CBZ archives unique * the size of files inside CBZ archives unique

View File

@ -33,6 +33,7 @@ jsoup = "org.jsoup:jsoup:1.17.2"
disklrucache = "com.jakewharton:disklrucache:2.0.2" disklrucache = "com.jakewharton:disklrucache:2.0.2"
unifile = "com.github.tachiyomiorg:unifile:7c257e1c64" unifile = "com.github.tachiyomiorg:unifile:7c257e1c64"
common-compress = "org.apache.commons:commons-compress:1.26.0"
junrar = "com.github.junrar:junrar:7.5.5" junrar = "com.github.junrar:junrar:7.5.5"
zip4j = "net.lingala.zip4j:zip4j:2.11.5" zip4j = "net.lingala.zip4j:zip4j:2.11.5"
@ -111,6 +112,7 @@ google-api-client-oauth = "com.google.oauth-client:google-oauth-client:1.34.1"
[bundles] [bundles]
acra = ["acra-http", "acra-scheduler"] acra = ["acra-http", "acra-scheduler"]
archive = ["common-compress", "junrar"]
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"] okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"]
js-engine = ["quickjs-android"] js-engine = ["quickjs-android"]
sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"] sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"]

View File

@ -324,9 +324,13 @@
<string name="pref_center_margin">Center margin type</string> <string name="pref_center_margin">Center margin type</string>
<string name="pref_center_margin_summary">Insert spacer to accommodate deadspace on foldable devices.</string> <string name="pref_center_margin_summary">Insert spacer to accommodate deadspace on foldable devices.</string>
<!-- Cache archived manga to disk --> <!-- Archive reader mode -->
<string name="cache_archived_manga_to_disk">Cache images inside CBZ/CBR archives on disk</string> <string name="archive_mode_load_from_file">Load from file</string>
<string name="cache_archived_manga_to_disk_subtitle">Temporarily copy images inside comic book archives to disk while reading \nMay improve reader performance</string> <string name="archive_mode_load_into_memory">Load into memory</string>
<string name="archive_mode_cache_to_disk">Copy to disk</string>
<string name="pref_archive_reader_mode">Archive reader mode</string>
<string name="pref_archive_reader_mode_summary">The way in which images inside archives, such as CBZ or CBR, are being loaded</string>
<!-- Entry Page --> <!-- Entry Page -->
<!-- Entry Info --> <!-- Entry Info -->

View File

@ -15,7 +15,7 @@ kotlin {
// SY <-- // SY <--
implementation(libs.unifile) implementation(libs.unifile)
implementation(libs.junrar) implementation(libs.bundles.archive)
// SY --> // SY -->
implementation(libs.zip4j) implementation(libs.zip4j)
// SY <-- // SY <--

View File

@ -1,6 +1,7 @@
package tachiyomi.source.local package tachiyomi.source.local
import android.content.Context import android.content.Context
import android.os.Build
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
@ -11,10 +12,6 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.CbzCrypto import eu.kanade.tachiyomi.util.storage.CbzCrypto
import eu.kanade.tachiyomi.util.storage.CbzCrypto.addStreamToZip
import eu.kanade.tachiyomi.util.storage.CbzCrypto.getCoverStreamFromZip
import eu.kanade.tachiyomi.util.storage.CbzCrypto.getZipInputStream
import eu.kanade.tachiyomi.util.storage.CbzCrypto.isEncryptedZip
import eu.kanade.tachiyomi.util.storage.EpubFile import eu.kanade.tachiyomi.util.storage.EpubFile
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
@ -31,7 +28,11 @@ import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
import tachiyomi.core.metadata.comicinfo.getComicInfo import tachiyomi.core.metadata.comicinfo.getComicInfo
import tachiyomi.core.metadata.tachiyomi.MangaDetails import tachiyomi.core.metadata.tachiyomi.MangaDetails
import tachiyomi.core.common.storage.UniFileTempFileManager import tachiyomi.core.common.storage.UniFileTempFileManager
import tachiyomi.core.common.storage.addStreamToZip
import tachiyomi.core.common.storage.extension import tachiyomi.core.common.storage.extension
import tachiyomi.core.common.storage.getCoverStreamFromZip
import tachiyomi.core.common.storage.getZipInputStream
import tachiyomi.core.common.storage.isEncryptedZip
import tachiyomi.core.common.storage.nameWithoutExtension import tachiyomi.core.common.storage.nameWithoutExtension
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.ImageUtil
@ -268,7 +269,13 @@ actual class LocalSource(
} }
} }
is Format.Rar -> { is Format.Rar -> {
JunrarArchive(tempFileManager.createTempFile(chapter)).use { rar -> val archive = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
JunrarArchive(tempFileManager.createTempFile(chapter))
} else {
JunrarArchive(chapter.openInputStream())
}
archive.use { rar ->
rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile -> rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
rar.getInputStream(comicInfoFile).buffered().use { stream -> rar.getInputStream(comicInfoFile).buffered().use { stream ->
return copyComicInfoFile(stream, folder) return copyComicInfoFile(stream, folder)
@ -330,7 +337,7 @@ actual class LocalSource(
val format = Format.valueOf(chapterFile) val format = Format.valueOf(chapterFile)
if (format is Format.Epub) { if (format is Format.Epub) {
EpubFile(tempFileManager.createTempFile(format.file)).use { epub -> EpubFile(format.file, context).use { epub ->
epub.fillMetadata(manga, this) epub.fillMetadata(manga, this)
} }
} }
@ -397,10 +404,15 @@ actual class LocalSource(
format.file.isEncryptedZip() format.file.isEncryptedZip()
) )
} }
// SY <--
} }
is Format.Rar -> { is Format.Rar -> {
JunrarArchive(tempFileManager.createTempFile(format.file)).use { archive -> val rarArchive = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
JunrarArchive(tempFileManager.createTempFile(format.file))
} else {
JunrarArchive(format.file.openInputStream())
}
rarArchive.use { archive ->
// SY <--
val entry = archive.fileHeaders val entry = archive.fileHeaders
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } } .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
@ -409,15 +421,17 @@ actual class LocalSource(
} }
} }
is Format.Epub -> { is Format.Epub -> {
EpubFile(tempFileManager.createTempFile(format.file)).use { epub -> // SY -->
val entry = epub.getImagesFromPages() EpubFile(format.file, context).use { epub ->
.firstOrNull() // SY <--
?.let { epub.getEntry(it) } val entry = epub.getImagesFromPages()
.firstOrNull()
?.let { epub.getEntry(it) }
entry?.let { coverManager.update(manga, epub.getInputStream(it)) } entry?.let { coverManager.update(manga, epub.getInputStream(it)) }
}
} }
} }
}
} catch (e: Throwable) { } catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Error updating cover for ${manga.title}" } logcat(LogPriority.ERROR, e) { "Error updating cover for ${manga.title}" }
null null

View File

@ -4,7 +4,7 @@ import android.content.Context
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.storage.CbzCrypto import eu.kanade.tachiyomi.util.storage.CbzCrypto
import eu.kanade.tachiyomi.util.storage.CbzCrypto.addStreamToZip import tachiyomi.core.common.storage.addStreamToZip
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import tachiyomi.core.common.storage.nameWithoutExtension import tachiyomi.core.common.storage.nameWithoutExtension
import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.ImageUtil