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