Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46bf139f01 | |||
| c3fb5c0bec | |||
| 000a4ffc3f | |||
| 7b0b879d65 | |||
| 8a622f6c7d | |||
| 253060a3bc | |||
| b6b33e8c00 | |||
| 2e4f811090 | |||
| 215a1908f7 | |||
| 082acf000c | |||
| 2d1240b274 | |||
| 1e98709cc3 | |||
| 5550ddad4e | |||
| f9148c0c5e | |||
| 089e6268e7 | |||
| 712cd1493f | |||
| bbc8adc3e8 | |||
| 077b673c0a | |||
| 49eacf5178 | |||
| 98d1dddf4a | |||
| 37a616f3db | |||
| ad18696a1a | |||
| 34bb012a1c | |||
| 08c4989aa3 | |||
| 14dae420f5 | |||
| 65ed3c5ae6 | |||
| 5ae3508665 | |||
| e32eb0e009 | |||
| e0812ab5c8 | |||
| df9f79c120 | |||
| 990eb33b98 | |||
| e1bab1172a | |||
| aeeff72bed | |||
| 5895e78b39 | |||
| b24719a3e9 | |||
| d551619d9d | |||
| 06ad6c2e16 | |||
| df7e470e08 |
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
+1
-1
@@ -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,
|
||||||
|
|||||||
+3
@@ -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) {
|
||||||
|
|||||||
+16
-23
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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 & 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>
|
||||||
|
|||||||
+6
-8
@@ -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 {
|
||||||
|
|||||||
+1
-1
@@ -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) {
|
||||||
|
|||||||
+2
-2
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user