Improve performance across screens (#4071)

* Keep screens loaded while they're hidden and rebuild the library faster

* Count chapters in SQL for the migration list

* Show the extension list sooner and check trust against loaded stores

* Cancel the previous tracker search when searching again

* Write reader progress less often and keep its work off the main thread

* Stop rebuilding the settings search index on every key

* Watch updates for widgets only while one is placed

The query re-ran on every chapter or manga write even with no widget on
the home screen.

* Observe download pages and limit how often their progress updates

* Throttle the library update notification and write fetch data together
This commit is contained in:
AntsyLich
2026-10-11 04:28:52 +00:00
committed by GitHub
parent b15af71b94
commit 260e31cd2c
33 changed files with 458 additions and 136 deletions
@@ -0,0 +1,60 @@
package tachiyomi.core.common.util.lang
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingCommand
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration
/**
* Keeps a view model's flows running while its screen is hidden, but holds back database and other frequent
* updates until the screen is visible again, so returning to it shows the last state instead of re-running
* everything. Flows collected without a gate in their context are unaffected.
*/
class SubscriberGate : AbstractCoroutineContextElement(SubscriberGate) {
companion object Key : CoroutineContext.Key<SubscriberGate>
private val open = MutableStateFlow(true)
fun whileSubscribed(stopTimeout: Duration): SharingStarted = object : SharingStarted {
override fun command(subscriptionCount: StateFlow<Int>): Flow<SharingCommand> = flow {
emit(SharingCommand.START)
subscriptionCount
.map { it > 0 }
.distinctUntilChanged()
.collectLatest { subscribed ->
if (!subscribed) delay(stopTimeout)
open.value = subscribed
}
}
}
internal suspend fun awaitOpen() {
open.first { it }
}
}
fun <T> Flow<T>.pausedWhileUnsubscribed(): Flow<T> = flow {
val gate = currentCoroutineContext()[SubscriberGate]
if (gate == null) {
emitAll(this@pausedWhileUnsubscribed)
} else {
this@pausedWhileUnsubscribed.conflate().collect {
gate.awaitOpen()
emit(it)
}
}
}
@@ -318,8 +318,16 @@ object ImageUtil {
null
}
if (image == null) return Color.WHITE.toDrawable()
return try {
chooseBackground(context, image)
} finally {
image.recycle()
}
}
private fun chooseBackground(context: Context, image: Bitmap): Drawable {
val whiteColor = Color.WHITE
if (image == null) return whiteColor.toDrawable()
if (image.width < 50 || image.height < 50) {
return whiteColor.toDrawable()
}