Compare commits

...

38 Commits

Author SHA1 Message Date
Jobobby04 46bf139f01 Possibly fix extension obsolete bug 2024-05-05 13:17:29 -04:00
Jobobby04 c3fb5c0bec Revert "Bump compose version"
This reverts commit 5550ddad4e.
2024-05-05 12:58:03 -04:00
Reagan 000a4ffc3f Change keyboard type in extension repo dialog (#764)
(cherry picked from commit 550f1197e818c35c7c05fd6184e69c7d29559e9f)
2024-05-05 12:57:45 -04:00
Jobobby04 7b0b879d65 Downgrade crashlytics plugin 2024-05-05 00:07:41 -04:00
Dexroneum 8a622f6c7d [RU] Translations (#1161)
* [RU] Translations

* [RU] Deleted unused strings
2024-05-04 23:39:57 -04:00
Jobobby04 253060a3bc Minor cleanup 2024-05-04 23:15:17 -04:00
Jobobby04 b6b33e8c00 Get new page url on image fetch failure for EHentai 2024-05-04 23:13:52 -04:00
Jobobby04 2e4f811090 Add getImageUrl override to EHentai 2024-05-04 23:13:26 -04:00
Jobobby04 215a1908f7 Include lewd filter in filter highlight 2024-05-04 23:13:06 -04:00
Jobobby04 082acf000c Fix Local Manga details edit 2024-05-04 23:12:45 -04:00
Jobobby04 2d1240b274 Update dependencies and cleanup 2024-05-04 18:41:03 -04:00
Jobobby04 1e98709cc3 Revert "Fix badge count getting cut off on tab title"
This reverts commit f9148c0c5e.
2024-05-04 18:12:54 -04:00
AntsyLich 5550ddad4e Bump compose version
Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
(cherry picked from commit e473c7f09fc009161145aca94bd70027f042b0bf)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt
#	app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt
2024-05-04 17:07:27 -04:00
AntsyLich f9148c0c5e Fix badge count getting cut off on tab title
Fixes #335

(cherry picked from commit 263e467cdeb948b8f3679e2ea0282a291cf2f131)
2024-05-04 16:57:20 -04:00
Radon Rosborough 089e6268e7 Massively improve findFile performance (#728)
* Massively improve findFile performance

* Update libs.versions.toml

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
(cherry picked from commit 7ec2108812fbe0483111dbe996e29e5a621b583a)
2024-05-04 16:56:45 -04:00
AntsyLich 712cd1493f Address firebase ktx module deprecation
(cherry picked from commit 28dca3b7b818ad095008e7cd49ec07a82b0ebcad)

# Conflicts:
#	gradle/libs.versions.toml
2024-05-04 16:52:36 -04:00
AntsyLich bbc8adc3e8 Trust extension by repo (#570)
(cherry picked from commit 70cd688ac245a70a3146e2ac7374f24b0c3453ab)
2024-05-04 16:51:47 -04:00
AntsyLich 077b673c0a Fix some extension related issue and cleanups
- Extension being marked as not installed instead of untrusted after updating with private installer
- Extension update counter not updating due to extension being marked as untrusted
- Minimize `Key "extension-XXX-YYY" was already used` crash

(cherry picked from commit 21145144cdf550aa775047603e06e261951ebc42)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt
2024-05-04 16:51:26 -04:00
renovate[bot] 49eacf5178 fix(deps): update leakcanary to v2.14 (#715)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit fa6dba6cc76f0b08cbc9bf222b0e087f4fb16d76)

# Conflicts:
#	gradle/libs.versions.toml
2024-05-04 16:01:19 -04:00
renovate[bot] 98d1dddf4a fix(deps): update dependency com.android.tools.build:gradle to v8.4.0 (#753)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 8a51d56c594e1f7ae4ebc01fc6a639292dde78bd)
2024-05-04 16:00:27 -04:00
renovate[bot] 37a616f3db fix(deps): update dependency androidx.test.espresso:espresso-core to v3.6.0-alpha04 (#749)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit a2f7d47a0a65bf88ac609b2227d440a7a2f841bf)
2024-05-04 16:00:14 -04:00
renovate[bot] ad18696a1a fix(deps): update dependency androidx.core:core-ktx to v1.13.1 (#748)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit b720f34267e4111466cdabf3a298d006231e4b55)
2024-05-04 15:59:53 -04:00
renovate[bot] 34bb012a1c fix(deps): update dependency androidx.test.ext:junit-ktx to v1.2.0-alpha04 (#751)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit c6a1412f18cb16f89c4ddeadb3448c141a49072e)
2024-05-04 15:59:43 -04:00
renovate[bot] 08c4989aa3 fix(deps): update aboutlib.version to v11.1.4 (#744)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 6290cf222df922240575e2199459ab7b707d6ae2)

# Conflicts:
#	gradle/libs.versions.toml
2024-05-04 15:59:38 -04:00
FooIbar 14dae420f5 Log app crash exceptions in dumped crash logs (#742)
(cherry picked from commit a3d438e2f5b427eb8b4c391ab9fe10c5a83baf29)
2024-05-04 15:59:04 -04:00
w 65ed3c5ae6 Update subsampling-scale-image-view (#687)
Update libs.versions.toml

(cherry picked from commit 80461d883f7d6ca2203ae7455223ff49e8ef96ab)
2024-05-04 15:58:49 -04:00
FooIbar 5ae3508665 Use Coil pipeline instead of SSIV for image decode (#692)
(cherry picked from commit c3e7bb12f4cccf42dd3ea169111c771876e640fe)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt
2024-05-04 15:58:41 -04:00
MajorTanya e32eb0e009 Add MyAnimeList issue autoclose (#703)
[skip ci] Add MyAnimeList issue autoclose

This rule is intended to automatically close issues that report
problems with linking MAL that would be solved with the standard
solution of updating & changing the default UA.

The RegEx might be too general, but there isn't any neat pattern in
the previously filed issues.

(cherry picked from commit 9a3ffe2ea6cbf7ef2c2966c304a54b715a5fa682)
2024-05-04 15:51:26 -04:00
renovate[bot] e0812ab5c8 fix(deps): update dependency androidx.compose.compiler:compiler to v1.5.12 (#685)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 213effa169e28e144f3e323290d865b02d0bf94b)
2024-05-04 15:51:13 -04:00
renovate[bot] df9f79c120 fix(deps): update dependency androidx.benchmark:benchmark-macro-junit4 to v1.2.4 (#684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 25570147a1ca8ac374a75d7f29cf105bd686954b)
2024-05-04 15:51:04 -04:00
renovate[bot] 990eb33b98 fix(deps): update dependency androidx.activity:activity-compose to v1.9.0 (#689)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 2ad462b4d882c4a03359092515aa6b8d3cb4fd5d)
2024-05-04 15:50:50 -04:00
renovate[bot] e1bab1172a fix(deps): update dependency androidx.core:core-ktx to v1.13.0 (#690)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 7fd8f653529b8e1488dd57c051000abf2a80ed12)
2024-05-04 15:50:41 -04:00
FooIbar aeeff72bed Use Okio instead of java.io for image processing (#691)
(cherry picked from commit b152e3881bffd9050a8a0ed4030823886e3fe04f)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/App.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt
#	core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt
2024-05-04 15:50:20 -04:00
FooIbar 5895e78b39 Use m3 ripple and clean up interactionSource usage (#675)
Also remove a leftover of scoped storage adaptation.

(cherry picked from commit f27ca3b1b2f92258c213bca6b27d8eff4c7363ad)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/manga/components/MangaBottomActionMenu.kt
2024-05-04 15:05:16 -04:00
FooIbar b24719a3e9 Update compose bom and fix renovate config for it (#674)
(cherry picked from commit 843daa5304d0b1a93ba69f8cc69791e446a58596)

# Conflicts:
#	.github/renovate.json5
2024-05-04 15:04:40 -04:00
renovate[bot] d551619d9d fix(deps): update dependency com.google.firebase:firebase-analytics-ktx to v21.6.2 (#656)
Update dependency com.google.firebase:firebase-analytics-ktx to v21.6.2

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit f080a4937e61d3dde5473876c34db8f16844e30c)

# Conflicts:
#	gradle/libs.versions.toml
2024-05-04 15:04:12 -04:00
renovate[bot] 06ad6c2e16 Update aboutlib.version to v11.1.3 (#654)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 4c43a0ef66e9c8a321fb745b860319aaa074c57f)

# Conflicts:
#	gradle/libs.versions.toml
2024-05-04 15:03:39 -04:00
renovate[bot] df7e470e08 Update dependency com.android.tools.build:gradle to v8.3.2 (#655)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit ea0fe2414e1e30b6e82ddf65144035283b31a5c4)
2024-05-04 15:02:59 -04:00
45 changed files with 569 additions and 431 deletions
+6
View File
@@ -40,6 +40,12 @@ jobs:
"ignoreCase": true, "ignoreCase": true,
"labels": ["Cloudflare protected"], "labels": ["Cloudflare protected"],
"message": "Refer to the **Solving Cloudflare issues** section at https://mihon.app/docs/guides/troubleshooting/#cloudflare. If it doesn't work, migrate to other sources or wait until they lower their protection." "message": "Refer to the **Solving Cloudflare issues** section at https://mihon.app/docs/guides/troubleshooting/#cloudflare. If it doesn't work, migrate to other sources or wait until they lower their protection."
},
{
"type": "both",
"regex": "^.*(myanimelist|mal).*$",
"ignoreCase": true,
"message": "For issues with linking MyAnimeList, please follow these steps:\n1. Update Mihon to version 0.16.4 or newer\n2. Change your default User-Agent (`More → Settings → Advanced → Default user agent string`)\n3. Close and restart Mihon\n4. Attempt to link MyAnimeList again\n\nIf you had MyAnimeList linked before, try to unlink it first before trying these steps."
} }
] ]
auto-close-ignore-label: do-not-autoclose auto-close-ignore-label: do-not-autoclose
-2
View File
@@ -254,7 +254,6 @@ dependencies {
implementation(libs.logcat) implementation(libs.logcat)
// Crash reports/analytics // Crash reports/analytics
// implementation(libs.bundles.acra)
// "standardImplementation"(libs.firebase.analytics) // "standardImplementation"(libs.firebase.analytics)
// Shizuku // Shizuku
@@ -315,7 +314,6 @@ tasks {
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi", "-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi", "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi", "-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview", "-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi", "-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
@@ -179,7 +179,7 @@ class DomainModule : InjektModule {
addFactory { ToggleLanguage(get()) } addFactory { ToggleLanguage(get()) }
addFactory { ToggleSource(get()) } addFactory { ToggleSource(get()) }
addFactory { ToggleSourcePin(get()) } addFactory { ToggleSourcePin(get()) }
addFactory { TrustExtension(get()) } addFactory { TrustExtension(get(), get()) }
addSingletonFactory<ExtensionRepoRepository> { ExtensionRepoRepositoryImpl(get()) } addSingletonFactory<ExtensionRepoRepository> { ExtensionRepoRepositoryImpl(get()) }
addFactory { ExtensionRepoService(get(), get()) } addFactory { ExtensionRepoService(get(), get()) }
@@ -2,8 +2,6 @@ package eu.kanade.domain.base
import android.content.Context import android.content.Context
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -22,8 +20,6 @@ class BasePreferences(
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore) fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)
fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType)
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false) fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) { enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
@@ -20,7 +20,7 @@ class GetExtensionsByType(
extensionManager.installedExtensionsFlow, extensionManager.installedExtensionsFlow,
extensionManager.untrustedExtensionsFlow, extensionManager.untrustedExtensionsFlow,
extensionManager.availableExtensionsFlow, extensionManager.availableExtensionsFlow,
) { _activeLanguages, _installed, _untrusted, _available -> ) { enabledLanguages, _installed, _untrusted, _available ->
val (updates, installed) = _installed val (updates, installed) = _installed
.filter { (showNsfwSources || !it.isNsfw) } .filter { (showNsfwSources || !it.isNsfw) }
.sortedWith( .sortedWith(
@@ -41,9 +41,9 @@ class GetExtensionsByType(
} }
.flatMap { ext -> .flatMap { ext ->
if (ext.sources.isEmpty()) { if (ext.sources.isEmpty()) {
return@flatMap if (ext.lang in _activeLanguages) listOf(ext) else emptyList() return@flatMap if (ext.lang in enabledLanguages) listOf(ext) else emptyList()
} }
ext.sources.filter { it.lang in _activeLanguages } ext.sources.filter { it.lang in enabledLanguages }
.map { .map {
ext.copy( ext.copy(
name = it.name, name = it.name,
@@ -3,15 +3,18 @@ package eu.kanade.domain.extension.interactor
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import androidx.core.content.pm.PackageInfoCompat import androidx.core.content.pm.PackageInfoCompat
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
import tachiyomi.core.common.preference.getAndSet import tachiyomi.core.common.preference.getAndSet
class TrustExtension( class TrustExtension(
private val extensionRepoRepository: ExtensionRepoRepository,
private val preferences: SourcePreferences, private val preferences: SourcePreferences,
) { ) {
fun isTrusted(pkgInfo: PackageInfo, signatureHash: String): Boolean { suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List<String>): Boolean {
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:$signatureHash" val trustedFingerprints = extensionRepoRepository.getAll().map { it.signingKeyFingerprint }.toHashSet()
return key in preferences.trustedExtensions().get() val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}"
return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions().get()
} }
fun trust(pkgName: String, versionCode: Long, signatureHash: String) { fun trust(pkgName: String, versionCode: Long, signatureHash: String) {
@@ -19,9 +22,7 @@ class TrustExtension(
// Remove previously trusted versions // Remove previously trusted versions
val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet() val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet()
removed.also { removed.also { it += "$pkgName:$versionCode:$signatureHash" }
it += "$pkgName:$versionCode:$signatureHash"
}
} }
} }
@@ -5,7 +5,6 @@ import android.net.Uri
import android.provider.Settings import android.provider.Settings
import android.util.DisplayMetrics import android.util.DisplayMetrics
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -362,10 +361,8 @@ private fun InfoText(
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge, primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
) { ) {
val interactionSource = remember { MutableInteractionSource() }
val clickableModifier = if (onClick != null) { val clickableModifier = if (onClick != null) {
Modifier.clickable(interactionSource, indication = null) { onClick() } Modifier.clickable(interactionSource = null, indication = null, onClick = onClick)
} else { } else {
Modifier Modifier
} }
@@ -37,7 +37,7 @@ fun CrashScreen(
acceptText = stringResource(MR.strings.pref_dump_crash_logs), acceptText = stringResource(MR.strings.pref_dump_crash_logs),
onAcceptClick = { onAcceptClick = {
scope.launch { scope.launch {
CrashLogUtil(context).dumpLogs() CrashLogUtil(context).dumpLogs(exception)
} }
}, },
rejectText = stringResource(MR.strings.crash_screen_restart_application), rejectText = stringResource(MR.strings.crash_screen_restart_application),
@@ -9,13 +9,13 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.outlined.ArrowDownward import androidx.compose.material.icons.outlined.ArrowDownward
import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material.ripple
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -8,7 +8,6 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -33,12 +32,12 @@ import androidx.compose.material.icons.outlined.Label
import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.RemoveDone import androidx.compose.material.icons.outlined.RemoveDone
import androidx.compose.material.icons.outlined.SwapCalls import androidx.compose.material.icons.outlined.SwapCalls
import androidx.compose.material.ripple
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
@@ -199,7 +198,7 @@ private fun RowScope.Button(
.size(48.dp) .size(48.dp)
.weight(animatedWeight) .weight(animatedWeight)
.combinedClickable( .combinedClickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = null,
indication = ripple(bounded = false), indication = ripple(bounded = false),
onLongClick = onLongClick, onLongClick = onLongClick,
onClick = onClick, onClick = onClick,
@@ -1,6 +1,7 @@
package eu.kanade.presentation.more.settings.screen.browse.components package eu.kanade.presentation.more.settings.screen.browse.components
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -14,6 +15,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.text.input.KeyboardType
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import mihon.domain.extensionrepo.model.ExtensionRepo import mihon.domain.extensionrepo.model.ExtensionRepo
@@ -74,6 +76,7 @@ fun ExtensionRepoCreateDialog(
Text(text = stringResource(msgRes)) Text(text = stringResource(msgRes))
}, },
isError = name.isNotEmpty() && nameAlreadyExists, isError = name.isNotEmpty() && nameAlreadyExists,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
singleLine = true, singleLine = true,
) )
} }
@@ -38,6 +38,7 @@ import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
import eu.kanade.tachiyomi.crash.CrashActivity import eu.kanade.tachiyomi.crash.CrashActivity
import eu.kanade.tachiyomi.crash.GlobalExceptionHandler import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
import eu.kanade.tachiyomi.data.coil.MangaKeyer import eu.kanade.tachiyomi.data.coil.MangaKeyer
@@ -208,6 +209,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy)) add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy))
add(MangaKeyer()) add(MangaKeyer())
add(MangaCoverKeyer()) add(MangaCoverKeyer())
add(BufferedSourceFetcher.Factory())
// SY --> // SY -->
add(PagePreviewKeyer()) add(PagePreviewKeyer())
add(PagePreviewFetcher.Factory(callFactoryLazy)) add(PagePreviewFetcher.Factory(callFactoryLazy))
@@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.data.coil
import coil3.ImageLoader
import coil3.decode.DataSource
import coil3.decode.ImageSource
import coil3.fetch.FetchResult
import coil3.fetch.Fetcher
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import okio.BufferedSource
class BufferedSourceFetcher(
private val data: BufferedSource,
private val options: Options,
) : Fetcher {
override suspend fun fetch(): FetchResult {
return SourceFetchResult(
source = ImageSource(
source = data,
fileSystem = options.fileSystem,
),
mimeType = null,
dataSource = DataSource.MEMORY,
)
}
class Factory : Fetcher.Factory<BufferedSource> {
override fun create(
data: BufferedSource,
options: Options,
imageLoader: ImageLoader,
): Fetcher {
return BufferedSourceFetcher(data, options)
}
}
}
@@ -1,15 +1,18 @@
package eu.kanade.tachiyomi.data.coil package eu.kanade.tachiyomi.data.coil
import android.graphics.Bitmap
import android.os.Build import android.os.Build
import androidx.core.graphics.drawable.toDrawable
import coil3.ImageLoader import coil3.ImageLoader
import coil3.asCoilImage import coil3.asCoilImage
import coil3.decode.DecodeResult import coil3.decode.DecodeResult
import coil3.decode.DecodeUtils
import coil3.decode.Decoder import coil3.decode.Decoder
import coil3.decode.ImageSource import coil3.decode.ImageSource
import coil3.fetch.SourceFetchResult import coil3.fetch.SourceFetchResult
import coil3.request.Options import coil3.request.Options
import coil3.request.bitmapConfig
import eu.kanade.tachiyomi.util.storage.CbzCrypto import eu.kanade.tachiyomi.util.storage.CbzCrypto
import eu.kanade.tachiyomi.util.system.GLUtil
import net.lingala.zip4j.ZipFile import net.lingala.zip4j.ZipFile
import net.lingala.zip4j.model.FileHeader import net.lingala.zip4j.model.FileHeader
import okio.BufferedSource import okio.BufferedSource
@@ -38,29 +41,58 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
} }
val decoder = resources.sourceOrNull()?.use { val decoder = resources.sourceOrNull()?.use {
zip4j.use { zipFile -> zip4j.use { zipFile ->
ImageDecoder.newInstance(zipFile?.getInputStream(entry) ?: it.inputStream()) ImageDecoder.newInstance(zipFile?.getInputStream(entry) ?: it.inputStream(), options.cropBorders, displayProfile)
} }
} }
// SY <-- // SY <--
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder" } check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder" }
val bitmap = decoder.decode() val srcWidth = decoder.width
val srcHeight = decoder.height
val dstWidth = options.size.widthPx(options.scale) { srcWidth }
val dstHeight = options.size.heightPx(options.scale) { srcHeight }
val sampleSize = DecodeUtils.calculateInSampleSize(
srcWidth = srcWidth,
srcHeight = srcHeight,
dstWidth = dstWidth,
dstHeight = dstHeight,
scale = options.scale,
)
var bitmap = decoder.decode(sampleSize = sampleSize)
decoder.recycle() decoder.recycle()
check(bitmap != null) { "Failed to decode image" } check(bitmap != null) { "Failed to decode image" }
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
options.bitmapConfig == Bitmap.Config.HARDWARE &&
maxOf(bitmap.width, bitmap.height) <= GLUtil.maxTextureSize
) {
val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false)
if (hwBitmap != null) {
bitmap.recycle()
bitmap = hwBitmap
}
}
return DecodeResult( return DecodeResult(
image = bitmap.asCoilImage(), image = bitmap.asCoilImage(),
isSampled = false, isSampled = sampleSize > 1,
) )
} }
class Factory : Decoder.Factory { class Factory : Decoder.Factory {
override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? { override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
if (!isApplicable(result.source.source())) return null return if (options.customDecoder || isApplicable(result.source.source())) {
return TachiyomiImageDecoder(result.source, options) TachiyomiImageDecoder(result.source, options)
} else {
null
}
} }
private fun isApplicable(source: BufferedSource): Boolean { private fun isApplicable(source: BufferedSource): Boolean {
@@ -83,4 +115,8 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
override fun hashCode() = javaClass.hashCode() override fun hashCode() = javaClass.hashCode()
} }
companion object {
var displayProfile: ByteArray? = null
}
} }
@@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.data.coil
import coil3.Extras
import coil3.getExtra
import coil3.request.ImageRequest
import coil3.request.Options
import coil3.size.Dimension
import coil3.size.Scale
import coil3.size.Size
import coil3.size.isOriginal
import coil3.size.pxOrElse
internal inline fun Size.widthPx(scale: Scale, original: () -> Int): Int {
return if (isOriginal) original() else width.toPx(scale)
}
internal inline fun Size.heightPx(scale: Scale, original: () -> Int): Int {
return if (isOriginal) original() else height.toPx(scale)
}
internal fun Dimension.toPx(scale: Scale): Int = pxOrElse {
when (scale) {
Scale.FILL -> Int.MIN_VALUE
Scale.FIT -> Int.MAX_VALUE
}
}
fun ImageRequest.Builder.cropBorders(enable: Boolean) = apply {
extras[cropBordersKey] = enable
}
val Options.cropBorders: Boolean
get() = getExtra(cropBordersKey)
private val cropBordersKey = Extras.Key(default = false)
fun ImageRequest.Builder.customDecoder(enable: Boolean) = apply {
extras[customDecoderKey] = enable
}
val Options.customDecoder: Boolean
get() = getExtra(customDecoderKey)
private val customDecoderKey = Extras.Key(default = false)
@@ -475,7 +475,7 @@ class DownloadManager(
fun renameMangaDir(oldTitle: String, newTitle: String, source: Long) { fun renameMangaDir(oldTitle: String, newTitle: String, source: Long) {
val sourceDir = provider.findSourceDir(sourceManager.getOrStub(source)) ?: return val sourceDir = provider.findSourceDir(sourceManager.getOrStub(source)) ?: return
val mangaDir = sourceDir.findFile(DiskUtil.buildValidFilename(oldTitle), true) ?: return val mangaDir = sourceDir.findFile(DiskUtil.buildValidFilename(oldTitle)) ?: return
mangaDir.renameTo(DiskUtil.buildValidFilename(newTitle)) mangaDir.renameTo(DiskUtil.buildValidFilename(newTitle))
} }
} }
@@ -57,7 +57,7 @@ class DownloadProvider(
* @param source the source to query. * @param source the source to query.
*/ */
fun findSourceDir(source: Source): UniFile? { fun findSourceDir(source: Source): UniFile? {
return downloadsDir?.findFile(getSourceDirName(source), true) return downloadsDir?.findFile(getSourceDirName(source))
} }
/** /**
@@ -68,7 +68,7 @@ class DownloadProvider(
*/ */
fun findMangaDir(mangaTitle: String, source: Source): UniFile? { fun findMangaDir(mangaTitle: String, source: Source): UniFile? {
val sourceDir = findSourceDir(source) val sourceDir = findSourceDir(source)
return sourceDir?.findFile(getMangaDirName(mangaTitle), true) return sourceDir?.findFile(getMangaDirName(mangaTitle))
} }
/** /**
@@ -82,7 +82,7 @@ class DownloadProvider(
fun findChapterDir(chapterName: String, chapterScanlator: String?, mangaTitle: String, source: Source): UniFile? { fun findChapterDir(chapterName: String, chapterScanlator: String?, mangaTitle: String, source: Source): UniFile? {
val mangaDir = findMangaDir(mangaTitle, source) val mangaDir = findMangaDir(mangaTitle, source)
return getValidChapterDirNames(chapterName, chapterScanlator).asSequence() return getValidChapterDirNames(chapterName, chapterScanlator).asSequence()
.mapNotNull { mangaDir?.findFile(it, true) } .mapNotNull { mangaDir?.findFile(it) }
.firstOrNull() .firstOrNull()
} }
@@ -97,7 +97,7 @@ class DownloadProvider(
val mangaDir = findMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source) ?: return null to emptyList() val mangaDir = findMangaDir(/* SY --> */ manga.ogTitle /* SY <-- */, source) ?: return null to emptyList()
return mangaDir to chapters.mapNotNull { chapter -> return mangaDir to chapters.mapNotNull { chapter ->
getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence() getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence()
.mapNotNull { mangaDir.findFile(it, true) } .mapNotNull { mangaDir.findFile(it) }
.firstOrNull() .firstOrNull()
} }
} }
@@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.util.storage.CbzCrypto
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
import exh.source.isEhBasedSource
import exh.util.DataSaver import exh.util.DataSaver
import exh.util.DataSaver.Companion.getImage import exh.util.DataSaver.Companion.getImage
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@@ -511,6 +512,9 @@ class Downloader(
.retryWhen { _, attempt -> .retryWhen { _, attempt ->
if (attempt < 3) { if (attempt < 3) {
delay((2L shl attempt.toInt()) * 1000) delay((2L shl attempt.toInt()) * 1000)
if (source.isEhBasedSource()) {
page.imageUrl = source.getImageUrl(page)
}
true true
} else { } else {
false false
@@ -709,7 +713,7 @@ class Downloader(
) )
// Remove the old file // Remove the old file
dir.findFile(COMIC_INFO_FILE, true)?.delete() dir.findFile(COMIC_INFO_FILE)?.delete()
dir.createFile(COMIC_INFO_FILE)!!.openOutputStream().use { dir.createFile(COMIC_INFO_FILE)!!.openOutputStream().use {
val comicInfoString = xml.encodeToString(ComicInfo.serializer(), comicInfo) val comicInfoString = xml.encodeToString(ComicInfo.serializer(), comicInfo)
it.write(comicInfoString.toByteArray()) it.write(comicInfoString.toByteArray())
@@ -20,9 +20,8 @@ import exh.source.BlacklistedSources
import exh.source.EH_SOURCE_ID import exh.source.EH_SOURCE_ID
import exh.source.EXH_SOURCE_ID import exh.source.EXH_SOURCE_ID
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
@@ -32,7 +31,6 @@ import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.util.lang.launchNow
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.source.model.StubSource import tachiyomi.domain.source.model.StubSource
@@ -54,10 +52,10 @@ class ExtensionManager(
private val trustExtension: TrustExtension = Injekt.get(), private val trustExtension: TrustExtension = Injekt.get(),
) { ) {
// SY --> val scope = CoroutineScope(SupervisorJob())
private val _isInitialized = MutableStateFlow(false) private val _isInitialized = MutableStateFlow(false)
val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow() val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
// SY <--
/** /**
* API where all the available extensions can be found. * API where all the available extensions can be found.
@@ -71,13 +69,31 @@ class ExtensionManager(
private val iconMap = mutableMapOf<String, Drawable>() private val iconMap = mutableMapOf<String, Drawable>()
private val _installedExtensionsFlow = MutableStateFlow(emptyList<Extension.Installed>()) private val _installedExtensionsMapFlow = MutableStateFlow(emptyMap<String, Extension.Installed>())
val installedExtensionsFlow = _installedExtensionsFlow.asStateFlow() val installedExtensionsFlow = _installedExtensionsMapFlow.mapExtensions(scope)
private val _availableExtensionsMapFlow = MutableStateFlow(emptyMap<String, Extension.Available>())
// SY -->
val availableExtensionsFlow = _availableExtensionsMapFlow.map { it.filterNotBlacklisted().values.toList() }
.stateIn(scope, SharingStarted.Lazily, _availableExtensionsMapFlow.value.values.toList())
// SY <--
private val _untrustedExtensionsMapFlow = MutableStateFlow(emptyMap<String, Extension.Untrusted>())
val untrustedExtensionsFlow = _untrustedExtensionsMapFlow.mapExtensions(scope)
init {
initExtensions()
ExtensionInstallReceiver(InstallationListener()).register(context)
}
private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet() private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet()
fun getAppIconForSource(sourceId: Long): Drawable? { fun getAppIconForSource(sourceId: Long): Drawable? {
val pkgName = _installedExtensionsFlow.value.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName val pkgName = _installedExtensionsMapFlow.value.values
.find { ext ->
ext.sources.any { it.id == sourceId }
}
?.pkgName
if (pkgName != null) { if (pkgName != null) {
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) {
ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo
@@ -95,15 +111,6 @@ class ExtensionManager(
// SY <-- // SY <--
} }
private val _availableExtensionsFlow = MutableStateFlow(emptyList<Extension.Available>())
// SY -->
@OptIn(DelicateCoroutinesApi::class)
val availableExtensionsFlow = _availableExtensionsFlow
.map { it.filterNotBlacklisted() }
.stateIn(GlobalScope, SharingStarted.Eagerly, emptyList())
// SY <--
private var availableExtensionsSourcesData: Map<Long, StubSource> = emptyMap() private var availableExtensionsSourcesData: Map<Long, StubSource> = emptyMap()
private fun setupAvailableExtensionsSourcesDataMap(extensions: List<Extension.Available>) { private fun setupAvailableExtensionsSourcesDataMap(extensions: List<Extension.Available>) {
@@ -115,38 +122,30 @@ class ExtensionManager(
fun getSourceData(id: Long) = availableExtensionsSourcesData[id] fun getSourceData(id: Long) = availableExtensionsSourcesData[id]
private val _untrustedExtensionsFlow = MutableStateFlow(emptyList<Extension.Untrusted>())
val untrustedExtensionsFlow = _untrustedExtensionsFlow.asStateFlow()
init {
initExtensions()
ExtensionInstallReceiver(InstallationListener()).register(context)
}
/** /**
* Loads and registers the installed extensions. * Loads and registers the installed extensions.
*/ */
private fun initExtensions() { private fun initExtensions() {
val extensions = ExtensionLoader.loadExtensions(context) val extensions = ExtensionLoader.loadExtensions(context)
_installedExtensionsFlow.value = extensions _installedExtensionsMapFlow.value = extensions
.filterIsInstance<LoadResult.Success>() .filterIsInstance<LoadResult.Success>()
.map { it.extension } .associate { it.extension.pkgName to it.extension }
_untrustedExtensionsFlow.value = extensions _untrustedExtensionsMapFlow.value = extensions
.filterIsInstance<LoadResult.Untrusted>() .filterIsInstance<LoadResult.Untrusted>()
.map { it.extension } .associate { it.extension.pkgName to it.extension }
// SY --> // SY -->
.filterNotBlacklisted() .filterNotBlacklisted()
// SY <--
_isInitialized.value = true _isInitialized.value = true
// SY <--
} }
// EXH --> // EXH -->
private fun <T : Extension> Iterable<T>.filterNotBlacklisted(): List<T> { private fun <T : Extension> Map<String, T>.filterNotBlacklisted(): Map<String, T> {
val blacklistEnabled = preferences.enableSourceBlacklist().get() val blacklistEnabled = preferences.enableSourceBlacklist().get()
return filterNot { extension -> return filterNot { (_, extension) ->
extension.isBlacklisted(blacklistEnabled) extension.isBlacklisted(blacklistEnabled)
.also { .also {
if (it) this@ExtensionManager.xLogD("Removing blacklisted extension: (name: %s, pkgName: %s)!", extension.name, extension.pkgName) if (it) this@ExtensionManager.xLogD("Removing blacklisted extension: (name: %s, pkgName: %s)!", extension.name, extension.pkgName)
@@ -160,7 +159,7 @@ class ExtensionManager(
// EXH <-- // EXH <--
/** /**
* Finds the available extensions in the [api] and updates [availableExtensions]. * Finds the available extensions in the [api] and updates [_availableExtensionsMapFlow].
*/ */
suspend fun findAvailableExtensions() { suspend fun findAvailableExtensions() {
val extensions: List<Extension.Available> = try { val extensions: List<Extension.Available> = try {
@@ -173,7 +172,7 @@ class ExtensionManager(
enableAdditionalSubLanguages(extensions) enableAdditionalSubLanguages(extensions)
_availableExtensionsFlow.value = extensions _availableExtensionsMapFlow.value = extensions.associateBy { it.pkgName }
updatedInstalledExtensionsStatuses(extensions) updatedInstalledExtensionsStatuses(extensions)
setupAvailableExtensionsSourcesDataMap(extensions) setupAvailableExtensionsSourcesDataMap(extensions)
} }
@@ -219,42 +218,36 @@ class ExtensionManager(
return return
} }
val mutInstalledExtensions = _installedExtensionsFlow.value.toMutableList() val installedExtensionsMap = _installedExtensionsMapFlow.value.toMutableMap()
var changed = false var changed = false
for ((pkgName, extension) in installedExtensionsMap) {
val availableExt = availableExtensions.find { it.pkgName == pkgName }
for ((index, installedExt) in mutInstalledExtensions.withIndex()) { if (availableExt == null && !extension.isObsolete) {
val pkgName = installedExt.pkgName installedExtensionsMap[pkgName] = extension.copy(isObsolete = true)
// SY -->
val availableExt = _availableExtensionsFlow.value.find { it.pkgName == pkgName }
// SY <--
if (availableExt == null && !installedExt.isObsolete) {
mutInstalledExtensions[index] = installedExt.copy(isObsolete = true)
changed = true changed = true
// SY --> // SY -->
} else if (installedExt.isBlacklisted() && !installedExt.isRedundant) { } else if (extension.isBlacklisted() && !extension.isRedundant) {
mutInstalledExtensions[index] = installedExt.copy(isRedundant = true) installedExtensionsMap[pkgName] = extension.copy(isRedundant = true)
changed = true changed = true
// SY <-- // SY <--
} else if (availableExt != null) { } else if (availableExt != null) {
val hasUpdate = installedExt.updateExists(availableExt) val hasUpdate = extension.updateExists(availableExt)
if (extension.hasUpdate != hasUpdate) {
if (installedExt.hasUpdate != hasUpdate) { installedExtensionsMap[pkgName] = extension.copy(
mutInstalledExtensions[index] = installedExt.copy(
hasUpdate = hasUpdate, hasUpdate = hasUpdate,
repoUrl = availableExt.repoUrl, repoUrl = availableExt.repoUrl,
) )
changed = true
} else { } else {
mutInstalledExtensions[index] = installedExt.copy( installedExtensionsMap[pkgName] = extension.copy(
repoUrl = availableExt.repoUrl, repoUrl = availableExt.repoUrl,
) )
changed = true
} }
changed = true
} }
} }
if (changed) { if (changed) {
_installedExtensionsFlow.value = mutInstalledExtensions _installedExtensionsMapFlow.value = installedExtensionsMap
} }
updatePendingUpdatesCount() updatePendingUpdatesCount()
} }
@@ -278,8 +271,7 @@ class ExtensionManager(
* @param extension The extension to be updated. * @param extension The extension to be updated.
*/ */
fun updateExtension(extension: Extension.Installed): Flow<InstallStep> { fun updateExtension(extension: Extension.Installed): Flow<InstallStep> {
val availableExt = _availableExtensionsFlow.value.find { it.pkgName == extension.pkgName } val availableExt = _availableExtensionsMapFlow.value[extension.pkgName] ?: return emptyFlow()
?: return emptyFlow()
return installExtension(availableExt) return installExtension(availableExt)
} }
@@ -315,24 +307,16 @@ class ExtensionManager(
* *
* @param extension the extension to trust * @param extension the extension to trust
*/ */
fun trust(extension: Extension.Untrusted) { suspend fun trust(extension: Extension.Untrusted) {
val untrustedPkgNames = _untrustedExtensionsFlow.value.map { it.pkgName }.toSet() _untrustedExtensionsMapFlow.value[extension.pkgName] ?: return
if (extension.pkgName !in untrustedPkgNames) return
trustExtension.trust(extension.pkgName, extension.versionCode, extension.signatureHash) trustExtension.trust(extension.pkgName, extension.versionCode, extension.signatureHash)
val nowTrustedExtensions = _untrustedExtensionsFlow.value _untrustedExtensionsMapFlow.value -= extension.pkgName
.filter { it.pkgName == extension.pkgName && it.versionCode == extension.versionCode }
_untrustedExtensionsFlow.value -= nowTrustedExtensions
launchNow { ExtensionLoader.loadExtensionFromPkgName(context, extension.pkgName)
nowTrustedExtensions .let { it as? LoadResult.Success }
.map { extension -> ?.let { registerNewExtension(it.extension) }
async { ExtensionLoader.loadExtensionFromPkgName(context, extension.pkgName) }.await()
}
.filterIsInstance<LoadResult.Success>()
.forEach { registerNewExtension(it.extension) }
}
} }
/** /**
@@ -348,7 +332,7 @@ class ExtensionManager(
} }
// SY <-- // SY <--
_installedExtensionsFlow.value += extension _installedExtensionsMapFlow.value += extension
} }
/** /**
@@ -365,13 +349,7 @@ class ExtensionManager(
} }
// SY <-- // SY <--
val mutInstalledExtensions = _installedExtensionsFlow.value.toMutableList() _installedExtensionsMapFlow.value += extension
val oldExtension = mutInstalledExtensions.find { it.pkgName == extension.pkgName }
if (oldExtension != null) {
mutInstalledExtensions -= oldExtension
}
mutInstalledExtensions += extension
_installedExtensionsFlow.value = mutInstalledExtensions
} }
/** /**
@@ -381,14 +359,8 @@ class ExtensionManager(
* @param pkgName The package name of the uninstalled application. * @param pkgName The package name of the uninstalled application.
*/ */
private fun unregisterExtension(pkgName: String) { private fun unregisterExtension(pkgName: String) {
val installedExtension = _installedExtensionsFlow.value.find { it.pkgName == pkgName } _installedExtensionsMapFlow.value -= pkgName
if (installedExtension != null) { _untrustedExtensionsMapFlow.value -= pkgName
_installedExtensionsFlow.value -= installedExtension
}
val untrustedExtension = _untrustedExtensionsFlow.value.find { it.pkgName == pkgName }
if (untrustedExtension != null) {
_untrustedExtensionsFlow.value -= untrustedExtension
}
} }
/** /**
@@ -407,14 +379,9 @@ class ExtensionManager(
} }
override fun onExtensionUntrusted(extension: Extension.Untrusted) { override fun onExtensionUntrusted(extension: Extension.Untrusted) {
val installedExtension = _installedExtensionsFlow.value _installedExtensionsMapFlow.value -= extension.pkgName
.find { it.pkgName == extension.pkgName } _untrustedExtensionsMapFlow.value += extension
updatePendingUpdatesCount()
if (installedExtension != null) {
_installedExtensionsFlow.value -= installedExtension
} else {
_untrustedExtensionsFlow.value += extension
}
} }
override fun onPackageUninstalled(pkgName: String) { override fun onPackageUninstalled(pkgName: String) {
@@ -436,17 +403,24 @@ class ExtensionManager(
} }
private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean { private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean {
val availableExt = availableExtension ?: _availableExtensionsFlow.value.find { it.pkgName == pkgName } val availableExt = availableExtension
?: _availableExtensionsMapFlow.value[pkgName]
?: return false ?: return false
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion) return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
} }
private fun updatePendingUpdatesCount() { private fun updatePendingUpdatesCount() {
val pendingUpdateCount = _installedExtensionsFlow.value.count { it.hasUpdate } val pendingUpdateCount = _installedExtensionsMapFlow.value.values.count { it.hasUpdate }
preferences.extensionUpdatesCount().set(pendingUpdateCount) preferences.extensionUpdatesCount().set(pendingUpdateCount)
if (pendingUpdateCount == 0) { if (pendingUpdateCount == 0) {
ExtensionUpdateNotifier(context).dismiss() ExtensionUpdateNotifier(context).dismiss()
} }
} }
private operator fun <T : Extension> Map<String, T>.plus(extension: T) = plus(extension.pkgName to extension)
private fun <T : Extension> StateFlow<Map<String, T>>.mapExtensions(scope: CoroutineScope): StateFlow<List<T>> {
return map { it.values.toList() }.stateIn(scope, SharingStarted.Lazily, value.values.toList())
}
} }
@@ -9,12 +9,10 @@ import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.model.LoadResult
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch
import kotlinx.coroutines.async
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.util.lang.launchNow
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
/** /**
@@ -23,29 +21,23 @@ import tachiyomi.core.common.util.system.logcat
* *
* @param listener The listener that should be notified of extension installation events. * @param listener The listener that should be notified of extension installation events.
*/ */
internal class ExtensionInstallReceiver(private val listener: Listener) : internal class ExtensionInstallReceiver(private val listener: Listener) : BroadcastReceiver() {
BroadcastReceiver() {
val scope = CoroutineScope(SupervisorJob())
/**
* Registers this broadcast receiver
*/
fun register(context: Context) { fun register(context: Context) {
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_NOT_EXPORTED) ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
} }
/** private val filter = IntentFilter().apply {
* Returns the intent filter this receiver should subscribe to. addAction(Intent.ACTION_PACKAGE_ADDED)
*/ addAction(Intent.ACTION_PACKAGE_REPLACED)
private val filter addAction(Intent.ACTION_PACKAGE_REMOVED)
get() = IntentFilter().apply { addAction(ACTION_EXTENSION_ADDED)
addAction(Intent.ACTION_PACKAGE_ADDED) addAction(ACTION_EXTENSION_REPLACED)
addAction(Intent.ACTION_PACKAGE_REPLACED) addAction(ACTION_EXTENSION_REMOVED)
addAction(Intent.ACTION_PACKAGE_REMOVED) addDataScheme("package")
addAction(ACTION_EXTENSION_ADDED) }
addAction(ACTION_EXTENSION_REPLACED)
addAction(ACTION_EXTENSION_REMOVED)
addDataScheme("package")
}
/** /**
* Called when one of the events of the [filter] is received. When the package is an extension, * Called when one of the events of the [filter] is received. When the package is an extension,
@@ -58,7 +50,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
Intent.ACTION_PACKAGE_ADDED, ACTION_EXTENSION_ADDED -> { Intent.ACTION_PACKAGE_ADDED, ACTION_EXTENSION_ADDED -> {
if (isReplacing(intent)) return if (isReplacing(intent)) return
launchNow { scope.launch {
when (val result = getExtensionFromIntent(context, intent)) { when (val result = getExtensionFromIntent(context, intent)) {
is LoadResult.Success -> listener.onExtensionInstalled(result.extension) is LoadResult.Success -> listener.onExtensionInstalled(result.extension)
is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension) is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
@@ -67,7 +59,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
} }
} }
Intent.ACTION_PACKAGE_REPLACED, ACTION_EXTENSION_REPLACED -> { Intent.ACTION_PACKAGE_REPLACED, ACTION_EXTENSION_REPLACED -> {
launchNow { scope.launch {
when (val result = getExtensionFromIntent(context, intent)) { when (val result = getExtensionFromIntent(context, intent)) {
is LoadResult.Success -> listener.onExtensionUpdated(result.extension) is LoadResult.Success -> listener.onExtensionUpdated(result.extension)
is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension) is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
@@ -107,9 +99,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
logcat(LogPriority.WARN) { "Package name not found" } logcat(LogPriority.WARN) { "Package name not found" }
return LoadResult.Error return LoadResult.Error
} }
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) { return ExtensionLoader.loadExtensionFromPkgName(context, pkgName)
ExtensionLoader.loadExtensionFromPkgName(context, pkgName)
}.await()
} }
/** /**
@@ -172,7 +172,7 @@ internal object ExtensionLoader {
* Attempts to load an extension from the given package name. It checks if the extension * Attempts to load an extension from the given package name. It checks if the extension
* contains the required feature flag before trying to load it. * contains the required feature flag before trying to load it.
*/ */
fun loadExtensionFromPkgName(context: Context, pkgName: String): LoadResult { suspend fun loadExtensionFromPkgName(context: Context, pkgName: String): LoadResult {
val extensionPackage = getExtensionInfoFromPkgName(context, pkgName) val extensionPackage = getExtensionInfoFromPkgName(context, pkgName)
if (extensionPackage == null) { if (extensionPackage == null) {
logcat(LogPriority.ERROR) { "Extension package is not found ($pkgName)" } logcat(LogPriority.ERROR) { "Extension package is not found ($pkgName)" }
@@ -223,7 +223,8 @@ internal object ExtensionLoader {
* @param context The application context. * @param context The application context.
* @param extensionInfo The extension to load. * @param extensionInfo The extension to load.
*/ */
private fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult { @Suppress("LongMethod", "CyclomaticComplexMethod", "ReturnCount")
private suspend fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult {
val pkgManager = context.packageManager val pkgManager = context.packageManager
val pkgInfo = extensionInfo.packageInfo val pkgInfo = extensionInfo.packageInfo
val appInfo = pkgInfo.applicationInfo val appInfo = pkgInfo.applicationInfo
@@ -252,7 +253,7 @@ internal object ExtensionLoader {
if (signatures.isNullOrEmpty()) { if (signatures.isNullOrEmpty()) {
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" } logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
return LoadResult.Error return LoadResult.Error
} else if (!trustExtension.isTrusted(pkgInfo, signatures.last())) { } else if (!trustExtension.isTrusted(pkgInfo, signatures)) {
val extension = Extension.Untrusted( val extension = Extension.Untrusted(
extName, extName,
pkgName, pkgName,
@@ -806,6 +806,11 @@ class EHentai(
override fun pageListParse(response: Response) = override fun pageListParse(response: Response) =
throw UnsupportedOperationException("Unused method was called somehow!") throw UnsupportedOperationException("Unused method was called somehow!")
override suspend fun getImageUrl(page: Page): String {
val imageUrlResponse = client.newCall(imageUrlRequest(page)).awaitSuccess()
return realImageUrlParse(imageUrlResponse, page)
}
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl")) @Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl"))
override fun fetchImageUrl(page: Page): Observable<String> { override fun fetchImageUrl(page: Page): Observable<String> {
return client.newCall(imageUrlRequest(page)) return client.newCall(imageUrlRequest(page))
@@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@@ -195,7 +196,9 @@ class ExtensionsScreenModel(
} }
fun trustExtension(extension: Extension.Untrusted) { fun trustExtension(extension: Extension.Untrusted) {
extensionManager.trust(extension) screenModelScope.launch {
extensionManager.trust(extension)
}
} }
@Immutable @Immutable
@@ -234,6 +234,9 @@ class LibraryScreenModel(
prefs.filterBookmarked, prefs.filterBookmarked,
prefs.filterCompleted, prefs.filterCompleted,
prefs.filterIntervalCustom, prefs.filterIntervalCustom,
// SY -->
prefs.filterLewd,
// SY <--
) + trackFilter.values ) + trackFilter.values
).any { it != TriState.DISABLED } ).any { it != TriState.DISABLED }
} }
@@ -62,6 +62,7 @@ import eu.kanade.presentation.reader.appbars.NavBarType
import eu.kanade.presentation.reader.appbars.ReaderAppBars import eu.kanade.presentation.reader.appbars.ReaderAppBars
import eu.kanade.presentation.reader.settings.ReaderSettingsDialog import eu.kanade.presentation.reader.settings.ReaderSettingsDialog
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
@@ -1280,7 +1281,9 @@ class ReaderActivity : BaseActivity() {
input.copyTo(output) input.copyTo(output)
} }
} }
SubsamplingScaleImageView.setDisplayProfile(outputStream.toByteArray()) val data = outputStream.toByteArray()
SubsamplingScaleImageView.setDisplayProfile(data)
TachiyomiImageDecoder.displayProfile = data
} }
} }
@@ -1141,7 +1141,7 @@ class ReaderViewModel @JvmOverloads constructor(
return imageSaver.save( return imageSaver.save(
image = Image.Page( image = Image.Page(
inputStream = { ImageUtil.mergeBitmaps(imageBitmap, imageBitmap2, isLTR, 0, bg) }, inputStream = { ImageUtil.mergeBitmaps(imageBitmap, imageBitmap2, isLTR, 0, bg).inputStream() },
name = filename, name = filename,
location = location, location = location,
), ),
@@ -43,7 +43,9 @@ internal class ZipPageLoader(file: UniFile, context: Context) : PageLoader() {
} }
private val apacheZip: ZipFile? = if (!file.isEncryptedZip() && Build.VERSION.SDK_INT > Build.VERSION_CODES.N) { private val apacheZip: ZipFile? = if (!file.isEncryptedZip() && Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
ZipFile(channel) ZipFile.Builder()
.setSeekableByteChannel(channel)
.get()
} else { } else {
null null
} }
@@ -18,23 +18,27 @@ import androidx.annotation.StyleRes
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
import androidx.core.os.postDelayed import androidx.core.os.postDelayed
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil3.BitmapImage
import coil3.dispose import coil3.dispose
import coil3.imageLoader import coil3.imageLoader
import coil3.request.CachePolicy import coil3.request.CachePolicy
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.crossfade import coil3.request.crossfade
import coil3.size.Precision
import coil3.size.ViewSizeResolver
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_IN_OUT_QUAD import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_IN_OUT_QUAD
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_OUT_QUAD import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_OUT_QUAD
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
import com.github.chrisbanes.photoview.PhotoView import com.github.chrisbanes.photoview.PhotoView
import eu.kanade.tachiyomi.data.coil.cropBorders
import eu.kanade.tachiyomi.data.coil.customDecoder
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView
import eu.kanade.tachiyomi.util.system.GLUtil import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.view.isVisibleOnScreen import eu.kanade.tachiyomi.util.view.isVisibleOnScreen
import java.io.InputStream import okio.BufferedSource
import java.nio.ByteBuffer
/** /**
* A wrapper view for showing page image. * A wrapper view for showing page image.
@@ -140,14 +144,14 @@ open class ReaderPageImageView @JvmOverloads constructor(
} }
} }
fun setImage(inputStream: InputStream, isAnimated: Boolean, config: Config) { fun setImage(source: BufferedSource, isAnimated: Boolean, config: Config) {
this.config = config this.config = config
if (isAnimated) { if (isAnimated) {
prepareAnimatedImageView() prepareAnimatedImageView()
setAnimatedImage(inputStream, config) setAnimatedImage(source, config)
} else { } else {
prepareNonAnimatedImageView() prepareNonAnimatedImageView()
setNonAnimatedImage(inputStream, config) setNonAnimatedImage(source, config)
} }
} }
@@ -256,7 +260,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
} }
private fun setNonAnimatedImage( private fun setNonAnimatedImage(
image: Any, data: Any,
config: Config, config: Config,
) = (pageView as? SubsamplingScaleImageView)?.apply { ) = (pageView as? SubsamplingScaleImageView)?.apply {
setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration()) setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration())
@@ -277,12 +281,36 @@ open class ReaderPageImageView @JvmOverloads constructor(
}, },
) )
when (image) { if (isWebtoon) {
is BitmapDrawable -> setImage(ImageSource.bitmap(image.bitmap)) val request = ImageRequest.Builder(context)
is InputStream -> setImage(ImageSource.inputStream(image)) .data(data)
else -> throw IllegalArgumentException("Not implemented for class ${image::class.simpleName}") .memoryCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.DISABLED)
.target(
onSuccess = { result ->
val image = result as BitmapImage
setImage(ImageSource.bitmap(image.bitmap))
isVisible = true
},
onError = {
this@ReaderPageImageView.onImageLoadError()
},
)
.size(ViewSizeResolver(this@ReaderPageImageView))
.precision(Precision.INEXACT)
.cropBorders(config.cropBorders)
.customDecoder(true)
.crossfade(false)
.build()
context.imageLoader.enqueue(request)
} else {
when (data) {
is BitmapDrawable -> setImage(ImageSource.bitmap(data.bitmap))
is BufferedSource -> setImage(ImageSource.inputStream(data.inputStream()))
else -> throw IllegalArgumentException("Not implemented for class ${data::class.simpleName}")
}
isVisible = true
} }
isVisible = true
} }
private fun prepareAnimatedImageView() { private fun prepareAnimatedImageView() {
@@ -325,18 +353,13 @@ open class ReaderPageImageView @JvmOverloads constructor(
} }
private fun setAnimatedImage( private fun setAnimatedImage(
image: Any, data: Any,
config: Config, config: Config,
) = (pageView as? AppCompatImageView)?.apply { ) = (pageView as? AppCompatImageView)?.apply {
if (this is PhotoView) { if (this is PhotoView) {
setZoomTransitionDuration(config.zoomDuration.getSystemScaledDuration()) setZoomTransitionDuration(config.zoomDuration.getSystemScaledDuration())
} }
val data = when (image) {
is Drawable -> image
is InputStream -> ByteBuffer.wrap(image.readBytes())
else -> throw IllegalArgumentException("Not implemented for class ${image::class.simpleName}")
}
val request = ImageRequest.Builder(context) val request = ImageRequest.Builder(context)
.data(data) .data(data)
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)
@@ -19,15 +19,14 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import logcat.LogPriority import logcat.LogPriority
import okio.Buffer
import okio.BufferedSource
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.decoder.ImageDecoder import tachiyomi.decoder.ImageDecoder
import java.io.BufferedInputStream
import java.io.ByteArrayInputStream
import java.io.InputStream
import kotlin.math.max import kotlin.math.max
/** /**
@@ -159,53 +158,45 @@ class PagerPageHolder(
val streamFn2 = extraPage?.stream val streamFn2 = extraPage?.stream
try { try {
val (bais, isAnimated, background) = withIOContext { val (source, isAnimated, background) = withIOContext {
streamFn().buffered(16).use { stream -> streamFn().buffered(16).use { source ->
// SY --> // SY -->
( if (extraPage != null) {
if (extraPage != null) { streamFn2?.invoke()
streamFn2?.invoke() ?.buffered(16)
?.buffered(16) } else {
null
}.use { source2 ->
val itemSource = if (viewer.config.dualPageSplit) {
process(item.first, Buffer().readFrom(source))
} else {
mergePages(Buffer().readFrom(source), source2?.let { Buffer().readFrom(it) })
}
// SY <--
val isAnimated = ImageUtil.isAnimatedAndSupported(itemSource)
val background = if (!isAnimated && viewer.config.automaticBackground) {
ImageUtil.chooseBackground(context, itemSource.peek())
} else { } else {
null null
} }
).use { stream2 -> Triple(itemSource, isAnimated, background)
if (viewer.config.dualPageSplit) { }
process(item.first, stream)
} else {
mergePages(stream, stream2)
}.use { itemStream ->
// SY <--
val bais = ByteArrayInputStream(itemStream.readBytes())
val isAnimated = ImageUtil.isAnimatedAndSupported(bais)
bais.reset()
val background = if (!isAnimated && viewer.config.automaticBackground) {
ImageUtil.chooseBackground(context, bais)
} else {
null
}
bais.reset()
Triple(bais, isAnimated, background)
}
}
} }
} }
withUIContext { withUIContext {
bais.use { setImage(
setImage( source,
it, isAnimated,
isAnimated, Config(
Config( zoomDuration = viewer.config.doubleTapAnimDuration,
zoomDuration = viewer.config.doubleTapAnimDuration, minimumScaleType = viewer.config.imageScaleType,
minimumScaleType = viewer.config.imageScaleType, cropBorders = viewer.config.imageCropBorders,
cropBorders = viewer.config.imageCropBorders, zoomStartPosition = viewer.config.imageZoomType,
zoomStartPosition = viewer.config.imageZoomType, landscapeZoom = viewer.config.landscapeZoom,
landscapeZoom = viewer.config.landscapeZoom, ),
), )
) if (!isAnimated) {
if (!isAnimated) { pageBackground = background
pageBackground = background
}
} }
removeErrorLayout() removeErrorLayout()
} }
@@ -217,124 +208,119 @@ class PagerPageHolder(
} }
} }
private fun process(page: ReaderPage, imageStream: BufferedInputStream): InputStream { private fun process(page: ReaderPage, imageSource: BufferedSource): BufferedSource {
if (viewer.config.dualPageRotateToFit) { if (viewer.config.dualPageRotateToFit) {
return rotateDualPage(imageStream) return rotateDualPage(imageSource)
} }
if (!viewer.config.dualPageSplit) { if (!viewer.config.dualPageSplit) {
return imageStream return imageSource
} }
if (page is InsertPage) { if (page is InsertPage) {
return splitInHalf(imageStream) return splitInHalf(imageSource)
} }
val isDoublePage = ImageUtil.isWideImage(imageStream) val isDoublePage = ImageUtil.isWideImage(imageSource)
if (!isDoublePage) { if (!isDoublePage) {
return imageStream return imageSource
} }
onPageSplit(page) onPageSplit(page)
return splitInHalf(imageStream) return splitInHalf(imageSource)
} }
private fun rotateDualPage(imageStream: BufferedInputStream): InputStream { private fun rotateDualPage(imageSource: BufferedSource): BufferedSource {
val isDoublePage = ImageUtil.isWideImage(imageStream) val isDoublePage = ImageUtil.isWideImage(imageSource)
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(imageSource, rotation)
} else { } else {
imageStream imageSource
} }
} }
private fun mergePages(imageStream: InputStream, imageStream2: InputStream?): InputStream { private fun mergePages(imageSource: BufferedSource, imageSource2: BufferedSource?): BufferedSource {
// Handle adding a center margin to wide images if requested // Handle adding a center margin to wide images if requested
if (imageStream2 == null) { if (imageSource2 == null) {
return if (imageStream is BufferedInputStream && return if (
!ImageUtil.isAnimatedAndSupported(imageStream) && !ImageUtil.isAnimatedAndSupported(imageSource) &&
ImageUtil.isWideImage(imageStream) && ImageUtil.isWideImage(imageSource) &&
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
) { ) {
ImageUtil.addHorizontalCenterMargin(imageStream, height, context) ImageUtil.addHorizontalCenterMargin(imageSource, height, context)
} else { } else {
imageStream imageSource
} }
} }
if (page.fullPage) return imageStream if (page.fullPage) return imageSource
if (ImageUtil.isAnimatedAndSupported(imageStream)) { if (ImageUtil.isAnimatedAndSupported(imageSource)) {
page.fullPage = true page.fullPage = true
splitDoublePages() splitDoublePages()
return imageStream return imageSource
} else if (ImageUtil.isAnimatedAndSupported(imageStream2)) { } else if (ImageUtil.isAnimatedAndSupported(imageSource2)) {
page.isolatedPage = true page.isolatedPage = true
extraPage?.fullPage = true extraPage?.fullPage = true
splitDoublePages() splitDoublePages()
return imageStream return imageSource
} }
val imageBytes = imageStream.readBytes()
val imageBitmap = try { val imageBitmap = try {
ImageDecoder.newInstance(imageBytes.inputStream())?.decode() ImageDecoder.newInstance(imageSource.inputStream())?.decode()
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Cannot combine pages" } logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
null null
} }
if (imageBitmap == null) { if (imageBitmap == null) {
imageStream2.close() imageSource2.close()
imageStream.close()
page.fullPage = true page.fullPage = true
splitDoublePages() splitDoublePages()
logcat(LogPriority.ERROR) { "Cannot combine pages" } logcat(LogPriority.ERROR) { "Cannot combine pages" }
return imageBytes.inputStream() return imageSource
} }
scope.launch { progressIndicator.setProgress(96) } scope.launch { progressIndicator.setProgress(96) }
val height = imageBitmap.height val height = imageBitmap.height
val width = imageBitmap.width val width = imageBitmap.width
if (height < width) { if (height < width) {
imageStream2.close() imageSource2.close()
imageStream.close()
page.fullPage = true page.fullPage = true
splitDoublePages() splitDoublePages()
return imageBytes.inputStream() return imageSource
} }
val imageBytes2 = imageStream2.readBytes()
val imageBitmap2 = try { val imageBitmap2 = try {
ImageDecoder.newInstance(imageBytes2.inputStream())?.decode() ImageDecoder.newInstance(imageSource2.inputStream())?.decode()
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Cannot combine pages" } logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
null null
} }
if (imageBitmap2 == null) { if (imageBitmap2 == null) {
imageStream2.close() imageSource2.close()
imageStream.close()
extraPage?.fullPage = true extraPage?.fullPage = true
page.isolatedPage = true page.isolatedPage = true
splitDoublePages() splitDoublePages()
logcat(LogPriority.ERROR) { "Cannot combine pages" } logcat(LogPriority.ERROR) { "Cannot combine pages" }
return imageBytes.inputStream() return imageSource
} }
scope.launch { progressIndicator.setProgress(97) } scope.launch { progressIndicator.setProgress(97) }
val height2 = imageBitmap2.height val height2 = imageBitmap2.height
val width2 = imageBitmap2.width val width2 = imageBitmap2.width
if (height2 < width2) { if (height2 < width2) {
imageStream2.close() imageSource2.close()
imageStream.close()
extraPage?.fullPage = true extraPage?.fullPage = true
page.isolatedPage = true page.isolatedPage = true
splitDoublePages() splitDoublePages()
return imageBytes.inputStream() return imageSource
} }
val isLTR = (viewer !is R2LPagerViewer) xor viewer.config.invertDoublePages val isLTR = (viewer !is R2LPagerViewer) xor viewer.config.invertDoublePages
imageStream.close() imageSource.close()
imageStream2.close() imageSource2.close()
val centerMargin = if (viewer.config.centerMarginType and PagerConfig.CenterMarginType.DOUBLE_PAGE_CENTER_MARGIN > 0 && !viewer.config.imageCropBorders) { val centerMargin = if (viewer.config.centerMarginType and PagerConfig.CenterMarginType.DOUBLE_PAGE_CENTER_MARGIN > 0 && !viewer.config.imageCropBorders) {
96 / (this.height.coerceAtLeast(1) / max(height, height2).coerceAtLeast(1)).coerceAtLeast(1) 96 / (this.height.coerceAtLeast(1) / max(height, height2).coerceAtLeast(1)).coerceAtLeast(1)
@@ -363,7 +349,7 @@ class PagerPageHolder(
} }
} }
private fun splitInHalf(imageStream: InputStream): InputStream { private fun splitInHalf(imageSource: BufferedSource): BufferedSource {
var side = when { var side = when {
viewer is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.RIGHT viewer is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.RIGHT
viewer !is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.LEFT viewer !is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.LEFT
@@ -387,7 +373,7 @@ class PagerPageHolder(
0 0
} }
return ImageUtil.splitInHalf(imageStream, side, sideMargin) return ImageUtil.splitInHalf(imageSource, side, sideMargin)
} }
private fun onPageSplit(page: ReaderPage) { private fun onPageSplit(page: ReaderPage) {
@@ -22,15 +22,14 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.suspendCancellableCoroutine
import logcat.LogPriority import logcat.LogPriority
import okio.Buffer
import okio.BufferedSource
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import java.io.BufferedInputStream
import java.io.InputStream
/** /**
* Holder of the webtoon reader for a single page of a chapter. * Holder of the webtoon reader for a single page of a chapter.
@@ -188,16 +187,14 @@ class WebtoonPageHolder(
val streamFn = page?.stream ?: return val streamFn = page?.stream ?: return
try { try {
val (openStream, isAnimated) = withIOContext { val (source, isAnimated) = withIOContext {
val stream = streamFn().buffered(16) val source = streamFn().use { process(Buffer().readFrom(it)) }
val openStream = process(stream) val isAnimated = ImageUtil.isAnimatedAndSupported(source)
Pair(source, isAnimated)
val isAnimated = ImageUtil.isAnimatedAndSupported(stream)
Pair(openStream, isAnimated)
} }
withUIContext { withUIContext {
frame.setImage( frame.setImage(
openStream, source,
isAnimated, isAnimated,
ReaderPageImageView.Config( ReaderPageImageView.Config(
zoomDuration = viewer.config.doubleTapAnimDuration, zoomDuration = viewer.config.doubleTapAnimDuration,
@@ -207,10 +204,6 @@ class WebtoonPageHolder(
) )
removeErrorLayout() removeErrorLayout()
} }
// Suspend the coroutine to close the input stream only when the WebtoonPageHolder is recycled
suspendCancellableCoroutine<Nothing> { continuation ->
continuation.invokeOnCancellation { openStream.close() }
}
} catch (e: Throwable) { } catch (e: Throwable) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
withUIContext { withUIContext {
@@ -219,29 +212,29 @@ class WebtoonPageHolder(
} }
} }
private fun process(imageStream: BufferedInputStream): InputStream { private fun process(imageSource: BufferedSource): BufferedSource {
if (viewer.config.dualPageRotateToFit) { if (viewer.config.dualPageRotateToFit) {
return rotateDualPage(imageStream) return rotateDualPage(imageSource)
} }
if (viewer.config.dualPageSplit) { if (viewer.config.dualPageSplit) {
val isDoublePage = ImageUtil.isWideImage(imageStream) val isDoublePage = ImageUtil.isWideImage(imageSource)
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(imageSource, upperSide)
} }
} }
return imageStream return imageSource
} }
private fun rotateDualPage(imageStream: BufferedInputStream): InputStream { private fun rotateDualPage(imageSource: BufferedSource): BufferedSource {
val isDoublePage = ImageUtil.isWideImage(imageStream) val isDoublePage = ImageUtil.isWideImage(imageSource)
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(imageSource, rotation)
} else { } else {
imageStream imageSource
} }
} }
@@ -20,12 +20,13 @@ class CrashLogUtil(
private val extensionManager: ExtensionManager = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(),
) { ) {
suspend fun dumpLogs() = withNonCancellableContext { suspend fun dumpLogs(exception: Throwable? = null) = withNonCancellableContext {
try { try {
val file = context.createFileInCacheDir("tachiyomi_crash_logs.txt") val file = context.createFileInCacheDir("tachiyomi_crash_logs.txt")
file.appendText(getDebugInfo() + "\n\n") file.appendText(getDebugInfo() + "\n\n")
getExtensionsInfo()?.let { file.appendText("$it\n\n") } getExtensionsInfo()?.let { file.appendText("$it\n\n") }
exception?.let { file.appendText("$it\n\n") }
Runtime.getRuntime().exec("logcat *:E -d -f ${file.absolutePath}").waitFor() Runtime.getRuntime().exec("logcat *:E -d -f ${file.absolutePath}").waitFor()
@@ -17,10 +17,13 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okio.BufferedSource
import okio.buffer
import okio.sink
import okio.source
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.InputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
import kotlin.concurrent.thread import kotlin.concurrent.thread
@@ -67,7 +70,7 @@ class MemAutoFlushingLookupTable<T>(
Runtime.getRuntime().addShutdownHook(shutdownHook) Runtime.getRuntime().addShutdownHook(shutdownHook)
} }
private fun InputStream.requireBytes(targetArray: ByteArray, byteCount: Int): Boolean { private fun BufferedSource.requireBytes(targetArray: ByteArray, byteCount: Int): Boolean {
var readIter = 0 var readIter = 0
while (true) { while (true) {
val readThisIter = read(targetArray, readIter, byteCount - readIter) val readThisIter = read(targetArray, readIter, byteCount - readIter)
@@ -80,7 +83,7 @@ class MemAutoFlushingLookupTable<T>(
private fun initialLoad() { private fun initialLoad() {
launch { launch {
try { try {
atomicFile.openRead().buffered().use { input -> atomicFile.openRead().source().buffer().use { input ->
val bb = ByteBuffer.allocate(8) val bb = ByteBuffer.allocate(8)
while (true) { while (true) {
@@ -126,7 +129,7 @@ class MemAutoFlushingLookupTable<T>(
val fos = atomicFile.startWrite() val fos = atomicFile.startWrite()
try { try {
val out = fos.buffered() val out = fos.sink().buffer()
table.forEach { key, value -> table.forEach { key, value ->
val v = serializer.write(value).toByteArray(Charsets.UTF_8) val v = serializer.write(value).toByteArray(Charsets.UTF_8)
bb.putInt(0, key) bb.putInt(0, key)
@@ -26,9 +26,9 @@ class EpubFile(file: UniFile, context: Context) : Closeable {
* Zip file of this epub. * Zip file of this epub.
*/ */
private val zip = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { private val zip = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
ZipFile(tempFileManager.createTempFile(file)) ZipFile.Builder().setFile(tempFileManager.createTempFile(file)).get()
} else { } else {
ZipFile(file.openReadOnlyChannel(context)) ZipFile.Builder().setSeekableByteChannel(file.openReadOnlyChannel(context)).get()
} }
// SY <-- // SY <--
@@ -26,11 +26,10 @@ 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 okio.Buffer
import okio.BufferedSource
import tachiyomi.decoder.Format import tachiyomi.decoder.Format
import tachiyomi.decoder.ImageDecoder import tachiyomi.decoder.ImageDecoder
import java.io.BufferedInputStream
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.net.URLConnection import java.net.URLConnection
@@ -83,9 +82,9 @@ object ImageUtil {
?: "jpg" ?: "jpg"
} }
fun isAnimatedAndSupported(stream: InputStream): Boolean { fun isAnimatedAndSupported(source: BufferedSource): Boolean {
return try { return try {
val type = getImageType(stream) ?: return false val type = getImageType(source.peek().inputStream()) ?: return false
// https://coil-kt.github.io/coil/getting_started/#supported-image-formats // https://coil-kt.github.io/coil/getting_started/#supported-image-formats
when (type.format) { when (type.format) {
Format.Gif -> true Format.Gif -> true
@@ -132,18 +131,16 @@ object ImageUtil {
* *
* @return true if the width is greater than the height * @return true if the width is greater than the height
*/ */
fun isWideImage(imageStream: BufferedInputStream): Boolean { fun isWideImage(imageSource: BufferedSource): Boolean {
val options = extractImageOptions(imageStream) val options = extractImageOptions(imageSource)
return options.outWidth > options.outHeight return options.outWidth > options.outHeight
} }
/** /**
* Extract the 'side' part from imageStream and return it as InputStream. * Extract the 'side' part from [BufferedSource] and return it as [BufferedSource].
*/ */
fun splitInHalf(imageStream: InputStream, side: Side, sidePadding: Int): InputStream { fun splitInHalf(imageSource: BufferedSource, side: Side, sidePadding: Int): BufferedSource {
val imageBytes = imageStream.readBytes() val imageBitmap = BitmapFactory.decodeStream(imageSource.inputStream())
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
val height = imageBitmap.height val height = imageBitmap.height
val width = imageBitmap.width val width = imageBitmap.width
@@ -157,22 +154,20 @@ object ImageUtil {
half.applyCanvas { half.applyCanvas {
drawBitmap(imageBitmap, part, singlePage, null) drawBitmap(imageBitmap, part, singlePage, null)
} }
val output = ByteArrayOutputStream() val output = Buffer()
half.compress(Bitmap.CompressFormat.JPEG, 100, output) half.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream())
return ByteArrayInputStream(output.toByteArray()) return output
} }
fun rotateImage(imageStream: InputStream, degrees: Float): InputStream { fun rotateImage(imageSource: BufferedSource, degrees: Float): BufferedSource {
val imageBytes = imageStream.readBytes() val imageBitmap = BitmapFactory.decodeStream(imageSource.inputStream())
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
val rotated = rotateBitMap(imageBitmap, degrees) val rotated = rotateBitMap(imageBitmap, degrees)
val output = ByteArrayOutputStream() val output = Buffer()
rotated.compress(Bitmap.CompressFormat.JPEG, 100, output) rotated.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream())
return ByteArrayInputStream(output.toByteArray()) return output
} }
private fun rotateBitMap(bitmap: Bitmap, degrees: Float): Bitmap { private fun rotateBitMap(bitmap: Bitmap, degrees: Float): Bitmap {
@@ -184,10 +179,8 @@ object ImageUtil {
* Split the image into left and right parts, then merge them into a * Split the image into left and right parts, then merge them into a
* new vertically-aligned image. * new vertically-aligned image.
*/ */
fun splitAndMerge(imageStream: InputStream, upperSide: Side): InputStream { fun splitAndMerge(imageSource: BufferedSource, upperSide: Side): BufferedSource {
val imageBytes = imageStream.readBytes() val imageBitmap = BitmapFactory.decodeStream(imageSource.inputStream())
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
val height = imageBitmap.height val height = imageBitmap.height
val width = imageBitmap.width val width = imageBitmap.width
@@ -209,9 +202,9 @@ object ImageUtil {
drawBitmap(imageBitmap, leftPart, bottomPart, null) drawBitmap(imageBitmap, leftPart, bottomPart, null)
} }
val output = ByteArrayOutputStream() val output = Buffer()
result.compress(Bitmap.CompressFormat.JPEG, 100, output) result.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream())
return ByteArrayInputStream(output.toByteArray()) return output
} }
enum class Side { enum class Side {
@@ -225,8 +218,8 @@ object ImageUtil {
* to compensate for scaling. * to compensate for scaling.
*/ */
fun addHorizontalCenterMargin(imageStream: InputStream, viewHeight: Int, backgroundContext: Context): InputStream { fun addHorizontalCenterMargin(imageSource: BufferedSource, viewHeight: Int, backgroundContext: Context): BufferedSource {
val imageBitmap = ImageDecoder.newInstance(imageStream)?.decode()!! val imageBitmap = ImageDecoder.newInstance(imageSource.inputStream())?.decode()!!
val height = imageBitmap.height val height = imageBitmap.height
val width = imageBitmap.width val width = imageBitmap.width
@@ -237,7 +230,7 @@ object ImageUtil {
val leftTargetPart = Rect(0, 0, width / 2, height) val leftTargetPart = Rect(0, 0, width / 2, height)
val rightTargetPart = Rect(width / 2 + centerPadding, 0, width + centerPadding, height) val rightTargetPart = Rect(width / 2 + centerPadding, 0, width + centerPadding, height)
val bgColor = chooseBackground(backgroundContext, imageStream) val bgColor = chooseBackground(backgroundContext, imageSource)
bgColor.setBounds(width / 2, 0, width / 2 + centerPadding, height) bgColor.setBounds(width / 2, 0, width / 2 + centerPadding, height)
val result = createBitmap(width + centerPadding, height) val result = createBitmap(width + centerPadding, height)
@@ -247,9 +240,9 @@ object ImageUtil {
bgColor.draw(this) bgColor.draw(this)
} }
val output = ByteArrayOutputStream() val output = Buffer()
result.compress(Bitmap.CompressFormat.JPEG, 100, output) result.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream())
return ByteArrayInputStream(output.toByteArray()) return output
} }
// SY <-- // SY <--
@@ -258,11 +251,8 @@ 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(imageStream: InputStream): Boolean { private fun isTallImage(imageSource: BufferedSource): Boolean {
val options = extractImageOptions( val options = extractImageOptions(imageSource)
imageStream,
resetAfterExtraction = false,
)
return (options.outHeight / options.outWidth) > 3 return (options.outHeight / options.outWidth) > 3
} }
@@ -275,22 +265,18 @@ object ImageUtil {
imageFile: UniFile, imageFile: UniFile,
filenamePrefix: String, filenamePrefix: String,
): Boolean { ): Boolean {
if (isAnimatedAndSupported(imageFile.openInputStream()) || val imageSource = imageFile.openInputStream().use { Buffer().readFrom(it) }
!isTallImage(imageFile.openInputStream()) if (isAnimatedAndSupported(imageSource) || !isTallImage(imageSource)) {
) {
return true return true
} }
val bitmapRegionDecoder = getBitmapRegionDecoder(imageFile.openInputStream()) val bitmapRegionDecoder = getBitmapRegionDecoder(imageSource.peek().inputStream())
if (bitmapRegionDecoder == null) { if (bitmapRegionDecoder == null) {
logcat { "Failed to create new instance of BitmapRegionDecoder" } logcat { "Failed to create new instance of BitmapRegionDecoder" }
return false return false
} }
val options = extractImageOptions( val options = extractImageOptions(imageSource).apply {
imageFile.openInputStream(),
resetAfterExtraction = false,
).apply {
inJustDecodeBounds = false inJustDecodeBounds = false
} }
@@ -380,8 +366,8 @@ object ImageUtil {
/** /**
* Algorithm for determining what background to accompany a comic/manga page * Algorithm for determining what background to accompany a comic/manga page
*/ */
fun chooseBackground(context: Context, imageStream: InputStream): Drawable { fun chooseBackground(context: Context, imageSource: BufferedSource): Drawable {
val decoder = ImageDecoder.newInstance(imageStream) val decoder = ImageDecoder.newInstance(imageSource.inputStream())
val image = decoder?.decode() val image = decoder?.decode()
decoder?.recycle() decoder?.recycle()
@@ -603,16 +589,9 @@ object ImageUtil {
/** /**
* Used to check an image's dimensions without loading it in the memory. * Used to check an image's dimensions without loading it in the memory.
*/ */
private fun extractImageOptions( private fun extractImageOptions(imageSource: BufferedSource): BitmapFactory.Options {
imageStream: InputStream,
resetAfterExtraction: Boolean = true,
): BitmapFactory.Options {
imageStream.mark(Int.MAX_VALUE)
val imageBytes = imageStream.readBytes()
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options) BitmapFactory.decodeStream(imageSource.peek().inputStream(), null, options)
if (resetAfterExtraction) imageStream.reset()
return options return options
} }
@@ -657,7 +636,7 @@ object ImageUtil {
centerMargin: Int, centerMargin: Int,
@ColorInt background: Int = Color.WHITE, @ColorInt background: Int = Color.WHITE,
progressCallback: ((Int) -> Unit)? = null, progressCallback: ((Int) -> Unit)? = null,
): ByteArrayInputStream { ): BufferedSource {
val height = imageBitmap.height val height = imageBitmap.height
val width = imageBitmap.width val width = imageBitmap.width
val height2 = imageBitmap2.height val height2 = imageBitmap2.height
@@ -687,10 +666,10 @@ object ImageUtil {
canvas.drawBitmap(imageBitmap2, imageBitmap2.rect, bottomPart, null) canvas.drawBitmap(imageBitmap2, imageBitmap2.rect, bottomPart, null)
progressCallback?.invoke(99) progressCallback?.invoke(99)
val output = ByteArrayOutputStream() val output = Buffer()
result.compress(Bitmap.CompressFormat.JPEG, 100, output) result.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream())
progressCallback?.invoke(100) progressCallback?.invoke(100)
return ByteArrayInputStream(output.toByteArray()) return output
} }
private val Bitmap.rect: Rect private val Bitmap.rect: Rect
+5 -5
View File
@@ -1,5 +1,5 @@
[versions] [versions]
agp_version = "8.3.1" agp_version = "8.4.0"
lifecycle_version = "2.7.0" lifecycle_version = "2.7.0"
paging_version = "3.2.1" paging_version = "3.2.1"
@@ -10,7 +10,7 @@ annotation = "androidx.annotation:annotation:1.7.1"
appcompat = "androidx.appcompat:appcompat:1.6.1" appcompat = "androidx.appcompat:appcompat:1.6.1"
biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05" biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4" constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
corektx = "androidx.core:core-ktx:1.12.0" corektx = "androidx.core:core-ktx:1.13.1"
splashscreen = "androidx.core:core-splashscreen:1.0.1" splashscreen = "androidx.core:core-splashscreen:1.0.1"
recyclerview = "androidx.recyclerview:recyclerview:1.3.2" recyclerview = "androidx.recyclerview:recyclerview:1.3.2"
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01" viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
@@ -25,9 +25,9 @@ workmanager = "androidx.work:work-runtime:2.9.0"
paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" } paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" }
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" } paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.3" benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.4"
test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha03" test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha04"
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha03" test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha04"
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0" test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0"
[bundles] [bundles]
+4 -3
View File
@@ -1,12 +1,13 @@
[versions] [versions]
compiler = "1.5.11" compiler = "1.5.12"
compose-bom = "2024.02.00-alpha02" # 2024.04.00-alpha01 has several bugs with the new animateItem() modifier
compose-bom = "2024.03.00-alpha02"
accompanist = "0.35.0-alpha" accompanist = "0.35.0-alpha"
[libraries] [libraries]
compiler = { module = "androidx.compose.compiler:compiler", version.ref = "compiler" } compiler = { module = "androidx.compose.compiler:compiler", version.ref = "compiler" }
activity = "androidx.activity:activity-compose:1.8.2" activity = "androidx.activity:activity-compose:1.9.0"
bom = { group = "dev.chrisbanes.compose", name = "compose-bom", version.ref = "compose-bom" } bom = { group = "dev.chrisbanes.compose", name = "compose-bom", version.ref = "compose-bom" }
foundation = { module = "androidx.compose.foundation:foundation" } foundation = { module = "androidx.compose.foundation:foundation" }
animation = { module = "androidx.compose.animation:animation" } animation = { module = "androidx.compose.animation:animation" }
+5 -9
View File
@@ -1,7 +1,6 @@
[versions] [versions]
aboutlib_version = "11.1.1" aboutlib_version = "11.1.4"
acra = "5.11.3" leakcanary = "2.14"
leakcanary = "2.13"
moko = "0.23.0" moko = "0.23.0"
okhttp_version = "5.0.0-alpha.12" okhttp_version = "5.0.0-alpha.12"
richtext = "0.20.0" richtext = "0.20.0"
@@ -32,7 +31,7 @@ quickjs-android = "app.cash.quickjs:quickjs-android:0.9.2"
jsoup = "org.jsoup:jsoup:1.17.2" 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:e0def6b3dc"
common-compress = "org.apache.commons:commons-compress:1.26.1" common-compress = "org.apache.commons:commons-compress:1.26.1"
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"
@@ -52,7 +51,7 @@ coil-gif = { module = "io.coil-kt.coil3:coil-gif" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose" } coil-compose = { module = "io.coil-kt.coil3:coil-compose" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp" } coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp" }
subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:aeaa170036" subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:b8e1b0ed2b"
image-decoder = "com.github.tachiyomiorg:image-decoder:e08e9be535" image-decoder = "com.github.tachiyomiorg:image-decoder:e08e9be535"
exifinterface = "androidx.exifinterface:exifinterface:1.3.7" exifinterface = "androidx.exifinterface:exifinterface:1.3.7"
@@ -77,9 +76,7 @@ moko-gradle = { module = "dev.icerock.moko:resources-generator", version.ref = "
logcat = "com.squareup.logcat:logcat:0.1" logcat = "com.squareup.logcat:logcat:0.1"
acra-http = { module = "ch.acra:acra-http", version.ref = "acra" } firebase-analytics = "com.google.firebase:firebase-analytics:22.0.0"
acra-scheduler = { module = "ch.acra:acra-advanced-scheduler", version.ref = "acra" }
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.6.1"
aboutLibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" } aboutLibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" }
aboutLibraries-compose = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlib_version" } aboutLibraries-compose = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlib_version" }
@@ -113,7 +110,6 @@ google-api-services-drive = "com.google.apis:google-api-services-drive:v3-rev197
google-api-client-oauth = "com.google.oauth-client:google-oauth-client:1.34.1" google-api-client-oauth = "com.google.oauth-client:google-oauth-client:1.34.1"
[bundles] [bundles]
acra = ["acra-http", "acra-scheduler"]
archive = ["common-compress", "junrar"] 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"]
+3 -3
View File
@@ -1,8 +1,8 @@
[versions] [versions]
[libraries] [libraries]
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.6.1" firebase-analytics = "com.google.firebase:firebase-analytics:22.0.0"
firebase-crashlytics-ktx = "com.google.firebase:firebase-crashlytics-ktx:18.6.3" firebase-crashlytics-ktx = "com.google.firebase:firebase-crashlytics:19.0.0"
firebase-crashlytics-gradle = "com.google.firebase:firebase-crashlytics-gradle:2.9.9" firebase-crashlytics-gradle = "com.google.firebase:firebase-crashlytics-gradle:2.9.9"
simularity = "info.debatty:java-string-similarity:2.0.0" simularity = "info.debatty:java-string-similarity:2.0.0"
@@ -11,4 +11,4 @@ xlog = "com.elvishew:xlog:1.11.0"
ratingbar = "me.zhanghai.android.materialratingbar:library:1.4.0" ratingbar = "me.zhanghai.android.materialratingbar:library:1.4.0"
composeRatingbar = "com.github.a914-gowtham:compose-ratingbar:1.2.3" composeRatingbar = "com.github.a914-gowtham:compose-ratingbar:1.2.3"
versionsx = "com.github.ben-manes:gradle-versions-plugin:0.42.0" versionsx = "com.github.ben-manes:gradle-versions-plugin:0.51.0"
@@ -203,10 +203,8 @@
<string name="pref_local_source_hidden_folders_summery">Allow local source to read hidden folders</string> <string name="pref_local_source_hidden_folders_summery">Allow local source to read hidden folders</string>
<!-- Backup settings --> <!-- Backup settings -->
<string name="pref_backup_and_sync_summary">Manual &amp; automatic backups and sync</string>
<string name="custom_entry_info">Custom entry info</string> <string name="custom_entry_info">Custom entry info</string>
<string name="all_read_entries">All read entries</string> <string name="all_read_entries">All read entries</string>
<string name="label_backup">Backup</string>
<string name="label_sync">Sync</string> <string name="label_sync">Sync</string>
<string name="label_triggers">Triggers</string> <string name="label_triggers">Triggers</string>
@@ -220,24 +218,16 @@
<string name="pref_sync_api_key_summ">Enter the API key to synchronize your library</string> <string name="pref_sync_api_key_summ">Enter the API key to synchronize your library</string>
<string name="pref_sync_now_group_title">Sync Actions</string> <string name="pref_sync_now_group_title">Sync Actions</string>
<string name="pref_sync_now">Sync now</string> <string name="pref_sync_now">Sync now</string>
<string name="pref_sync_confirmation_title">Sync confirmation</string>
<string name="pref_sync_now_subtitle">Initiate immediate synchronization of your data</string> <string name="pref_sync_now_subtitle">Initiate immediate synchronization of your data</string>
<string name="pref_sync_confirmation_message">Syncing will overwrite your local library with the remote library. Are you sure you want to continue?</string>
<string name="pref_sync_service">Service</string> <string name="pref_sync_service">Service</string>
<string name="pref_sync_service_summ">Select the service to sync your library with</string>
<string name="pref_sync_service_category">Sync</string> <string name="pref_sync_service_category">Sync</string>
<string name="pref_sync_automatic_category">Automatic Synchronization</string> <string name="pref_sync_automatic_category">Automatic Synchronization</string>
<string name="pref_sync_interval">Synchronization frequency</string> <string name="pref_sync_interval">Synchronization frequency</string>
<string name="pref_choose_what_to_sync">Choose what to sync</string> <string name="pref_choose_what_to_sync">Choose what to sync</string>
<string name="success_reset_sync_timestamp">Last sync timestamp reset</string>
<string name="syncyomi">SyncYomi</string> <string name="syncyomi">SyncYomi</string>
<string name="sync_completed_message">Done in %1$s</string>
<string name="last_synchronization">Last Synchronization: %1$s</string> <string name="last_synchronization">Last Synchronization: %1$s</string>
<string name="google_drive">Google Drive</string> <string name="google_drive">Google Drive</string>
<string name="pref_google_drive_sign_in">Sign in</string> <string name="pref_google_drive_sign_in">Sign in</string>
<string name="google_drive_sign_in_success">Signed in successfully</string>
<string name="google_drive_sign_in_failed">Sign in failed</string>
<string name="authentication">Authentication</string>
<string name="pref_google_drive_purge_sync_data">Clear Sync Data from Google Drive</string> <string name="pref_google_drive_purge_sync_data">Clear Sync Data from Google Drive</string>
<string name="google_drive_sync_data_purged">Sync data purged from Google Drive</string> <string name="google_drive_sync_data_purged">Sync data purged from Google Drive</string>
<string name="google_drive_sync_data_not_found">No sync data found in Google Drive</string> <string name="google_drive_sync_data_not_found">No sync data found in Google Drive</string>
@@ -179,6 +179,9 @@
<string name="pref_mark_read_dupe_chapters_summary">Помечать повторяющиеся главы как «Прочитано» после прочтения</string> <string name="pref_mark_read_dupe_chapters_summary">Помечать повторяющиеся главы как «Прочитано» после прочтения</string>
<string name="pref_library_mark_duplicate_chapters">Помечать новые повторяющиеся главы как «Прочитано»</string> <string name="pref_library_mark_duplicate_chapters">Помечать новые повторяющиеся главы как «Прочитано»</string>
<string name="pref_library_mark_duplicate_chapters_summary">Автоматически помечать новые главы как «Прочитано», если они были ранее прочитаны</string> <string name="pref_library_mark_duplicate_chapters_summary">Автоматически помечать новые главы как «Прочитано», если они были ранее прочитаны</string>
<string name="update_30min">Каждые 30 минут</string>
<string name="update_1hour">Каждый час</string>
<string name="update_3hour">Каждые 3 часа</string>
<!-- Browse settings --> <!-- Browse settings -->
<string name="pref_hide_feed">Скрыть вкладку «Лента»</string> <string name="pref_hide_feed">Скрыть вкладку «Лента»</string>
@@ -194,6 +197,47 @@
<!-- Backup settings --> <!-- Backup settings -->
<string name="custom_entry_info">Сведенья пользователя</string> <string name="custom_entry_info">Сведенья пользователя</string>
<string name="all_read_entries">Прочитанные серии</string> <string name="all_read_entries">Прочитанные серии</string>
<string name="label_sync">Синхронизировать</string>
<string name="label_triggers">Триггеры</string>
<!-- Sync settings -->
<string name="sync_error">Не удалось синхронизировать библиотеку</string>
<string name="sync_complete">Синхронизация библиотеки завершена</string>
<string name="sync_in_progress">Синхронизация уже выполняется</string>
<string name="pref_sync_host">Хост</string>
<string name="pref_sync_host_summ">Введите адрес хоста для синхронизации вашей библиотеки</string>
<string name="pref_sync_api_key">Ключ API</string>
<string name="pref_sync_api_key_summ">Введите API ключ для синхронизации вашей библиотеки</string>
<string name="pref_sync_now_group_title">Действия синхронизации</string>
<string name="pref_sync_now">Синхронизировать сейчас</string>
<string name="pref_sync_now_subtitle">Начать немедленную синхронизацию ваших данных</string>
<string name="pref_sync_service">Сервис</string>
<string name="pref_sync_service_category">Синхронизация</string>
<string name="pref_sync_automatic_category">Автоматическая синхронизация</string>
<string name="pref_sync_interval">Частота синхронизации</string>
<string name="pref_choose_what_to_sync">Выберите, что синхронизировать</string>
<string name="last_synchronization">Последняя синхронизация: %1$s</string>
<string name="google_drive">Google Диск</string>
<string name="pref_google_drive_sign_in">Войти в Google Диск</string>
<string name="pref_google_drive_purge_sync_data">Очистить данные синхронизации с Google Диска</string>
<string name="google_drive_sync_data_purged">Данные синхронизации удалены из Google Диска</string>
<string name="google_drive_sync_data_not_found">Данные синхронизации не найдены в Google Диске</string>
<string name="google_drive_sync_data_purge_error">Ошибка при очистке данных синхронизации с Google Диска, попробуйте войти снова.</string>
<string name="google_drive_login_success">Выполнен вход в Google Диск</string>
<string name="google_drive_login_failed">Не удалось войти в Google Диск: %s</string>
<string name="google_drive_not_signed_in">Не выполнен вход в Google Диск</string>
<string name="error_uploading_sync_data">Ошибка при загрузке данных синхронизации на Google Диск</string>
<string name="error_deleting_google_drive_lock_file">Ошибка удаления файла блокировки Google Диска</string>
<string name="error_before_sync_gdrive">Ошибка перед синхронизацией: %s</string>
<string name="pref_purge_confirmation_title">Подтверждение очистки</string>
<string name="pref_purge_confirmation_message">Очистка данных синхронизации приведет к удалению всех данных синхронизации с Google Диска. Вы уверены, что хотите продолжить?</string>
<string name="pref_sync_options">Создать триггеры синхронизации</string>
<string name="pref_sync_options_summ">Можно использовать для настройки триггеров синхронизации</string>
<string name="sync_on_chapter_read">Синхронизовать при прочтении главы</string>
<string name="sync_on_chapter_open">Синхронизовать при открытии главы</string>
<string name="sync_on_app_start">Синхронизовать при запуске приложения</string>
<string name="sync_on_app_resume">Синхронизовать при возвращении в приложение</string>
<string name="sync_library">Синхронизировать библиотеку</string>
<!-- Security settings --> <!-- Security settings -->
<string name="biometric_lock_times">Биометрическое время блокировки</string> <string name="biometric_lock_times">Биометрическое время блокировки</string>
@@ -10,7 +10,6 @@ import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.gestures.animateTo import androidx.compose.foundation.gestures.animateTo
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
@@ -78,8 +77,7 @@ fun AdaptiveSheet(
Box( Box(
modifier = Modifier modifier = Modifier
.clickable( .clickable(
enabled = true, interactionSource = null,
interactionSource = remember { MutableInteractionSource() },
indication = null, indication = null,
onClick = internalOnDismissRequest, onClick = internalOnDismissRequest,
) )
@@ -91,7 +89,7 @@ fun AdaptiveSheet(
modifier = Modifier modifier = Modifier
.requiredWidthIn(max = 460.dp) .requiredWidthIn(max = 460.dp)
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = null,
indication = null, indication = null,
onClick = {}, onClick = {},
) )
@@ -122,14 +120,14 @@ fun AdaptiveSheet(
) )
} }
val internalOnDismissRequest = { val internalOnDismissRequest = {
if (anchoredDraggableState.currentValue == 0) { if (anchoredDraggableState.settledValue == 0) {
scope.launch { anchoredDraggableState.animateTo(1) } scope.launch { anchoredDraggableState.animateTo(1) }
} }
} }
Box( Box(
modifier = Modifier modifier = Modifier
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = null,
indication = null, indication = null,
onClick = internalOnDismissRequest, onClick = internalOnDismissRequest,
) )
@@ -147,7 +145,7 @@ fun AdaptiveSheet(
modifier = Modifier modifier = Modifier
.widthIn(max = 460.dp) .widthIn(max = 460.dp)
.clickable( .clickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = null,
indication = null, indication = null,
onClick = {}, onClick = {},
) )
@@ -192,7 +190,7 @@ fun AdaptiveSheet(
LaunchedEffect(anchoredDraggableState) { LaunchedEffect(anchoredDraggableState) {
scope.launch { anchoredDraggableState.animateTo(0) } scope.launch { anchoredDraggableState.animateTo(0) }
snapshotFlow { anchoredDraggableState.currentValue } snapshotFlow { anchoredDraggableState.settledValue }
.drop(1) .drop(1)
.filter { it == 1 } .filter { it == 1 }
.collectLatest { .collectLatest {
@@ -6,13 +6,13 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.material.ripple
import androidx.compose.material3.ColorScheme import androidx.compose.material3.ColorScheme
import androidx.compose.material3.LocalAbsoluteTonalElevation import androidx.compose.material3.LocalAbsoluteTonalElevation
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.contentColorFor import androidx.compose.material3.contentColorFor
import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.NonRestartableComposable
@@ -1,7 +1,6 @@
package tachiyomi.presentation.core.util package tachiyomi.presentation.core.util
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.isImeVisible import androidx.compose.foundation.layout.isImeVisible
@@ -42,14 +41,12 @@ fun Modifier.secondaryItemAlpha(): Modifier = this.alpha(SecondaryItemAlpha)
fun Modifier.clickableNoIndication( fun Modifier.clickableNoIndication(
onLongClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null,
onClick: () -> Unit, onClick: () -> Unit,
): Modifier = composed { ) = this.combinedClickable(
Modifier.combinedClickable( interactionSource = null,
interactionSource = remember { MutableInteractionSource() }, indication = null,
indication = null, onLongClick = onLongClick,
onLongClick = onLongClick, onClick = onClick,
onClick = onClick, )
)
}
/** /**
* For TextField, the provided [action] will be invoked when * For TextField, the provided [action] will be invoked when
@@ -17,16 +17,10 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream
import logcat.LogPriority import logcat.LogPriority
import nl.adaptivity.xmlutil.AndroidXmlReader import nl.adaptivity.xmlutil.AndroidXmlReader
import nl.adaptivity.xmlutil.serialization.XML import nl.adaptivity.xmlutil.serialization.XML
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
import tachiyomi.core.metadata.comicinfo.ComicInfo
import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
import tachiyomi.core.metadata.comicinfo.getComicInfo
import tachiyomi.core.metadata.tachiyomi.MangaDetails
import tachiyomi.core.common.storage.UniFileTempFileManager import tachiyomi.core.common.storage.UniFileTempFileManager
import tachiyomi.core.common.storage.addStreamToZip import tachiyomi.core.common.storage.addStreamToZip
import tachiyomi.core.common.storage.extension import tachiyomi.core.common.storage.extension
@@ -37,6 +31,12 @@ 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
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
import tachiyomi.core.metadata.comicinfo.ComicInfo
import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus
import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
import tachiyomi.core.metadata.comicinfo.getComicInfo
import tachiyomi.core.metadata.tachiyomi.MangaDetails
import tachiyomi.domain.chapter.service.ChapterRecognition import tachiyomi.domain.chapter.service.ChapterRecognition
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
@@ -157,17 +157,39 @@ actual class LocalSource(
// SY --> // SY -->
fun updateMangaInfo(manga: SManga) { fun updateMangaInfo(manga: SManga) {
val existingFile = fileSystem.getFilesInMangaDirectory(manga.url).find { it.extension == "json" } val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url)
val file = existingFile val existingFile = mangaDirFiles
?: fileSystem.getMangaDirectory(manga.url)?.createFile("info.json") .firstOrNull { it.name == COMIC_INFO_FILE }
?: return val comicInfoArchiveFile = mangaDirFiles
file.openOutputStream().use { .firstOrNull { it.name == COMIC_INFO_ARCHIVE }
json.encodeToStream(manga.toJson(), it) val existingComicInfo = (existingFile?.openInputStream() ?: comicInfoArchiveFile?.getZipInputStream(COMIC_INFO_FILE))?.use {
AndroidXmlReader(it, StandardCharsets.UTF_8.name()).use {
xml.decodeFromReader<ComicInfo>(it)
}
}
val newComicInfo = if (existingComicInfo != null) {
manga.run {
existingComicInfo.copy(
series = ComicInfo.Series(title),
summary = description?.let { ComicInfo.Summary(it) },
writer = author?.let { ComicInfo.Writer(it) },
penciller = artist?.let { ComicInfo.Penciller(it) },
genre = genre?.let { ComicInfo.Genre(it) },
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
ComicInfoPublishingStatus.toComicInfoValue(status.toLong()),
),
)
}
} else {
manga.getComicInfo()
} }
}
private fun SManga.toJson(): MangaDetails { fileSystem.getMangaDirectory(manga.url)?.let {
return MangaDetails(title, author, artist, description, genre?.split(", "), status) copyComicInfoFile(
xml.encodeToString(ComicInfo.serializer(), newComicInfo).byteInputStream(),
it
)
}
} }
// SY <-- // SY <--
@@ -368,8 +390,8 @@ actual class LocalSource(
try { try {
val (mangaDirName, chapterName) = chapter.url.split('/', limit = 2) val (mangaDirName, chapterName) = chapter.url.split('/', limit = 2)
return fileSystem.getBaseDirectory() return fileSystem.getBaseDirectory()
?.findFile(mangaDirName, true) ?.findFile(mangaDirName)
?.findFile(chapterName, true) ?.findFile(chapterName)
?.let(Format.Companion::valueOf) ?.let(Format.Companion::valueOf)
?: throw Exception(context.stringResource(MR.strings.chapter_not_found)) ?: throw Exception(context.stringResource(MR.strings.chapter_not_found))
} catch (e: Format.UnknownFormatException) { } catch (e: Format.UnknownFormatException) {
@@ -17,13 +17,13 @@ actual class LocalSourceFileSystem(
actual fun getMangaDirectory(name: String): UniFile? { actual fun getMangaDirectory(name: String): UniFile? {
return getBaseDirectory() return getBaseDirectory()
?.findFile(name, true) ?.findFile(name)
?.takeIf { it.isDirectory } ?.takeIf { it.isDirectory }
} }
actual fun getFilesInMangaDirectory(name: String): List<UniFile> { actual fun getFilesInMangaDirectory(name: String): List<UniFile> {
return getBaseDirectory() return getBaseDirectory()
?.findFile(name, true) ?.findFile(name)
?.takeIf { it.isDirectory } ?.takeIf { it.isDirectory }
?.listFiles().orEmpty().toList() ?.listFiles().orEmpty().toList()
} }