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:
parent
45711cd394
commit
6719f22eff
@ -215,7 +215,7 @@ dependencies {
|
||||
// Disk
|
||||
implementation(libs.disklrucache)
|
||||
implementation(libs.unifile)
|
||||
implementation(libs.junrar)
|
||||
implementation(libs.bundles.archive)
|
||||
// SY -->
|
||||
implementation(libs.zip4j)
|
||||
// SY <--
|
||||
|
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
@ -122,6 +122,9 @@
|
||||
# XmlUtil
|
||||
-keep public enum nl.adaptivity.xmlutil.EventType { *; }
|
||||
|
||||
# Apache Commons Compress
|
||||
-keep class * extends org.apache.commons.compress.archivers.zip.ZipExtraField { <init>(); }
|
||||
|
||||
# Firebase
|
||||
-keep class com.google.firebase.installations.** { *; }
|
||||
-keep interface com.google.firebase.installations.** { *; }
|
||||
|
@ -570,10 +570,14 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.cacheArchiveMangaOnDisk(),
|
||||
title = stringResource(SYMR.strings.cache_archived_manga_to_disk),
|
||||
subtitle = stringResource(SYMR.strings.cache_archived_manga_to_disk_subtitle),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = readerPreferences.archiveReaderMode(),
|
||||
title = stringResource(SYMR.strings.pref_archive_reader_mode),
|
||||
subtitle = stringResource(SYMR.strings.pref_archive_reader_mode_summary),
|
||||
entries = ReaderPreferences.archiveModeTypes
|
||||
.mapIndexed { index, it -> index to stringResource(it) }
|
||||
.toMap()
|
||||
.toImmutableMap(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
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.NOMEDIA_FILE
|
||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||
@ -46,6 +45,7 @@ import logcat.LogPriority
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import okhttp3.Response
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.storage.addFilesToZip
|
||||
import tachiyomi.core.common.storage.extension
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.launchNow
|
||||
@ -572,10 +572,6 @@ class Downloader(
|
||||
tmpDir,
|
||||
imageFile,
|
||||
filenamePrefix,
|
||||
// SY -->
|
||||
zip4jFile = null,
|
||||
zip4jEntry = null,
|
||||
// SY <--
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to split downloaded image" }
|
||||
|
@ -389,7 +389,6 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
context = context,
|
||||
downloadManager = downloadManager,
|
||||
downloadProvider = downloadProvider,
|
||||
tempFileManager = tempFileManager,
|
||||
manga = manga,
|
||||
source = source, /* SY --> */
|
||||
sourceManager = sourceManager,
|
||||
|
@ -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.setting.ReaderPreferences
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.storage.UniFileTempFileManager
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
@ -28,7 +27,6 @@ class 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 -->
|
||||
@ -121,36 +119,39 @@ class ChapterLoader(
|
||||
source = source,
|
||||
downloadManager = downloadManager,
|
||||
downloadProvider = downloadProvider,
|
||||
tempFileManager = tempFileManager,
|
||||
)
|
||||
source is HttpSource -> HttpPageLoader(chapter, source)
|
||||
source is LocalSource -> source.getFormat(chapter.chapter).let { format ->
|
||||
when (format) {
|
||||
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 {
|
||||
RarPageLoader(tempFileManager.createTempFile(format.file))
|
||||
RarPageLoader(format.file)
|
||||
} catch (e: UnsupportedRarV5Exception) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
// 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 ->
|
||||
when (format) {
|
||||
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 {
|
||||
RarPageLoader(tempFileManager.createTempFile(format.file))
|
||||
RarPageLoader(format.file)
|
||||
// SY <--
|
||||
} catch (e: UnsupportedRarV5Exception) {
|
||||
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)
|
||||
|
@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import tachiyomi.core.common.storage.UniFileTempFileManager
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
@ -23,7 +22,6 @@ internal class DownloadPageLoader(
|
||||
private val source: Source,
|
||||
private val downloadManager: DownloadManager,
|
||||
private val downloadProvider: DownloadProvider,
|
||||
private val tempFileManager: UniFileTempFileManager,
|
||||
) : PageLoader() {
|
||||
|
||||
private val context: Application by injectLazy()
|
||||
@ -48,7 +46,9 @@ internal class DownloadPageLoader(
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -1,16 +1,19 @@
|
||||
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.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() {
|
||||
// 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
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.loader
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Build
|
||||
import com.github.junrar.Archive
|
||||
import com.github.junrar.rarfile.FileHeader
|
||||
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.setting.ReaderPreferences
|
||||
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 uy.kohesive.injekt.injectLazy
|
||||
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.
|
||||
*/
|
||||
internal class RarPageLoader(file: File) : PageLoader() {
|
||||
|
||||
private val rar = Archive(file)
|
||||
internal class RarPageLoader(file: UniFile) : PageLoader() {
|
||||
|
||||
// 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 readerPreferences: ReaderPreferences by injectLazy()
|
||||
private val tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also {
|
||||
@ -31,17 +46,18 @@ internal class RarPageLoader(file: File) : PageLoader() {
|
||||
}
|
||||
|
||||
init {
|
||||
if (readerPreferences.cacheArchiveMangaOnDisk().get()) {
|
||||
if (readerPreferences.archiveReaderMode().get() == ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK) {
|
||||
tmpDir.mkdirs()
|
||||
Archive(file).use { rar ->
|
||||
rar.fileHeaders.asSequence()
|
||||
.filterNot { it.isDirectory }
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } }
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.forEach { header ->
|
||||
val pageOutputStream = File(tmpDir, header.fileName.substringAfterLast("/"))
|
||||
File(tmpDir, header.fileName.substringAfterLast("/"))
|
||||
.also { it.createNewFile() }
|
||||
.outputStream()
|
||||
getStream(header).use {
|
||||
it.copyTo(pageOutputStream)
|
||||
.use { output ->
|
||||
rar.getInputStream(header).use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -58,16 +74,37 @@ internal class RarPageLoader(file: File) : PageLoader() {
|
||||
|
||||
override suspend fun getPages(): List<ReaderPage> {
|
||||
// SY -->
|
||||
if (readerPreferences.cacheArchiveMangaOnDisk().get()) {
|
||||
if (readerPreferences.archiveReaderMode().get() == ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK) {
|
||||
return DirectoryPageLoader(UniFile.fromFile(tmpDir)!!).getPages()
|
||||
}
|
||||
val mutex = Mutex()
|
||||
// SY <--
|
||||
return rar.fileHeaders.asSequence()
|
||||
.filter { !it.isDirectory && ImageUtil.isImage(it.fileName) { rar.getInputStream(it) } }
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.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 {
|
||||
stream = { getStream(header) }
|
||||
// SY -->
|
||||
stream = { imageBytes?.copyOf()?.inputStream() ?: getStream(header) }
|
||||
// SY <--
|
||||
status = Page.State.READY
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.loader
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
@ -8,106 +8,154 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
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.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.i18n.sy.SYMR
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.zip.ZipFile
|
||||
import java.nio.channels.SeekableByteChannel
|
||||
import net.lingala.zip4j.ZipFile as Zip4jFile
|
||||
|
||||
/**
|
||||
* 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 -->
|
||||
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 tmpDir = File(context.externalCacheDir, "reader_${file.hashCode()}").also {
|
||||
it.deleteRecursively()
|
||||
}
|
||||
private val zip4j: Zip4jFile = Zip4jFile(file)
|
||||
private val zip: ZipFile? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
if (!zip4j.isEncrypted) ZipFile(file, StandardCharsets.ISO_8859_1) else null
|
||||
|
||||
private val apacheZip: ZipFile? = if (!file.isEncryptedZip() && Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
|
||||
ZipFile(channel)
|
||||
} else {
|
||||
if (!zip4j.isEncrypted) ZipFile(file) else null
|
||||
null
|
||||
}
|
||||
|
||||
private val tmpFile =
|
||||
if (
|
||||
apacheZip == null &&
|
||||
readerPreferences.archiveReaderMode().get() != ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK
|
||||
) {
|
||||
tempFileManager.createTempFile(file)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
private val zip4j =
|
||||
if (apacheZip == null && tmpFile != null) {
|
||||
Zip4jFile(tmpFile)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
init {
|
||||
Zip4jFile(file).use { zip ->
|
||||
if (zip.isEncrypted) {
|
||||
if (!CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())) {
|
||||
if (file.isEncryptedZip()) {
|
||||
if (!file.testCbzPassword()) {
|
||||
this.recycle()
|
||||
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 <--
|
||||
override fun recycle() {
|
||||
super.recycle()
|
||||
zip?.close()
|
||||
apacheZip?.close()
|
||||
// SY -->
|
||||
zip4j.close()
|
||||
zip4j?.close()
|
||||
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 suspend fun getPages(): List<ReaderPage> {
|
||||
if (readerPreferences.cacheArchiveMangaOnDisk().get()) {
|
||||
if (readerPreferences.archiveReaderMode().get() == ReaderPreferences.ArchiveReaderMode.CACHE_TO_DISK) {
|
||||
return DirectoryPageLoader(UniFile.fromFile(tmpDir)!!).getPages()
|
||||
}
|
||||
|
||||
if (zip == null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
zip4j.charset = StandardCharsets.ISO_8859_1
|
||||
return if (apacheZip == null) {
|
||||
loadZip4j()
|
||||
} else {
|
||||
loadApacheZip(apacheZip)
|
||||
}
|
||||
}
|
||||
|
||||
return zip4j.fileHeaders.asSequence()
|
||||
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 = { zip4j.getInputStream(entry) }
|
||||
stream = { imageBytes?.copyOf()?.inputStream() ?: zip4j.getInputStream(entry) }
|
||||
status = Page.State.READY
|
||||
zip4jFile = zip4j
|
||||
zip4jEntry = entry
|
||||
}
|
||||
}.toList()
|
||||
} else {
|
||||
// SY <--
|
||||
return zip.entries().asSequence()
|
||||
}
|
||||
|
||||
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 = { zip.getInputStream(entry) }
|
||||
stream = { imageBytes?.copyOf()?.inputStream() ?: zip.getInputStream(entry) }
|
||||
status = Page.State.READY
|
||||
}
|
||||
}.toList()
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* No additional action required to load the page
|
||||
|
@ -1,8 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.model
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import net.lingala.zip4j.ZipFile
|
||||
import net.lingala.zip4j.model.FileHeader
|
||||
import java.io.InputStream
|
||||
|
||||
open class ReaderPage(
|
||||
@ -10,9 +8,6 @@ open class ReaderPage(
|
||||
url: String = "",
|
||||
imageUrl: String? = null,
|
||||
// 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 */
|
||||
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 */
|
||||
|
@ -180,7 +180,7 @@ class ReaderPreferences(
|
||||
|
||||
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)
|
||||
// SY <--
|
||||
@ -203,6 +203,12 @@ class ReaderPreferences(
|
||||
LOWEST(47),
|
||||
}
|
||||
|
||||
object ArchiveReaderMode {
|
||||
const val LOAD_FROM_FILE = 0
|
||||
const val LOAD_INTO_MEMORY = 1
|
||||
const val CACHE_TO_DISK = 2
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val WEBTOON_PADDING_MIN = 0
|
||||
const val WEBTOON_PADDING_MAX = 25
|
||||
@ -264,6 +270,12 @@ class ReaderPreferences(
|
||||
SYMR.strings.center_margin_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 <--
|
||||
}
|
||||
}
|
||||
|
@ -230,13 +230,7 @@ class PagerPageHolder(
|
||||
return splitInHalf(imageStream)
|
||||
}
|
||||
|
||||
val isDoublePage = ImageUtil.isWideImage(
|
||||
imageStream,
|
||||
// SY -->
|
||||
page.zip4jFile,
|
||||
page.zip4jEntry,
|
||||
// SY <--
|
||||
)
|
||||
val isDoublePage = ImageUtil.isWideImage(imageStream)
|
||||
if (!isDoublePage) {
|
||||
return imageStream
|
||||
}
|
||||
@ -247,13 +241,7 @@ class PagerPageHolder(
|
||||
}
|
||||
|
||||
private fun rotateDualPage(imageStream: BufferedInputStream): InputStream {
|
||||
val isDoublePage = ImageUtil.isWideImage(
|
||||
imageStream,
|
||||
// SY -->
|
||||
page.zip4jFile,
|
||||
page.zip4jEntry,
|
||||
// SY <--
|
||||
)
|
||||
val isDoublePage = ImageUtil.isWideImage(imageStream)
|
||||
return if (isDoublePage) {
|
||||
val rotation = if (viewer.config.dualPageRotateToFitInvert) -90f else 90f
|
||||
ImageUtil.rotateImage(imageStream, rotation)
|
||||
@ -267,13 +255,7 @@ class PagerPageHolder(
|
||||
if (imageStream2 == null) {
|
||||
return if (imageStream is BufferedInputStream &&
|
||||
!ImageUtil.isAnimatedAndSupported(imageStream) &&
|
||||
ImageUtil.isWideImage(
|
||||
imageStream,
|
||||
// SY -->
|
||||
page.zip4jFile,
|
||||
page.zip4jEntry,
|
||||
// SY <--
|
||||
) &&
|
||||
ImageUtil.isWideImage(imageStream) &&
|
||||
viewer.config.centerMarginType and PagerConfig.CenterMarginType.WIDE_PAGE_CENTER_MARGIN > 0 &&
|
||||
!viewer.config.imageCropBorders
|
||||
) {
|
||||
|
@ -224,13 +224,7 @@ class WebtoonPageHolder(
|
||||
}
|
||||
|
||||
if (viewer.config.dualPageSplit) {
|
||||
val isDoublePage = ImageUtil.isWideImage(
|
||||
imageStream,
|
||||
// SY -->
|
||||
page?.zip4jFile,
|
||||
page?.zip4jEntry,
|
||||
// SY <--
|
||||
)
|
||||
val isDoublePage = ImageUtil.isWideImage(imageStream)
|
||||
if (isDoublePage) {
|
||||
val upperSide = if (viewer.config.dualPageInvert) ImageUtil.Side.LEFT else ImageUtil.Side.RIGHT
|
||||
return ImageUtil.splitAndMerge(imageStream, upperSide)
|
||||
@ -241,13 +235,7 @@ class WebtoonPageHolder(
|
||||
}
|
||||
|
||||
private fun rotateDualPage(imageStream: BufferedInputStream): InputStream {
|
||||
val isDoublePage = ImageUtil.isWideImage(
|
||||
imageStream,
|
||||
// SY -->
|
||||
page?.zip4jFile,
|
||||
page?.zip4jEntry,
|
||||
// SY <--
|
||||
)
|
||||
val isDoublePage = ImageUtil.isWideImage(imageStream)
|
||||
return if (isDoublePage) {
|
||||
val rotation = if (viewer.config.dualPageRotateToFitInvert) -90f else 90f
|
||||
ImageUtil.rotateImage(imageStream, rotation)
|
||||
|
@ -656,6 +656,12 @@ object EXHMigrations {
|
||||
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 (prefs.getBoolean(Preference.privateKey("encrypt_database"), false)) {
|
||||
|
@ -187,7 +187,6 @@ class InterceptActivity : BaseActivity() {
|
||||
lifecycleScope.launchIO {
|
||||
loadGalleryEnd(gallery, sources[index])
|
||||
}
|
||||
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
|
@ -172,7 +172,7 @@
|
||||
<ID>ComplexCondition:LibraryUpdateJob.kt$LibraryUpdateJob$group == LibraryGroup.BY_DEFAULT || groupLibraryUpdateType == GroupLibraryMode.GLOBAL || (groupLibraryUpdateType == GroupLibraryMode.ALL_BUT_UNGROUPED && 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:MangaScreenModel.kt$MangaScreenModel$(selectedItem.selected && selected) || (!selectedItem.selected && !selected)</ID>
|
||||
<ID>ComplexCondition:PagerPageHolder.kt$PagerPageHolder$imageStream is BufferedInputStream && !ImageUtil.isAnimatedAndSupported(imageStream) && ImageUtil.isWideImage( imageStream, // SY --> page.zip4jFile, page.zip4jEntry, // SY <-- ) && viewer.config.centerMarginType and PagerConfig.CenterMarginType.WIDE_PAGE_CENTER_MARGIN > 0 && !viewer.config.imageCropBorders</ID>
|
||||
<ID>ComplexCondition:PagerPageHolder.kt$PagerPageHolder$imageStream is BufferedInputStream && !ImageUtil.isAnimatedAndSupported(imageStream) && ImageUtil.isWideImage(imageStream) && viewer.config.centerMarginType and PagerConfig.CenterMarginType.WIDE_PAGE_CENTER_MARGIN > 0 && !viewer.config.imageCropBorders</ID>
|
||||
<ID>ComplexCondition:PagerViewerAdapter.kt$PagerViewerAdapter$items[itemIndex]?.fullPage == true && itemIndex > 0 && items[itemIndex - 1] != null && (itemIndex - 1) % 2 == 0</ID>
|
||||
<ID>ComplexCondition:ReaderActivity.kt$ReaderActivity$readerPreferences.useAutoWebtoon().get() && (manga?.readingMode?.toInt() ?: ReadingMode.DEFAULT.flagValue) == ReadingMode.DEFAULT.flagValue && defaultReaderType != null && defaultReaderType == ReadingMode.WEBTOON.flagValue</ID>
|
||||
<ID>ComplexCondition:ReaderNavigationOverlayView.kt$ReaderNavigationOverlayView$isVisible || (!showOnStart && firstLaunch) || navigation is DisabledNavigation</ID>
|
||||
@ -539,8 +539,8 @@
|
||||
<ID>LongMethod:UpdatesQuery.kt$UpdatesQuery$override fun <R> execute(mapper: (SqlCursor) -> QueryResult<R>): QueryResult<R></ID>
|
||||
<ID>LongMethod:UpdatesUiItem.kt$internal fun LazyListScope.updatesUiItems( uiModels: List<UpdatesUiModel>, selectionMode: Boolean, // SY --> preserveReadingPosition: Boolean, // SY <-- onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit, onClickCover: (UpdatesItem) -> Unit, onClickUpdate: (UpdatesItem) -> Unit, onDownloadChapter: (List<UpdatesItem>, ChapterDownloadAction) -> Unit, )</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 --> private val sourceManager: SourceManager, private val readerPrefs: ReaderPreferences, private val mergedReferences: List<MergedMangaReference>, private val mergedManga: Map<Long, Manga>, // SY <-- )</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:ChapterLoader.kt$ChapterLoader$( private val context: Context, private val downloadManager: DownloadManager, private val downloadProvider: DownloadProvider, private val manga: Manga, private val source: Source, // SY --> private val sourceManager: SourceManager, private val readerPrefs: ReaderPreferences, private val mergedReferences: List<MergedMangaReference>, private val mergedManga: Map<Long, Manga>, // SY <-- )</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: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>
|
||||
@ -806,6 +806,7 @@
|
||||
<ID>MagicNumber:EXHMigrations.kt$EXHMigrations$59</ID>
|
||||
<ID>MagicNumber:EXHMigrations.kt$EXHMigrations$6</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$6907L</ID>
|
||||
<ID>MagicNumber:EXHMigrations.kt$EXHMigrations$6909L</ID>
|
||||
@ -1630,6 +1631,7 @@
|
||||
<ID>NestedBlockDepth:SearchEngine.kt$SearchEngine$fun queryToSql(q: List<QueryComponent>): Pair<String, List<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<SChapter>, manga: Manga, source: Source, manualFetch: Boolean = false, fetchWindow: Pair<Long, Long> = Pair(0, 0), ): List<Chapter></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:WebtoonRecyclerView.kt$WebtoonRecyclerView.Detector$override fun onTouchEvent(ev: MotionEvent): Boolean</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 IV_SIZE = 16</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$/** 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>
|
||||
|
@ -36,6 +36,7 @@ dependencies {
|
||||
implementation(libs.image.decoder)
|
||||
|
||||
implementation(libs.unifile)
|
||||
implementation(libs.bundles.archive)
|
||||
|
||||
api(kotlinx.coroutines.core)
|
||||
api(kotlinx.serialization.json)
|
||||
|
@ -3,25 +3,15 @@ package eu.kanade.tachiyomi.util.storage
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Base64
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
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.enums.AesKeyStrength
|
||||
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 java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
@ -136,12 +126,15 @@ object CbzCrypto {
|
||||
}
|
||||
|
||||
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() {
|
||||
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)]
|
||||
}.joinToString("", transform = { it.toString() })
|
||||
securityPreferences.sqlPassword().set(encrypt(password, encryptionCipherSql))
|
||||
@ -152,27 +145,6 @@ object CbzCrypto {
|
||||
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 {
|
||||
return securityPreferences.cbzPassword().get().isNotEmpty()
|
||||
}
|
||||
@ -228,133 +200,6 @@ object CbzCrypto {
|
||||
}
|
||||
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
|
||||
@ -369,4 +214,6 @@ private const val CRYPTO_SETTINGS = "$ALGORITHM/$BLOCK_MODE/$PADDING"
|
||||
private const val KEYSTORE = "AndroidKeyStore"
|
||||
private const val ALIAS_CBZ = "cbzPw"
|
||||
private const val ALIAS_SQL = "sqlPw"
|
||||
|
||||
private const val SQL_PASSWORD_LENGTH = 32
|
||||
// SY <--
|
||||
|
@ -1,22 +1,36 @@
|
||||
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.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.File
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
@ -33,14 +47,14 @@ class EpubFile(file: File) : Closeable {
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,21 @@
|
||||
package tachiyomi.core.common.storage
|
||||
|
||||
import android.content.Context
|
||||
import android.os.ParcelFileDescriptor
|
||||
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?
|
||||
get() = name?.substringAfterLast('.')
|
||||
@ -10,3 +25,201 @@ val UniFile.nameWithoutExtension: String?
|
||||
|
||||
val UniFile.displayablePath: String
|
||||
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 <--
|
||||
|
@ -26,8 +26,6 @@ import androidx.core.graphics.red
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import com.hippo.unifile.UniFile
|
||||
import logcat.LogPriority
|
||||
import net.lingala.zip4j.ZipFile
|
||||
import net.lingala.zip4j.model.FileHeader
|
||||
import tachiyomi.decoder.Format
|
||||
import tachiyomi.decoder.ImageDecoder
|
||||
import java.io.BufferedInputStream
|
||||
@ -133,20 +131,8 @@ object ImageUtil {
|
||||
*
|
||||
* @return true if the width is greater than the height
|
||||
*/
|
||||
fun isWideImage(
|
||||
imageStream: BufferedInputStream,
|
||||
// SY -->
|
||||
zip4jFile: ZipFile?,
|
||||
zip4jEntry: FileHeader?,
|
||||
// SY <--
|
||||
): Boolean {
|
||||
val options = extractImageOptions(
|
||||
imageStream,
|
||||
// SY -->
|
||||
zip4jFile,
|
||||
zip4jEntry,
|
||||
// SY <--
|
||||
)
|
||||
fun isWideImage(imageStream: BufferedInputStream): Boolean {
|
||||
val options = extractImageOptions(imageStream)
|
||||
return options.outWidth > options.outHeight
|
||||
}
|
||||
|
||||
@ -271,19 +257,9 @@ object ImageUtil {
|
||||
*
|
||||
* @return true if the height:width ratio is greater than 3.
|
||||
*/
|
||||
private fun isTallImage(
|
||||
imageStream: InputStream,
|
||||
// SY -->
|
||||
zip4jFile: ZipFile?,
|
||||
zip4jEntry: FileHeader?,
|
||||
// SY <--
|
||||
): Boolean {
|
||||
private fun isTallImage(imageStream: InputStream): Boolean {
|
||||
val options = extractImageOptions(
|
||||
imageStream,
|
||||
// SY -->
|
||||
zip4jFile,
|
||||
zip4jEntry,
|
||||
// SY <--
|
||||
resetAfterExtraction = false,
|
||||
)
|
||||
|
||||
@ -297,18 +273,9 @@ object ImageUtil {
|
||||
tmpDir: UniFile,
|
||||
imageFile: UniFile,
|
||||
filenamePrefix: String,
|
||||
// SY -->
|
||||
zip4jFile: ZipFile?,
|
||||
zip4jEntry: FileHeader?,
|
||||
// SY <--
|
||||
): Boolean {
|
||||
if (isAnimatedAndSupported(imageFile.openInputStream()) || !isTallImage(
|
||||
imageFile.openInputStream(),
|
||||
// SY -->
|
||||
zip4jFile,
|
||||
zip4jEntry,
|
||||
// SY <--
|
||||
)
|
||||
if (isAnimatedAndSupported(imageFile.openInputStream()) ||
|
||||
!isTallImage(imageFile.openInputStream())
|
||||
) {
|
||||
return true
|
||||
}
|
||||
@ -321,10 +288,6 @@ object ImageUtil {
|
||||
|
||||
val options = extractImageOptions(
|
||||
imageFile.openInputStream(),
|
||||
// SY -->
|
||||
zip4jFile,
|
||||
zip4jEntry,
|
||||
// SY <--
|
||||
resetAfterExtraction = false,
|
||||
).apply {
|
||||
inJustDecodeBounds = false
|
||||
@ -641,17 +604,8 @@ object ImageUtil {
|
||||
*/
|
||||
private fun extractImageOptions(
|
||||
imageStream: InputStream,
|
||||
// SY -->
|
||||
zip4jFile: ZipFile?,
|
||||
zip4jEntry: FileHeader?,
|
||||
// SY <--
|
||||
resetAfterExtraction: Boolean = true,
|
||||
): 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)
|
||||
|
||||
val imageBytes = imageStream.readBytes()
|
||||
@ -661,16 +615,6 @@ object ImageUtil {
|
||||
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
|
||||
* the size of files inside CBZ archives unique
|
||||
|
@ -33,6 +33,7 @@ jsoup = "org.jsoup:jsoup:1.17.2"
|
||||
|
||||
disklrucache = "com.jakewharton:disklrucache:2.0.2"
|
||||
unifile = "com.github.tachiyomiorg:unifile:7c257e1c64"
|
||||
common-compress = "org.apache.commons:commons-compress:1.26.0"
|
||||
junrar = "com.github.junrar:junrar:7.5.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]
|
||||
acra = ["acra-http", "acra-scheduler"]
|
||||
archive = ["common-compress", "junrar"]
|
||||
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"]
|
||||
js-engine = ["quickjs-android"]
|
||||
sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"]
|
||||
|
@ -324,9 +324,13 @@
|
||||
<string name="pref_center_margin">Center margin type</string>
|
||||
<string name="pref_center_margin_summary">Insert spacer to accommodate deadspace on foldable devices.</string>
|
||||
|
||||
<!-- Cache archived manga to disk -->
|
||||
<string name="cache_archived_manga_to_disk">Cache images inside CBZ/CBR archives on disk</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>
|
||||
<!-- Archive reader mode -->
|
||||
<string name="archive_mode_load_from_file">Load from file</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 Info -->
|
||||
|
@ -15,7 +15,7 @@ kotlin {
|
||||
// SY <--
|
||||
|
||||
implementation(libs.unifile)
|
||||
implementation(libs.junrar)
|
||||
implementation(libs.bundles.archive)
|
||||
// SY -->
|
||||
implementation(libs.zip4j)
|
||||
// SY <--
|
||||
|
@ -1,6 +1,7 @@
|
||||
package tachiyomi.source.local
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
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.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
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 kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
@ -31,7 +28,11 @@ import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
|
||||
import tachiyomi.core.metadata.comicinfo.getComicInfo
|
||||
import tachiyomi.core.metadata.tachiyomi.MangaDetails
|
||||
import tachiyomi.core.common.storage.UniFileTempFileManager
|
||||
import tachiyomi.core.common.storage.addStreamToZip
|
||||
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.util.lang.withIOContext
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
@ -268,7 +269,13 @@ actual class LocalSource(
|
||||
}
|
||||
}
|
||||
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.getInputStream(comicInfoFile).buffered().use { stream ->
|
||||
return copyComicInfoFile(stream, folder)
|
||||
@ -330,7 +337,7 @@ actual class LocalSource(
|
||||
|
||||
val format = Format.valueOf(chapterFile)
|
||||
if (format is Format.Epub) {
|
||||
EpubFile(tempFileManager.createTempFile(format.file)).use { epub ->
|
||||
EpubFile(format.file, context).use { epub ->
|
||||
epub.fillMetadata(manga, this)
|
||||
}
|
||||
}
|
||||
@ -397,10 +404,15 @@ actual class LocalSource(
|
||||
format.file.isEncryptedZip()
|
||||
)
|
||||
}
|
||||
// SY <--
|
||||
}
|
||||
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
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
|
||||
@ -409,7 +421,9 @@ actual class LocalSource(
|
||||
}
|
||||
}
|
||||
is Format.Epub -> {
|
||||
EpubFile(tempFileManager.createTempFile(format.file)).use { epub ->
|
||||
// SY -->
|
||||
EpubFile(format.file, context).use { epub ->
|
||||
// SY <--
|
||||
val entry = epub.getImagesFromPages()
|
||||
.firstOrNull()
|
||||
?.let { epub.getEntry(it) }
|
||||
|
@ -4,7 +4,7 @@ import android.content.Context
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
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 tachiyomi.core.common.storage.nameWithoutExtension
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
|
Loading…
x
Reference in New Issue
Block a user