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
|
// 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 <--
|
||||||
|
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
@ -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.** { *; }
|
||||||
|
@ -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(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -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" }
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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 */
|
||||||
|
@ -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 <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
@ -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)
|
||||||
|
@ -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)) {
|
||||||
|
@ -187,7 +187,6 @@ class InterceptActivity : BaseActivity() {
|
|||||||
lifecycleScope.launchIO {
|
lifecycleScope.launchIO {
|
||||||
loadGalleryEnd(gallery, sources[index])
|
loadGalleryEnd(gallery, sources[index])
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
} else {
|
} 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: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: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: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: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: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>
|
<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: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: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>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: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, )</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<QueryComponent>): Pair<String, List<String>></ID>
|
<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: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: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: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>
|
||||||
|
@ -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)
|
||||||
|
@ -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 <--
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 <--
|
||||||
|
@ -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
|
||||||
|
@ -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"]
|
||||||
|
@ -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 -->
|
||||||
|
@ -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 <--
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user