Compare commits

..

6 Commits

Author SHA1 Message Date
Cuong-Tran
88aff2c77f
Fix app update error notification disappearing (#1476) 2024-11-20 20:07:34 +06:00
AntsyLich
81effea01c
Slightly tweak Preference.PreferenceItem.CustomPreference 2024-11-20 17:54:30 +06:00
AntsyLich
9aef08c333
Fix loading screen not appearing when changing query in browser screen
Fixes #1438
Closes #1441
2024-11-20 17:54:30 +06:00
AntsyLich
dcddac5daa
Add option to lower the threshold for hardware bitmaps
Closes #1436
Closes #1486
2024-11-20 17:54:29 +06:00
AntsyLich
e6d96bd348
Switch to hardware bitmap in reader only if device can handle it
Closes #1460
2024-11-20 17:18:32 +06:00
AntsyLich
1909126921
Revert "Add option to always use SSIV for image decoding"
This reverts commit bb4d9fc81a043ac4f2d0105f19c09974ae2f7201.
2024-11-17 02:51:48 +06:00
19 changed files with 121 additions and 66 deletions

View File

@ -18,7 +18,6 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- Fixed "currentTab was used multiple times"
- Fixed a rare crash when invoking "Mark previous as read" action
- Fixed long strip images not loading in some old devices
- Added option to always use SSIV for image decoding
### Improved
- Bangumi search now shows the score and summary of a search result ([@MajorTanya](https://github.com/MajorTanya)) ([#1396](https://github.com/mihonapp/mihon/pull/1396))

View File

@ -2,6 +2,7 @@ package eu.kanade.domain.base
import android.content.Context
import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.util.system.GLUtil
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.i18n.MR
@ -31,5 +32,5 @@ class BasePreferences(
fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "")
fun alwaysUseSSIVToDecode() = preferenceStore.getBoolean("pref_always_use_ssiv_to_decode", false)
fun hardwareBitmapThreshold() = preferenceStore.getInt("pref_hardware_bitmap_threshold", GLUtil.SAFE_TEXTURE_LIMIT)
}

View File

@ -162,12 +162,12 @@ sealed class Preference {
data class CustomPreference(
override val title: String,
val content: @Composable (PreferenceItem<String>) -> Unit,
) : PreferenceItem<String>() {
val content: @Composable () -> Unit,
) : PreferenceItem<Unit>() {
override val enabled: Boolean = true
override val subtitle: String? = null
override val icon: ImageVector? = null
override val onValueChanged: suspend (newValue: String) -> Boolean = { true }
override val onValueChanged: suspend (newValue: Unit) -> Boolean = { true }
}
}

View File

@ -167,7 +167,7 @@ internal fun PreferenceItem(
InfoWidget(text = item.title)
}
is Preference.PreferenceItem.CustomPreference -> {
item.content(item)
item.content()
}
}
}

View File

@ -47,6 +47,7 @@ import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
import eu.kanade.tachiyomi.network.PREF_DOH_SHECAN
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
@ -331,9 +332,23 @@ object SettingsAdvancedScreen : SearchableSettings {
basePreferences.displayProfile().set(uri.toString())
}
}
val hardwareBitmapThresholdPref = basePreferences.hardwareBitmapThreshold()
val hardwareBitmapThreshold by hardwareBitmapThresholdPref.collectAsState()
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_reader),
preferenceItems = persistentListOf(
Preference.PreferenceItem.ListPreference(
pref = hardwareBitmapThresholdPref,
title = stringResource(MR.strings.pref_hardware_bitmap_threshold),
subtitle = stringResource(
MR.strings.pref_hardware_bitmap_threshold_summary,
hardwareBitmapThreshold,
),
enabled = GLUtil.DEVICE_TEXTURE_LIMIT > GLUtil.SAFE_TEXTURE_LIMIT,
entries = GLUtil.CUSTOM_TEXTURE_LIMIT_OPTIONS
.associateWith { it.toString() }
.toImmutableMap(),
),
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.pref_display_profile),
subtitle = basePreferences.displayProfile().get(),
@ -341,10 +356,6 @@ object SettingsAdvancedScreen : SearchableSettings {
chooseColorProfile.launch(arrayOf("*/*"))
},
),
Preference.PreferenceItem.SwitchPreference(
pref = basePreferences.alwaysUseSSIVToDecode(),
title = stringResource(MR.strings.pref_always_use_ssiv_to_decode),
),
),
)
}

View File

@ -41,6 +41,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.system.cancelNotification
@ -58,6 +59,7 @@ import org.conscrypt.Conscrypt
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat
import tachiyomi.i18n.MR
import tachiyomi.presentation.widget.WidgetManager
@ -142,6 +144,14 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
.onEach(FirebaseConfig::setCrashlyticsEnabled)
.launchIn(scope)
basePreferences.hardwareBitmapThreshold().let { preference ->
if (!preference.isSet()) preference.set(GLUtil.DEVICE_TEXTURE_LIMIT)
}
basePreferences.hardwareBitmapThreshold().changes()
.onEach { ImageUtil.hardwareBitmapThreshold = it }
.launchIn(scope)
setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode().get())
// Updates widget update

View File

@ -10,7 +10,6 @@ import coil3.decode.ImageSource
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import coil3.request.bitmapConfig
import eu.kanade.tachiyomi.util.system.GLUtil
import okio.BufferedSource
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.decoder.ImageDecoder
@ -46,10 +45,7 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
check(bitmap != null) { "Failed to decode image" }
if (
options.bitmapConfig == Bitmap.Config.HARDWARE &&
maxOf(bitmap.width, bitmap.height) <= GLUtil.maxTextureSize
) {
if (options.bitmapConfig == Bitmap.Config.HARDWARE && ImageUtil.canUseHardwareBitmap(bitmap)) {
val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false)
if (hwBitmap != null) {
bitmap.recycle()

View File

@ -71,6 +71,7 @@ object Notifications {
const val CHANNEL_APP_UPDATE = "app_apk_update_channel"
const val ID_APP_UPDATER = 1
const val ID_APP_UPDATE_PROMPT = 2
const val ID_APP_UPDATE_ERROR = 3
const val CHANNEL_EXTENSIONS_UPDATE = "ext_apk_update_channel"
const val ID_UPDATES_TO_EXTS = -401
const val ID_EXTENSION_INSTALLER = -402

View File

@ -181,9 +181,9 @@ internal class AppUpdateNotifier(private val context: Context) {
addAction(
R.drawable.ic_close_24dp,
context.stringResource(MR.strings.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_APP_UPDATER),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_APP_UPDATE_ERROR),
)
}
notificationBuilder.show(Notifications.ID_APP_UPDATER)
notificationBuilder.show(Notifications.ID_APP_UPDATE_ERROR)
}
}

View File

@ -14,7 +14,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalUriHandler
import androidx.paging.compose.collectAsLazyPagingItems
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
@ -29,6 +28,7 @@ import eu.kanade.tachiyomi.ui.home.HomeScreen
import eu.kanade.tachiyomi.ui.manga.MangaScreen
import eu.kanade.tachiyomi.ui.webview.WebViewScreen
import kotlinx.coroutines.launch
import mihon.presentation.core.util.collectAsLazyPagingItems
import tachiyomi.core.common.Constants
import tachiyomi.domain.manga.model.Manga
import tachiyomi.i18n.MR
@ -86,7 +86,7 @@ data class SourceSearchScreen(
}
BrowseSourceContent(
source = screenModel.source,
mangaList = screenModel.mangaPagerFlow.collectAsLazyPagingItems(),
mangaList = screenModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
displayMode = screenModel.displayMode,
snackbarHostState = snackbarHostState,

View File

@ -31,7 +31,6 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalUriHandler
import androidx.paging.compose.collectAsLazyPagingItems
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
@ -56,6 +55,7 @@ import eu.kanade.tachiyomi.ui.webview.WebViewScreen
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import mihon.presentation.core.util.collectAsLazyPagingItems
import tachiyomi.core.common.Constants
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.source.model.StubSource
@ -208,7 +208,7 @@ data class BrowseSourceScreen(
) { paddingValues ->
BrowseSourceContent(
source = screenModel.source,
mangaList = screenModel.mangaPagerFlow.collectAsLazyPagingItems(),
mangaList = screenModel.mangaPagerFlowFlow.collectAsLazyPagingItems(),
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
displayMode = screenModel.displayMode,
snackbarHostState = snackbarHostState,

View File

@ -26,10 +26,11 @@ import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.util.removeCovers
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
@ -105,9 +106,9 @@ class BrowseSourceScreenModel(
* Flow of Pager flow tied to [State.listing]
*/
private val hideInLibraryItems = sourcePreferences.hideInLibraryItems().get()
val mangaPagerFlow = state.map { it.listing }
val mangaPagerFlowFlow = state.map { it.listing }
.distinctUntilChanged()
.flatMapLatest { listing ->
.map { listing ->
Pager(PagingConfig(pageSize = 25)) {
getRemoteManga.subscribe(sourceId, listing.query ?: "", listing.filters)
}.flow.map { pagingData ->
@ -119,8 +120,9 @@ class BrowseSourceScreenModel(
}
.filter { !hideInLibraryItems || !it.value.favorite }
}
}
.cachedIn(ioCoroutineScope)
}
.stateIn(ioCoroutineScope, SharingStarted.Lazily, emptyFlow())
fun getColumnsPreference(orientation: Int): GridCells {
val isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE

View File

@ -33,17 +33,13 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_IN_OUT
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_OUT_QUAD
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
import com.github.chrisbanes.photoview.PhotoView
import eu.kanade.domain.base.BasePreferences
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.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.view.isVisibleOnScreen
import okio.BufferedSource
import tachiyomi.core.common.util.system.ImageUtil
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* A wrapper view for showing page image.
@ -61,8 +57,6 @@ open class ReaderPageImageView @JvmOverloads constructor(
private val isWebtoon: Boolean = false,
) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) {
private val alwaysUseSSIVToDecode by lazy { Injekt.get<BasePreferences>().alwaysUseSSIVToDecode().get() }
private var pageView: View? = null
private var config: Config? = null
@ -239,7 +233,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
} else {
SubsamplingScaleImageView(context)
}.apply {
setMaxTileSize(GLUtil.maxTextureSize)
setMaxTileSize(ImageUtil.hardwareBitmapThreshold)
setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER)
setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
setMinimumTileDpi(180)
@ -300,11 +294,14 @@ open class ReaderPageImageView @JvmOverloads constructor(
isVisible = true
}
is BufferedSource -> {
if (alwaysUseSSIVToDecode || !isWebtoon || !ImageUtil.canUseCoilToDecode(data)) {
if (!isWebtoon) {
setHardwareConfig(ImageUtil.canUseHardwareBitmap(data))
setImage(ImageSource.inputStream(data.inputStream()))
isVisible = true
} else {
val request = ImageRequest.Builder(context)
return@apply
}
ImageRequest.Builder(context)
.data(data)
.memoryCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.DISABLED)
@ -315,7 +312,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
isVisible = true
},
onError = {
this@ReaderPageImageView.onImageLoadError()
onImageLoadError()
},
)
.size(ViewSizeResolver(this@ReaderPageImageView))
@ -324,8 +321,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
.customDecoder(true)
.crossfade(false)
.build()
context.imageLoader.enqueue(request)
}
.let(context.imageLoader::enqueue)
}
else -> {
throw IllegalArgumentException("Not implemented for class ${data::class.simpleName}")

View File

@ -3,10 +3,9 @@ package eu.kanade.tachiyomi.util.system
import javax.microedition.khronos.egl.EGL10
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.egl.EGLContext
import kotlin.math.max
object GLUtil {
val maxTextureSize: Int by lazy {
val DEVICE_TEXTURE_LIMIT: Int by lazy {
// Get EGL Display
val egl = EGLContext.getEGL() as EGL10
val display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY)
@ -38,10 +37,20 @@ object GLUtil {
// Release
egl.eglTerminate(display)
// Return largest texture size found, or default
max(maximumTextureSize, IMAGE_MAX_BITMAP_DIMENSION)
// Return largest texture size found (after making it a multiplier of [Multiplier]), or default
if (maximumTextureSize > SAFE_TEXTURE_LIMIT) {
(maximumTextureSize / MULTIPLIER) * MULTIPLIER
} else {
SAFE_TEXTURE_LIMIT
}
}
const val SAFE_TEXTURE_LIMIT: Int = 2048
val CUSTOM_TEXTURE_LIMIT_OPTIONS: List<Int> by lazy {
val steps = ((DEVICE_TEXTURE_LIMIT / MULTIPLIER) - 1)
List(steps) { (it + 2) * MULTIPLIER }.asReversed()
}
}
// Safe minimum default size
private const val IMAGE_MAX_BITMAP_DIMENSION = 2048
private const val MULTIPLIER: Int = 1024

View File

@ -310,9 +310,20 @@ object ImageUtil {
val bottomOffset = topOffset + splitHeight
}
fun canUseCoilToDecode(imageSource: BufferedSource): Boolean {
val options = extractImageOptions(imageSource)
return maxOf(options.outWidth, options.outHeight) <= GLUtil.maxTextureSize
fun canUseHardwareBitmap(bitmap: Bitmap): Boolean {
return canUseHardwareBitmap(bitmap.width, bitmap.height)
}
fun canUseHardwareBitmap(imageSource: BufferedSource): Boolean {
return with(extractImageOptions(imageSource)) {
canUseHardwareBitmap(outWidth, outHeight)
}
}
var hardwareBitmapThreshold: Int = GLUtil.SAFE_TEXTURE_LIMIT
private fun canUseHardwareBitmap(width: Int, height: Int): Boolean {
return maxOf(width, height) <= hardwareBitmapThreshold
}
/**

View File

@ -48,7 +48,7 @@ coil-gif = { module = "io.coil-kt.coil3:coil-gif" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp" }
subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:b8e1b0ed2b"
subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:66e0db195d"
image-decoder = "com.github.tachiyomiorg:image-decoder:41c059e540"
natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1"

View File

@ -392,7 +392,8 @@
<string name="pref_show_reading_mode">Show reading mode</string>
<string name="pref_show_reading_mode_summary">Briefly show current mode when reader is opened</string>
<string name="pref_display_profile">Custom display profile</string>
<string name="pref_always_use_ssiv_to_decode">Always use SSIV to decode long strip images</string>
<string name="pref_hardware_bitmap_threshold">Custom hardware bitmap threshold</string>
<string name="pref_hardware_bitmap_threshold_summary">If reader loads a blank image incrementally reduce the threshold.\nSelected: %s</string>
<string name="pref_crop_borders">Crop borders</string>
<string name="pref_custom_brightness">Custom brightness</string>
<string name="pref_grayscale">Grayscale</string>

View File

@ -42,5 +42,7 @@ dependencies {
implementation(compose.ui.tooling.preview)
implementation(compose.ui.util)
implementation(androidx.paging.runtime)
implementation(androidx.paging.compose)
implementation(kotlinx.immutables)
}

View File

@ -0,0 +1,16 @@
package mihon.presentation.core.util
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@Composable
fun <T: Any> StateFlow<Flow<PagingData<T>>>.collectAsLazyPagingItems(): LazyPagingItems<T> {
val flow by collectAsState()
return flow.collectAsLazyPagingItems()
}