diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt
index beb6b85a0..2deb06773 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt
@@ -1,6 +1,5 @@
package eu.kanade.presentation.more.settings.screen
-import android.widget.Toast
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
@@ -25,7 +24,6 @@ import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
@@ -57,13 +55,8 @@ import eu.kanade.tachiyomi.ui.category.biometric.BiometricTimesScreen
import eu.kanade.tachiyomi.util.storage.CbzCrypto
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
-import eu.kanade.tachiyomi.util.system.toast
-import logcat.LogPriority
-import tachiyomi.core.util.lang.withIOContext
-import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
-import java.io.File
object SettingsSecurityScreen : SearchableSettings {
@@ -168,45 +161,6 @@ object SettingsSecurityScreen : SearchableSettings {
},
enabled = isCbzPasswordSet,
),
- Preference.PreferenceItem.ListPreference(
- pref = securityPreferences.localCoverLocation(),
- title = stringResource(R.string.save_local_manga_covers),
- entries = SecurityPreferences.CoverCacheLocation.values()
- .associateWith { stringResource(it.titleResId) },
- enabled = passwordProtectDownloads,
- onValueChanged = {
- try {
- withIOContext {
- CbzCrypto.deleteLocalCoverCache(context)
- CbzCrypto.deleteLocalCoverSystemFiles(context)
- }
- true
- } catch (e: Exception) {
- logcat(LogPriority.ERROR, e)
- context.toast(e.toString(), Toast.LENGTH_SHORT).show()
- false
- }
- },
- ),
- Preference.PreferenceItem.TextPreference(
- title = stringResource(R.string.delete_cached_local_source_covers),
- subtitle = stringResource(R.string.delete_cached_local_source_covers_subtitle),
- onClick = {
- try {
- CbzCrypto.deleteLocalCoverCache(context)
- CbzCrypto.deleteLocalCoverSystemFiles(context)
- context.toast(R.string.successfully_deleted_all_locally_cached_covers, Toast.LENGTH_SHORT).show()
- } catch (e: Exception) {
- logcat(LogPriority.ERROR, e)
- context.toast(R.string.something_went_wrong_deleting_your_cover_images, Toast.LENGTH_LONG).show()
- }
- },
- enabled = produceState(false) {
- withIOContext {
- value = context.getExternalFilesDir("covers/local")?.absolutePath?.let { File(it).listFiles()?.isNotEmpty() } == true
- }
- }.value,
- ),
kotlin.run {
val navigator = LocalNavigator.currentOrThrow
val count by securityPreferences.authenticatorTimeRanges().collectAsState()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt
index 398ff1098..836796dfc 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt
@@ -214,7 +214,7 @@ class PreferenceModule(val application: Application) : InjektModule {
SourcePreferences(get())
}
addSingletonFactory {
- SecurityPreferences(get(), application.applicationContext)
+ SecurityPreferences(get())
}
addSingletonFactory {
LibraryPreferences(get())
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt
index e0a5ffe8e..382ed0c6a 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt
@@ -9,6 +9,9 @@ import coil.decode.ImageDecoderDecoder
import coil.decode.ImageSource
import coil.fetch.SourceResult
import coil.request.Options
+import eu.kanade.tachiyomi.util.storage.CbzCrypto
+import net.lingala.zip4j.ZipFile
+import net.lingala.zip4j.model.FileHeader
import okio.BufferedSource
import tachiyomi.core.util.system.ImageUtil
import tachiyomi.decoder.ImageDecoder
@@ -19,9 +22,24 @@ import tachiyomi.decoder.ImageDecoder
class TachiyomiImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder {
override suspend fun decode(): DecodeResult {
- val decoder = resources.sourceOrNull()?.use {
- ImageDecoder.newInstance(it.inputStream())
+ // SY -->
+ var zip4j: ZipFile? = null
+ var entry: FileHeader? = null
+
+ if (resources.sourceOrNull()?.peek()?.use { CbzCrypto.detectCoverImageArchive(it.inputStream()) } == true) {
+ if (resources.source().peek().use { ImageUtil.findImageType(it.inputStream()) == null }) {
+ zip4j = ZipFile(resources.file().toFile().absolutePath)
+ entry = zip4j.fileHeaders.firstOrNull { it.fileName.equals(CbzCrypto.DEFAULT_COVER_NAME, ignoreCase = true) }
+
+ if (zip4j.isEncrypted) zip4j.setPassword(CbzCrypto.getDecryptedPasswordCbz())
+ }
}
+ val decoder = resources.sourceOrNull()?.use {
+ zip4j.use { zipFile ->
+ ImageDecoder.newInstance(zipFile?.getInputStream(entry) ?: it.inputStream())
+ }
+ }
+ // SY <--
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder" }
@@ -45,6 +63,9 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
private fun isApplicable(source: BufferedSource): Boolean {
val type = source.peek().inputStream().use {
+ // SY -->
+ if (CbzCrypto.detectCoverImageArchive(it)) return true
+ // SY <--
ImageUtil.findImageType(it)
}
return when (type) {
diff --git a/core/src/main/java/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt b/core/src/main/java/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt
index 0e4e50aa4..71b4e52dc 100644
--- a/core/src/main/java/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt
+++ b/core/src/main/java/eu/kanade/tachiyomi/core/security/SecurityPreferences.kt
@@ -1,13 +1,11 @@
package eu.kanade.tachiyomi.core.security
-import android.content.Context
import eu.kanade.tachiyomi.core.R
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.preference.getEnum
class SecurityPreferences(
private val preferenceStore: PreferenceStore,
- private val context: Context,
) {
fun useAuthenticator() = preferenceStore.getBoolean("use_biometric_lock", false)
@@ -23,7 +21,7 @@ class SecurityPreferences(
fun authenticatorDays() = this.preferenceStore.getInt("biometric_days", 0x7F)
- fun encryptDatabase() = this.preferenceStore.getBoolean("encrypt_database", !context.getDatabasePath("tachiyomi.db").exists())
+ fun encryptDatabase() = this.preferenceStore.getBoolean("encrypt_database", false)
fun sqlPassword() = this.preferenceStore.getString("sql_password", "")
@@ -32,9 +30,6 @@ class SecurityPreferences(
fun encryptionType() = this.preferenceStore.getEnum("encryption_type", EncryptionType.AES_256)
fun cbzPassword() = this.preferenceStore.getString("cbz_password", "")
-
- fun localCoverLocation() = this.preferenceStore.getEnum("local_cover_location", CoverCacheLocation.IN_MANGA_DIRECTORY)
-
// SY <--
/**
@@ -56,11 +51,5 @@ class SecurityPreferences(
AES_128(R.string.aes_128),
ZIP_STANDARD(R.string.standard_zip_encryption),
}
- enum class CoverCacheLocation(val titleResId: Int) {
- IN_MANGA_DIRECTORY(R.string.save_in_manga_directory),
- INTERNAL(R.string.save_internally),
- NEVER(R.string.save_never),
- }
-
// SY <--
}
diff --git a/core/src/main/java/eu/kanade/tachiyomi/util/storage/CbzCrypto.kt b/core/src/main/java/eu/kanade/tachiyomi/util/storage/CbzCrypto.kt
index 242b19a0a..125fa93e2 100644
--- a/core/src/main/java/eu/kanade/tachiyomi/util/storage/CbzCrypto.kt
+++ b/core/src/main/java/eu/kanade/tachiyomi/util/storage/CbzCrypto.kt
@@ -1,10 +1,8 @@
package eu.kanade.tachiyomi.util.storage
-import android.content.Context
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
-import eu.kanade.tachiyomi.core.R
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
@@ -20,7 +18,7 @@ import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
-import java.io.File
+import java.io.InputStream
import java.security.KeyStore
import java.security.SecureRandom
import javax.crypto.Cipher
@@ -36,6 +34,7 @@ import javax.crypto.spec.IvParameterSpec
*/
object CbzCrypto {
const val DATABASE_NAME = "tachiyomiEncrypted.db"
+ const val DEFAULT_COVER_NAME = "cover.jpg"
private val securityPreferences: SecurityPreferences by injectLazy()
private val keyStore = KeyStore.getInstance(KEYSTORE).apply {
load(null)
@@ -210,23 +209,15 @@ object CbzCrypto {
}
}
- fun deleteLocalCoverCache(context: Context) {
- if (context.getExternalFilesDir(LOCAL_CACHE_DIR)?.exists() == true) {
- context.getExternalFilesDir(LOCAL_CACHE_DIR)?.deleteRecursively()
+ fun detectCoverImageArchive(stream: InputStream): Boolean {
+ val bytes = ByteArray(128)
+ if (stream.markSupported()) {
+ stream.mark(bytes.size)
+ stream.read(bytes, 0, bytes.size).also { stream.reset() }
+ } else {
+ stream.read(bytes, 0, bytes.size)
}
- }
-
- fun deleteLocalCoverSystemFiles(context: Context) {
- val baseFolderLocation = "${context.getString(R.string.app_name)}${File.separator}local"
-
- DiskUtil.getExternalStorages(context)
- .map { File(it.absolutePath, baseFolderLocation) }
- .asSequence()
- .flatMap { it.listFiles().orEmpty().toList() }
- .filter { it.isDirectory }
- .flatMap { it.listFiles().orEmpty().toList() }
- .filter { it.name == ".cacheCoverInternal" || it.name == ".nocover" }
- .forEach { it.delete() }
+ return String(bytes).contains(DEFAULT_COVER_NAME, ignoreCase = true)
}
}
@@ -242,7 +233,4 @@ 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 LOCAL_CACHE_DIR = "covers/local"
-
// SY <--
diff --git a/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt b/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt
index 688972faa..4c781b986 100644
--- a/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt
+++ b/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt
@@ -42,6 +42,10 @@ import kotlin.math.min
object ImageUtil {
fun isImage(name: String, openStream: (() -> InputStream)? = null): Boolean {
+ // SY -->
+ if (File(name).extension.equals("cbi", ignoreCase = true)) return true
+ // SY <--
+
val contentType = try {
URLConnection.guessContentTypeFromName(name)
} catch (e: Exception) {
diff --git a/i18n/src/main/res/values/strings_sy.xml b/i18n/src/main/res/values/strings_sy.xml
index 5f88a954d..f5b8bbb4f 100644
--- a/i18n/src/main/res/values/strings_sy.xml
+++ b/i18n/src/main/res/values/strings_sy.xml
@@ -230,14 +230,6 @@
Password protect downloads
Encrypts CBZ archive downloads with the given password.\nWARNING: DATA INSIDE THE ARCHIVES WILL BE LOST FOREVER IF YOU FORGET THE PASSWORD
Delete CBZ archive password
- In manga directory
- Internally
- Never (local source manga won\'t have covers)
- Save local manga covers
- Delete cached local source covers
- Internally cached local source manga covers are NOT deleted automatically Please delete them here then open the local source extension to generate them again
- Successfully deleted all locally cached covers
- Something went wrong deleting your cover images:
CBZ archive password
Wrong CBZ archive password
Encryption type
diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt
index f3c45d7ae..9f28346c5 100755
--- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt
+++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt
@@ -390,13 +390,16 @@ actual class LocalSource(
is Format.Zip -> {
ZipFile(format.file).use { zip ->
// SY -->
- if (zip.isEncrypted) zip.setPassword(CbzCrypto.getDecryptedPasswordCbz())
+ var encrypted = false
+ if (zip.isEncrypted) {
+ zip.setPassword(CbzCrypto.getDecryptedPasswordCbz())
+ encrypted = true
+ }
val entry = zip.fileHeaders.toList()
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { zip.getInputStream(it) } }
+ entry?.let { coverManager.update(manga, zip.getInputStream(it), encrypted) }
// SY <--
-
- entry?.let { coverManager.update(manga, zip.getInputStream(it)) }
}
}
is Format.Rar -> {
diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt
index 32c3ce6c4..050ef3177 100644
--- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt
+++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt
@@ -2,58 +2,40 @@ package tachiyomi.source.local.image
import android.content.Context
import com.hippo.unifile.UniFile
-import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.util.storage.CbzCrypto
import eu.kanade.tachiyomi.util.storage.DiskUtil
+import net.lingala.zip4j.ZipFile
+import net.lingala.zip4j.model.ZipParameters
import tachiyomi.core.util.system.ImageUtil
import tachiyomi.source.local.io.LocalSourceFileSystem
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
import java.io.File
import java.io.InputStream
private const val DEFAULT_COVER_NAME = "cover.jpg"
-
-// SY -->
-private const val NO_COVER_FILE = ".nocover"
-private const val CACHE_COVER_INTERNAL = ".cacheCoverInternal"
-private const val LOCAL_CACHE_DIR = "covers/local"
-// SY <--
+private const val COVER_ARCHIVE_NAME = "cover.cbi"
actual class LocalCoverManager(
private val context: Context,
private val fileSystem: LocalSourceFileSystem,
-
- // SY -->
- private val coverCacheDir: File? = context.getExternalFilesDir(LOCAL_CACHE_DIR),
- private val securityPreferences: SecurityPreferences = Injekt.get(),
- // SY <--
-
) {
actual fun find(mangaUrl: String): File? {
return fileSystem.getFilesInMangaDirectory(mangaUrl)
// Get all file whose names start with 'cover'
- // --> SY
- .filter { (it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true)) || it.name == NO_COVER_FILE || it.name == CACHE_COVER_INTERNAL }
+ .filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
// Get the first actual image
.firstOrNull {
- if (it.name != NO_COVER_FILE && it.name != CACHE_COVER_INTERNAL) {
- ImageUtil.isImage(it.name) { it.inputStream() }
- } else if (it.name == NO_COVER_FILE) {
- true
- } else if (it.name == CACHE_COVER_INTERNAL) {
- return File("$coverCacheDir/${it.parentFile?.name}/$DEFAULT_COVER_NAME")
- } else {
- false
- }
- // SY <--
+ ImageUtil.isImage(it.name) { it.inputStream() } || it.name == COVER_ARCHIVE_NAME
}
}
actual fun update(
manga: SManga,
inputStream: InputStream,
+ // SY -->
+ encrypted: Boolean,
+ // SY <--
): File? {
val directory = fileSystem.getMangaDirectory(manga.url)
if (directory == null) {
@@ -64,48 +46,40 @@ actual class LocalCoverManager(
var targetFile = find(manga.url)
if (targetFile == null) {
// SY -->
- targetFile = when (securityPreferences.localCoverLocation().get()) {
- SecurityPreferences.CoverCacheLocation.INTERNAL -> File(directory.absolutePath, CACHE_COVER_INTERNAL)
- SecurityPreferences.CoverCacheLocation.NEVER -> File(directory.absolutePath, NO_COVER_FILE)
- SecurityPreferences.CoverCacheLocation.IN_MANGA_DIRECTORY -> File(directory.absolutePath, DEFAULT_COVER_NAME)
+ if (encrypted) {
+ targetFile = File(directory.absolutePath, COVER_ARCHIVE_NAME)
+ } else {
+ targetFile = File(directory.absolutePath, DEFAULT_COVER_NAME)
+ targetFile.createNewFile()
}
- if (targetFile.parentFile?.parentFile?.name != "local") targetFile.parentFile?.mkdirs()
- targetFile.createNewFile()
+ // SY <--
}
- if (targetFile.name == NO_COVER_FILE) return null
- if (securityPreferences.localCoverLocation().get() == SecurityPreferences.CoverCacheLocation.IN_MANGA_DIRECTORY) {
- // SY <--
- // It might not exist at this point
- targetFile.parentFile?.mkdirs()
- inputStream.use { input ->
+ // It might not exist at this point
+ targetFile.parentFile?.mkdirs()
+ inputStream.use { input ->
+ // SY -->
+ if (encrypted) {
+ val zip4j = ZipFile(targetFile)
+ val zipParameters = ZipParameters()
+ zip4j.setPassword(CbzCrypto.getDecryptedPasswordCbz())
+ CbzCrypto.setZipParametersEncrypted(zipParameters)
+ zipParameters.fileNameInZip = DEFAULT_COVER_NAME
+ zip4j.addStream(input, zipParameters)
+
+ DiskUtil.createNoMediaFile(UniFile.fromFile(directory), context)
+
+ manga.thumbnail_url = zip4j.file.absolutePath
+ return zip4j.file
+ } else {
+ // SY <--
targetFile.outputStream().use { output ->
input.copyTo(output)
}
DiskUtil.createNoMediaFile(UniFile.fromFile(directory), context)
manga.thumbnail_url = targetFile.absolutePath
return targetFile
- // SY -->
}
- } else if (securityPreferences.localCoverLocation().get() == SecurityPreferences.CoverCacheLocation.INTERNAL) {
- // It might not exist at this point
- targetFile.parentFile?.mkdirs()
- val path = "$coverCacheDir/${targetFile.parentFile?.name}/$DEFAULT_COVER_NAME"
- val outputFile = File(path)
-
- outputFile.parentFile?.mkdirs()
- outputFile.createNewFile()
-
- inputStream.use { input ->
- outputFile.outputStream().use { output ->
- input.copyTo(output)
- }
- }
- manga.thumbnail_url = outputFile.absolutePath
- return outputFile
- } else {
- return null
}
- // SY <--
}
}
diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt
index fd31299c2..a9c3d5a59 100644
--- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt
+++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt
@@ -8,5 +8,7 @@ expect class LocalCoverManager {
fun find(mangaUrl: String): File?
- fun update(manga: SManga, inputStream: InputStream): File?
+ // SY -->
+ fun update(manga: SManga, inputStream: InputStream, encrypted: Boolean = false): File?
+ // SY <--
}