Compare commits

...

2 Commits

Author SHA1 Message Date
Jobobby04 509ace1a38 Revert "Reapply "Fix thread starvation caused by not yielding or using an inappropriate thread pool (#2955)""
This reverts commit 9c01119d24.

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt
2026-04-08 15:02:37 -04:00
Jobobby04 170358f88e Revert "Reapply "Fix cache invalidation isn't done at startup (#2970)""
This reverts commit c17e9573b7.
2026-04-08 14:52:15 -04:00
18 changed files with 139 additions and 250 deletions
@@ -223,7 +223,6 @@ object SettingsAdvancedScreen : SearchableSettings {
private fun getDataGroup(): Preference.PreferenceGroup { private fun getDataGroup(): Preference.PreferenceGroup {
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(MR.strings.label_data), title = stringResource(MR.strings.label_data),
@@ -232,10 +231,8 @@ object SettingsAdvancedScreen : SearchableSettings {
title = stringResource(MR.strings.pref_invalidate_download_cache), title = stringResource(MR.strings.pref_invalidate_download_cache),
subtitle = stringResource(MR.strings.pref_invalidate_download_cache_summary), subtitle = stringResource(MR.strings.pref_invalidate_download_cache_summary),
onClick = { onClick = {
scope.launch {
Injekt.get<DownloadCache>().invalidateCache() Injekt.get<DownloadCache>().invalidateCache()
context.toast(MR.strings.download_cache_invalidated) context.toast(MR.strings.download_cache_invalidated)
}
}, },
), ),
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
@@ -303,10 +303,7 @@ object SettingsDataScreen : SearchableSettings {
val chapterCache = remember { Injekt.get<ChapterCache>() } val chapterCache = remember { Injekt.get<ChapterCache>() }
var cacheReadableSizeSema by remember { mutableIntStateOf(0) } var cacheReadableSizeSema by remember { mutableIntStateOf(0) }
var cacheReadableSize by remember { mutableStateOf(context.stringResource(MR.strings.calculating)) } val cacheReadableSize = remember(cacheReadableSizeSema) { chapterCache.readableSize }
LaunchedEffect(cacheReadableSizeSema) {
cacheReadableSize = chapterCache.getReadableSize()
}
// SY --> // SY -->
val pagePreviewCache = remember { Injekt.get<PagePreviewCache>() } val pagePreviewCache = remember { Injekt.get<PagePreviewCache>() }
@@ -9,18 +9,12 @@ import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@@ -51,24 +45,10 @@ private fun StorageInfo(
) { ) {
val context = LocalContext.current val context = LocalContext.current
var available by remember(file) { mutableStateOf(-1L) } val available = remember(file) { DiskUtil.getAvailableStorageSpace(file) }
var total by remember(file) { mutableStateOf(-1L) } val availableText = remember(available) { Formatter.formatFileSize(context, available) }
val total = remember(file) { DiskUtil.getTotalStorageSpace(file) }
LaunchedEffect(file) { val totalText = remember(total) { Formatter.formatFileSize(context, total) }
available = withContext(Dispatchers.IO) { DiskUtil.getAvailableStorageSpace(file) }
total = withContext(Dispatchers.IO) { DiskUtil.getTotalStorageSpace(file) }
}
val availableText = if (available == -1L) {
stringResource(MR.strings.calculating)
} else {
Formatter.formatFileSize(context, available)
}
val totalText = if (total == -1L) {
stringResource(MR.strings.calculating)
} else {
Formatter.formatFileSize(context, total)
}
Column( Column(
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
@@ -78,7 +58,6 @@ private fun StorageInfo(
style = MaterialTheme.typography.header, style = MaterialTheme.typography.header,
) )
if (total > 0) {
LinearProgressIndicator( LinearProgressIndicator(
modifier = Modifier modifier = Modifier
.clip(MaterialTheme.shapes.small) .clip(MaterialTheme.shapes.small)
@@ -86,7 +65,6 @@ private fun StorageInfo(
.height(12.dp), .height(12.dp),
progress = { (1 - (available / total.toFloat())) }, progress = { (1 - (available / total.toFloat())) },
) )
}
Text( Text(
text = stringResource(MR.strings.available_disk_space_info, availableText, totalText), text = stringResource(MR.strings.available_disk_space_info, availableText, totalText),
@@ -13,7 +13,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import logcat.LogPriority import logcat.LogPriority
import okhttp3.Response import okhttp3.Response
@@ -64,13 +63,17 @@ class ChapterCache(
*/ */
private val cacheDir: File = diskCache.directory private val cacheDir: File = diskCache.directory
/**
* Returns real size of directory.
*/
private val realSize: Long
get() = DiskUtil.getDirectorySize(cacheDir)
/** /**
* Returns real size of directory in human readable format. * Returns real size of directory in human readable format.
*/ */
suspend fun getReadableSize(): String = withContext(Dispatchers.IO) { val readableSize: String
val size = DiskUtil.getDirectorySize(cacheDir) get() = Formatter.formatFileSize(context, realSize)
Formatter.formatFileSize(context, size)
}
// --> EH // --> EH
// Cache size is in MB // Cache size is in MB
@@ -12,17 +12,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
@@ -112,19 +109,13 @@ class DownloadCache(
ProtoBuf.decodeFromByteArray<RootDirectory>(it.readBytes()) ProtoBuf.decodeFromByteArray<RootDirectory>(it.readBytes())
} }
rootDownloadsDir = diskCache rootDownloadsDir = diskCache
lastRenew = System.currentTimeMillis()
} }
} catch (e: Throwable) { } catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Failed to initialize from disk cache" } logcat(LogPriority.ERROR, e) { "Failed to initialize from disk cache" }
diskCacheFile.delete() diskCacheFile.delete()
} }
} }
sourceManager.catalogueSources
.map { sources -> sources.map { it.id }.toSet() }
.distinctUntilChanged()
.collect {
restartRenewal()
}
} }
storageManager.changes storageManager.changes
@@ -362,34 +353,19 @@ class DownloadCache(
notifyChanges() notifyChanges()
} }
suspend fun invalidateCache() { fun invalidateCache() {
renewalJob?.cancelAndJoin()
diskCacheFile.delete()
lastRenew = 0L lastRenew = 0L
renewCache(forceRenew = true)
}
/**
* Safely cancels any in-progress renewal job, resets the last-renew timestamp, and
* immediately starts a new renewal, bypassing the time-based throttle.
*/
private fun restartRenewal() {
renewalJob?.cancel() renewalJob?.cancel()
lastRenew = 0L diskCacheFile.delete()
renewCache(forceRenew = true) renewCache()
} }
/** /**
* Renews the downloads cache. * Renews the downloads cache.
*
* @param forceRenew when `true`, the time-based throttle is bypassed. Use this after
* explicitly cancelling the previous job to avoid a race where the cancelled job's
* [invokeOnCompletion] handler sets [lastRenew] after the reset but before the new
* job's guard check.
*/ */
private fun renewCache(forceRenew: Boolean = false) { private fun renewCache() {
// Avoid renewing cache if in the process nor too often // Avoid renewing cache if in the process nor too often
if ((!forceRenew && lastRenew + renewInterval >= System.currentTimeMillis()) || renewalJob?.isActive == true) { if (lastRenew + renewInterval >= System.currentTimeMillis() || renewalJob?.isActive == true) {
return return
} }
@@ -400,14 +376,15 @@ class DownloadCache(
// Try to wait until extensions and sources have loaded // Try to wait until extensions and sources have loaded
// SY --> // SY -->
var sources = emptyList<Source>()
withTimeoutOrNull(30.seconds) { withTimeoutOrNull(30.seconds) {
// SY <-- extensionManager.isInitialized.first { it }
sourceManager.catalogueSources.first { it.isNotEmpty() } sourceManager.isInitialized.first { it }
// SY -->
sources = getSources()
} }
// SY <-- // SY <--
val sources = getSources()
val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id } val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id }
rootDownloadsDirMutex.withLock { rootDownloadsDirMutex.withLock {
@@ -482,9 +459,8 @@ class DownloadCache(
private var updateDiskCacheJob: Job? = null private var updateDiskCacheJob: Job? = null
private fun updateDiskCache() { private fun updateDiskCache() {
val previousJob = updateDiskCacheJob updateDiskCacheJob?.cancel()
updateDiskCacheJob = scope.launchIO { updateDiskCacheJob = scope.launchIO {
previousJob?.cancelAndJoin()
delay(1000) delay(1000)
ensureActive() ensureActive()
val bytes = ProtoBuf.encodeToByteArray(rootDownloadsDir) val bytes = ProtoBuf.encodeToByteArray(rootDownloadsDir)
@@ -109,10 +109,10 @@ class DownloadManager(
return queueState.value.find { it.chapter.id == chapterId } return queueState.value.find { it.chapter.id == chapterId }
} }
suspend fun startDownloadNow(chapterId: Long) { fun startDownloadNow(chapterId: Long) {
val existingDownload = getQueuedDownloadOrNull(chapterId) val existingDownload = getQueuedDownloadOrNull(chapterId)
// If not in queue try to start a new download // If not in queue try to start a new download
val toAdd = existingDownload ?: Download.fromChapterId(chapterId) ?: return val toAdd = existingDownload ?: runBlocking { Download.fromChapterId(chapterId) } ?: return
queueState.value.toMutableList().apply { queueState.value.toMutableList().apply {
existingDownload?.let { remove(it) } existingDownload?.let { remove(it) }
add(0, toAdd) add(0, toAdd)
@@ -89,7 +89,7 @@ class DownloadStore(
/** /**
* Returns the list of downloads to restore. It should be called in a background thread. * Returns the list of downloads to restore. It should be called in a background thread.
*/ */
suspend fun restore(): List<Download> { fun restore(): List<Download> {
val objs = preferences.all val objs = preferences.all
.mapNotNull { it.value as? String } .mapNotNull { it.value as? String }
.mapNotNull { deserialize(it) } .mapNotNull { deserialize(it) }
@@ -100,10 +100,10 @@ class DownloadStore(
val cachedManga = mutableMapOf<Long, Manga?>() val cachedManga = mutableMapOf<Long, Manga?>()
for ((mangaId, chapterId) in objs) { for ((mangaId, chapterId) in objs) {
val manga = cachedManga.getOrPut(mangaId) { val manga = cachedManga.getOrPut(mangaId) {
getManga.await(mangaId) runBlocking { getManga.await(mangaId) }
} ?: continue } ?: continue
val source = sourceManager.get(manga.source) as? HttpSource ?: continue val source = sourceManager.get(manga.source) as? HttpSource ?: continue
val chapter = getChapter.await(chapterId) ?: continue val chapter = runBlocking { getChapter.await(chapterId) } ?: continue
downloads.add(Download(source, manga, chapter)) downloads.add(Download(source, manga, chapter))
} }
} }
@@ -25,6 +25,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
@@ -49,6 +50,7 @@ import okhttp3.Response
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.extension import tachiyomi.core.common.storage.extension
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNow
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
@@ -119,9 +121,9 @@ class Downloader(
var isPaused: Boolean = false var isPaused: Boolean = false
init { init {
scope.launch { launchNow {
val chapters = store.restore() val chapters = async { store.restore() }
addAllToQueue(chapters) addAllToQueue(chapters.await())
} }
} }
@@ -23,7 +23,6 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import tachiyomi.core.common.Constants import tachiyomi.core.common.Constants
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.domain.chapter.interactor.GetChapter import tachiyomi.domain.chapter.interactor.GetChapter
import tachiyomi.domain.chapter.interactor.UpdateChapter import tachiyomi.domain.chapter.interactor.UpdateChapter
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
@@ -85,18 +84,11 @@ class NotificationReceiver : BroadcastReceiver() {
ACTION_CANCEL_APP_UPDATE_DOWNLOAD -> cancelDownloadAppUpdate(context) ACTION_CANCEL_APP_UPDATE_DOWNLOAD -> cancelDownloadAppUpdate(context)
// Open reader activity // Open reader activity
ACTION_OPEN_CHAPTER -> { ACTION_OPEN_CHAPTER -> {
val pendingResult = goAsync()
launchIO {
try {
openChapter( openChapter(
context, context,
intent.getLongExtra(EXTRA_MANGA_ID, -1), intent.getLongExtra(EXTRA_MANGA_ID, -1),
intent.getLongExtra(EXTRA_CHAPTER_ID, -1), intent.getLongExtra(EXTRA_CHAPTER_ID, -1),
) )
} finally {
pendingResult.finish()
}
}
} }
// Mark updated manga chapters as read // Mark updated manga chapters as read
ACTION_MARK_AS_READ -> { ACTION_MARK_AS_READ -> {
@@ -161,10 +153,9 @@ class NotificationReceiver : BroadcastReceiver() {
* @param mangaId id of manga * @param mangaId id of manga
* @param chapterId id of chapter * @param chapterId id of chapter
*/ */
private suspend fun openChapter(context: Context, mangaId: Long, chapterId: Long) { private fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
val manga = getManga.await(mangaId) val manga = runBlocking { getManga.await(mangaId) }
val chapter = getChapter.await(chapterId) val chapter = runBlocking { getChapter.await(chapterId) }
withUIContext {
if (manga != null && chapter != null) { if (manga != null && chapter != null) {
val intent = ReaderActivity.newIntent(context, manga.id, chapter.id).apply { val intent = ReaderActivity.newIntent(context, manga.id, chapter.id).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
@@ -174,7 +165,6 @@ class NotificationReceiver : BroadcastReceiver() {
context.toast(MR.strings.chapter_error) context.toast(MR.strings.chapter_error)
} }
} }
}
/** /**
* Method called when user wants to stop a backup restore job. * Method called when user wants to stop a backup restore job.
@@ -30,7 +30,6 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.emptyFlow 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 kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
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
@@ -141,7 +140,6 @@ class ExtensionManager(
* Loads and registers the installed extensions. * Loads and registers the installed extensions.
*/ */
private fun initExtensions() { private fun initExtensions() {
scope.launch {
val extensions = ExtensionLoader.loadExtensions(context) val extensions = ExtensionLoader.loadExtensions(context)
installedExtensionMapFlow.value = extensions installedExtensionMapFlow.value = extensions
@@ -157,7 +155,6 @@ class ExtensionManager(
_isInitialized.value = true _isInitialized.value = true
} }
}
// EXH --> // EXH -->
private fun <T : Extension> Map<String, T>.filterNotBlacklisted(): Map<String, T> { private fun <T : Extension> Map<String, T>.filterNotBlacklisted(): Map<String, T> {
@@ -18,7 +18,6 @@ import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo
import eu.kanade.tachiyomi.util.system.ChildFirstPathClassLoader import eu.kanade.tachiyomi.util.system.ChildFirstPathClassLoader
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
@@ -115,7 +114,7 @@ internal object ExtensionLoader {
* *
* @param context The application context. * @param context The application context.
*/ */
suspend fun loadExtensions(context: Context): List<LoadResult> { fun loadExtensions(context: Context): List<LoadResult> {
val pkgManager = context.packageManager val pkgManager = context.packageManager
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -161,10 +160,11 @@ internal object ExtensionLoader {
if (extPkgs.isEmpty()) return emptyList() if (extPkgs.isEmpty()) return emptyList()
// Load each extension concurrently and wait for completion // Load each extension concurrently and wait for completion
return coroutineScope { return runBlocking {
extPkgs.map { val deferred = extPkgs.map {
async { loadExtension(context, it) } async { loadExtension(context, it) }
}.awaitAll() }
deferred.awaitAll()
} }
} }
@@ -163,6 +163,13 @@ class MainActivity : BaseActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val didMigration = if (isLaunch) {
addAnalytics()
Migrator.awaitAndRelease()
} else {
false
}
// Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079 // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079
if (!isTaskRoot) { if (!isTaskRoot) {
finish() finish()
@@ -175,12 +182,6 @@ class MainActivity : BaseActivity() {
// SY <-- // SY <--
setComposeContent { setComposeContent {
var didMigration by remember { mutableStateOf<Boolean?>(null) }
LaunchedEffect(Unit) {
addAnalytics()
didMigration = Migrator.awaitAndRelease()
}
val context = LocalContext.current val context = LocalContext.current
var incognito by remember { mutableStateOf(getIncognitoState.await(null)) } var incognito by remember { mutableStateOf(getIncognitoState.await(null)) }
@@ -308,7 +309,7 @@ class MainActivity : BaseActivity() {
} }
// SY <-- // SY <--
var showChangelog by remember { mutableStateOf(didMigration == true && !BuildConfig.DEBUG) } var showChangelog by remember { mutableStateOf(didMigration && !BuildConfig.DEBUG) }
if (showChangelog) { if (showChangelog) {
// SY --> // SY -->
WhatsNewDialog(onDismissRequest = { showChangelog = false }) WhatsNewDialog(onDismissRequest = { showChangelog = false })
@@ -32,7 +32,6 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -79,7 +78,6 @@ import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.AddToLibraryFirst import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.AddToLibraryFirst
import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.Error import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.Error
import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.Success import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.Success
import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterItem
import eu.kanade.tachiyomi.ui.reader.loader.HttpPageLoader import eu.kanade.tachiyomi.ui.reader.loader.HttpPageLoader
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
@@ -103,8 +101,6 @@ import exh.source.isEhBasedSource
import exh.ui.ifSourcesLoaded import exh.ui.ifSourcesLoaded
import exh.util.defaultReaderType import exh.util.defaultReaderType
import exh.util.mangaType import exh.util.mangaType
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
@@ -125,7 +121,6 @@ import tachiyomi.core.common.i18n.pluralStringResource
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable import tachiyomi.core.common.util.lang.launchNonCancellable
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.logcat import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
@@ -399,37 +394,29 @@ class ReaderActivity : BaseActivity() {
is ReaderViewModel.Dialog.ChapterList -> { is ReaderViewModel.Dialog.ChapterList -> {
var chapters by remember { var chapters by remember {
mutableStateOf<ImmutableList<ReaderChapterItem>?>(null) mutableStateOf(viewModel.getChapters().toImmutableList())
} }
LaunchedEffect(state.dialog) {
withIOContext {
chapters = viewModel.getChapters().toImmutableList()
}
}
if (chapters != null) {
ChapterListDialog( ChapterListDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
screenModel = settingsScreenModel, screenModel = settingsScreenModel,
chapters = chapters ?: persistentListOf(), chapters = chapters,
onClickChapter = { onClickChapter = {
viewModel.loadNewChapterFromDialog(it) viewModel.loadNewChapterFromDialog(it)
onDismissRequest() onDismissRequest()
}, },
onBookmark = { chapter -> onBookmark = { chapter ->
viewModel.toggleBookmark(chapter.id, !chapter.bookmark) viewModel.toggleBookmark(chapter.id, !chapter.bookmark)
chapters = chapters?.map { chapters = chapters.map {
if (it.chapter.id == chapter.id) { if (it.chapter.id == chapter.id) {
it.copy(chapter = chapter.copy(bookmark = !chapter.bookmark)) it.copy(chapter = chapter.copy(bookmark = !chapter.bookmark))
} else { } else {
it it
} }
}?.toImmutableList() }.toImmutableList()
}, },
state.dateRelativeTime, state.dateRelativeTime,
) )
} }
}
// SY --> // SY -->
ReaderViewModel.Dialog.AutoScrollHelp -> AlertDialog( ReaderViewModel.Dialog.AutoScrollHelp -> AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@@ -604,9 +591,8 @@ class ReaderActivity : BaseActivity() {
} else { } else {
cropBorderContinuousVertical cropBorderContinuousVertical
} }
val readerBottomButtons by remember { val readerBottomButtons by readerPreferences.readerBottomButtons.changes().map { it.toImmutableSet() }
readerPreferences.readerBottomButtons.changes().map { it.toImmutableSet() } .collectAsState(persistentSetOf())
}.collectAsState(persistentSetOf())
val dualPageSplitPaged by readerPreferences.dualPageSplitPaged.collectAsState() val dualPageSplitPaged by readerPreferences.dualPageSplitPaged.collectAsState()
val forceHorizontalSeekbar by readerPreferences.forceHorizontalSeekbar.collectAsState() val forceHorizontalSeekbar by readerPreferences.forceHorizontalSeekbar.collectAsState()
@@ -59,6 +59,7 @@ import exh.source.isEhBasedManga
import exh.util.defaultReaderType import exh.util.defaultReaderType
import exh.util.mangaType import exh.util.mangaType
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -182,26 +183,19 @@ class ReaderViewModel @JvmOverloads constructor(
private var chapterToDownload: Download? = null private var chapterToDownload: Download? = null
private var unfilteredChapterListCache: List<tachiyomi.domain.chapter.model.Chapter>? = null private val unfilteredChapterList by lazy {
private suspend fun getUnfilteredChapterList(): List<tachiyomi.domain.chapter.model.Chapter> {
if (unfilteredChapterListCache == null) {
val manga = manga!! val manga = manga!!
unfilteredChapterListCache = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = false) runBlocking { getChaptersByMangaId.await(manga.id, applyScanlatorFilter = false) }
}
return unfilteredChapterListCache!!
} }
/** /**
* Chapter list for the active manga. It's retrieved lazily and should be accessed for the first * Chapter list for the active manga. It's retrieved lazily and should be accessed for the first
* time in a background thread to avoid blocking the UI. * time in a background thread to avoid blocking the UI.
*/ */
private var chapterListCache: List<ReaderChapter>? = null private val chapterList by lazy {
private suspend fun getChapterList(): List<ReaderChapter> {
chapterListCache?.let { return it }
val manga = manga!! val manga = manga!!
// SY --> // SY -->
val (chapters, mangaMap) = val (chapters, mangaMap) = runBlocking {
if (manga.source == MERGED_SOURCE_ID) { if (manga.source == MERGED_SOURCE_ID) {
getMergedChaptersByMangaId.await(manga.id, applyScanlatorFilter = true) to getMergedChaptersByMangaId.await(manga.id, applyScanlatorFilter = true) to
getMergedMangaById.await(manga.id) getMergedMangaById.await(manga.id)
@@ -209,7 +203,7 @@ class ReaderViewModel @JvmOverloads constructor(
} else { } else {
getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true) to null getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true) to null
} }
}
fun isChapterDownloaded(chapter: Chapter): Boolean { fun isChapterDownloaded(chapter: Chapter): Boolean {
val chapterManga = mangaMap?.get(chapter.mangaId) ?: manga val chapterManga = mangaMap?.get(chapter.mangaId) ?: manga
return downloadManager.isChapterDownloaded( return downloadManager.isChapterDownloaded(
@@ -259,7 +253,7 @@ class ReaderViewModel @JvmOverloads constructor(
else -> chapters else -> chapters
} }
val result = chaptersForReader chaptersForReader
.sortedWith(getChapterSort(manga, sortDescending = false)) .sortedWith(getChapterSort(manga, sortDescending = false))
.run { .run {
if (readerPreferences.skipDupe.get()) { if (readerPreferences.skipDupe.get()) {
@@ -277,8 +271,6 @@ class ReaderViewModel @JvmOverloads constructor(
} }
.map { it.toDbChapter() } .map { it.toDbChapter() }
.map(::ReaderChapter) .map(::ReaderChapter)
chapterListCache = result
return result
} }
private val incognitoMode: Boolean by lazy { getIncognitoState.await(manga?.source) } private val incognitoMode: Boolean by lazy { getIncognitoState.await(manga?.source) }
@@ -417,7 +409,7 @@ class ReaderViewModel @JvmOverloads constructor(
loadChapter( loadChapter(
loader!!, loader!!,
getChapterList().first { chapterId == it.chapter.id }, chapterList.first { chapterId == it.chapter.id },
/* SY --> */page, /* SY <-- */ /* SY --> */page, /* SY <-- */
) )
Result.success(true) Result.success(true)
@@ -435,10 +427,10 @@ class ReaderViewModel @JvmOverloads constructor(
} }
// SY --> // SY -->
suspend fun getChapters(): List<ReaderChapterItem> { fun getChapters(): List<ReaderChapterItem> {
val currentChapter = getCurrentChapter() val currentChapter = getCurrentChapter()
return getChapterList().map { return chapterList.map {
ReaderChapterItem( ReaderChapterItem(
chapter = it.chapter.toDomainChapter()!!, chapter = it.chapter.toDomainChapter()!!,
manga = manga!!, manga = manga!!,
@@ -462,7 +454,6 @@ class ReaderViewModel @JvmOverloads constructor(
): ViewerChapters { ): ViewerChapters {
loader.loadChapter(chapter /* SY --> */, page/* SY <-- */) loader.loadChapter(chapter /* SY --> */, page/* SY <-- */)
val chapterList = getChapterList()
val chapterPos = chapterList.indexOf(chapter) val chapterPos = chapterList.indexOf(chapter)
val newChapters = ViewerChapters( val newChapters = ViewerChapters(
chapter, chapter,
@@ -512,7 +503,7 @@ class ReaderViewModel @JvmOverloads constructor(
fun loadNewChapterFromDialog(chapter: Chapter) { fun loadNewChapterFromDialog(chapter: Chapter) {
viewModelScope.launchIO { viewModelScope.launchIO {
val newChapter = getChapterList().firstOrNull { it.chapter.id == chapter.id } ?: return@launchIO val newChapter = chapterList.firstOrNull { it.chapter.id == chapter.id } ?: return@launchIO
loadAdjacent(newChapter) loadAdjacent(newChapter)
} }
} }
@@ -674,12 +665,11 @@ class ReaderViewModel @JvmOverloads constructor(
* If both conditions are satisfied enqueues chapter for delete * If both conditions are satisfied enqueues chapter for delete
* @param currentChapter current chapter, which is going to be marked as read. * @param currentChapter current chapter, which is going to be marked as read.
*/ */
private suspend fun deleteChapterIfNeeded(currentChapter: ReaderChapter) { private fun deleteChapterIfNeeded(currentChapter: ReaderChapter) {
val removeAfterReadSlots = downloadPreferences.removeAfterReadSlots.get() val removeAfterReadSlots = downloadPreferences.removeAfterReadSlots.get()
if (removeAfterReadSlots == -1) return if (removeAfterReadSlots == -1) return
// Determine which chapter should be deleted and enqueue // Determine which chapter should be deleted and enqueue
val chapterList = getChapterList()
val currentChapterPosition = chapterList.indexOf(currentChapter) val currentChapterPosition = chapterList.indexOf(currentChapter)
val chapterToDelete = chapterList.getOrNull(currentChapterPosition - removeAfterReadSlots) val chapterToDelete = chapterList.getOrNull(currentChapterPosition - removeAfterReadSlots)
@@ -749,7 +739,7 @@ class ReaderViewModel @JvmOverloads constructor(
// SY --> // SY -->
if (manga?.isEhBasedManga() == true) { if (manga?.isEhBasedManga() == true) {
viewModelScope.launchNonCancellable { viewModelScope.launchNonCancellable {
val chapterUpdates = getUnfilteredChapterList() val chapterUpdates = unfilteredChapterList
.filter { it.sourceOrder > readerChapter.chapter.source_order } .filter { it.sourceOrder > readerChapter.chapter.source_order }
.map { chapter -> .map { chapter ->
ChapterUpdate( ChapterUpdate(
@@ -769,7 +759,7 @@ class ReaderViewModel @JvmOverloads constructor(
.contains(LibraryPreferences.MARK_DUPLICATE_CHAPTER_READ_EXISTING) .contains(LibraryPreferences.MARK_DUPLICATE_CHAPTER_READ_EXISTING)
if (!markDuplicateAsRead) return if (!markDuplicateAsRead) return
val duplicateUnreadChapters = getUnfilteredChapterList() val duplicateUnreadChapters = unfilteredChapterList
.mapNotNull { chapter -> .mapNotNull { chapter ->
if ( if (
!chapter.read && !chapter.read &&
@@ -784,7 +774,7 @@ class ReaderViewModel @JvmOverloads constructor(
updateChapter.awaitAll(duplicateUnreadChapters) updateChapter.awaitAll(duplicateUnreadChapters)
// SY --> // SY -->
duplicateUnreadChapters.forEach { chapterUpdate -> duplicateUnreadChapters.forEach { chapterUpdate ->
val chapter = getUnfilteredChapterList().first { it.id == chapterUpdate.id } val chapter = unfilteredChapterList.first { it.id == chapterUpdate.id }
deleteChapterIfNeeded(ReaderChapter(chapter)) deleteChapterIfNeeded(ReaderChapter(chapter))
} }
// SY <-- // SY <--
@@ -873,9 +863,9 @@ class ReaderViewModel @JvmOverloads constructor(
// SY --> // SY -->
fun toggleBookmark(chapterId: Long, bookmarked: Boolean) { fun toggleBookmark(chapterId: Long, bookmarked: Boolean) {
viewModelScope.launchNonCancellable { val chapter = chapterList.find { it.chapter.id == chapterId }?.chapter ?: return
val chapter = getChapterList().find { it.chapter.id == chapterId }?.chapter ?: return@launchNonCancellable
chapter.bookmark = bookmarked chapter.bookmark = bookmarked
viewModelScope.launchNonCancellable {
updateChapter.await( updateChapter.await(
ChapterUpdate( ChapterUpdate(
id = chapterId, id = chapterId,
@@ -910,7 +900,7 @@ class ReaderViewModel @JvmOverloads constructor(
*/ */
fun setMangaReadingMode(readingMode: ReadingMode) { fun setMangaReadingMode(readingMode: ReadingMode) {
val manga = manga ?: return val manga = manga ?: return
viewModelScope.launchIO { runBlocking(Dispatchers.IO) {
setMangaViewerFlags.awaitSetReadingMode(manga.id, readingMode.flagValue.toLong()) setMangaViewerFlags.awaitSetReadingMode(manga.id, readingMode.flagValue.toLong())
val currChapters = state.value.viewerChapters val currChapters = state.value.viewerChapters
if (currChapters != null) { if (currChapters != null) {
@@ -251,7 +251,7 @@ class UpdatesScreenModel(
} }
} }
private suspend fun startDownloadingNow(chapterId: Long) { private fun startDownloadingNow(chapterId: Long) {
downloadManager.startDownloadNow(chapterId) downloadManager.startDownloadNow(chapterId)
} }
@@ -35,7 +35,7 @@ object Migrator {
result = null result = null
} }
suspend fun awaitAndRelease(): Boolean { fun awaitAndRelease(): Boolean = runBlocking {
return await().also { release() } await().also { release() }
} }
} }
@@ -5,8 +5,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.asContextElement import kotlinx.coroutines.asContextElement
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.concurrent.RejectedExecutionException import java.util.concurrent.RejectedExecutionException
import kotlin.concurrent.atomics.AtomicInt import kotlin.concurrent.atomics.AtomicInt
@@ -19,10 +17,6 @@ import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
import kotlin.coroutines.resume import kotlin.coroutines.resume
// Global mutex to serialize transaction entry and prevent thread pool exhaustion.
// If you have multiple distinct database files/handlers, this should be a property of AndroidDatabaseHandler.
private val transactionMutex = Mutex()
/** /**
* Returns the transaction dispatcher if we are on a transaction, or the database dispatchers. * Returns the transaction dispatcher if we are on a transaction, or the database dispatchers.
*/ */
@@ -45,32 +39,12 @@ internal suspend fun AndroidDatabaseHandler.getCurrentDatabaseContext(): Corouti
* The dispatcher used to execute the given [block] will utilize threads from SQLDelight's query executor. * The dispatcher used to execute the given [block] will utilize threads from SQLDelight's query executor.
*/ */
internal suspend fun <T> AndroidDatabaseHandler.withTransaction(block: suspend () -> T): T { internal suspend fun <T> AndroidDatabaseHandler.withTransaction(block: suspend () -> T): T {
val transactionElement = coroutineContext[TransactionElement] // Use inherited transaction context if available, this allows nested suspending transactions.
val transactionContext =
// If we are already in a transaction, we don't need to lock the Mutex. coroutineContext[TransactionElement]?.transactionDispatcher ?: createTransactionContext()
// We just reuse the existing thread/context. return withContext(transactionContext) {
if (transactionElement != null) { val transactionElement = coroutineContext[TransactionElement]!!
return withContext(transactionElement.transactionDispatcher) {
transactionElement.acquire() transactionElement.acquire()
try {
db.transactionWithResult {
runBlocking(transactionElement.transactionDispatcher) {
block()
}
}
} finally {
transactionElement.release()
}
}
}
// transaction: Acquire Mutex BEFORE acquiring a thread.
// This ensures we only block a real thread when we have exclusive access.
return transactionMutex.withLock {
val transactionContext = createTransactionContext()
withContext(transactionContext) {
val element = coroutineContext[TransactionElement]!!
element.acquire()
try { try {
db.transactionWithResult { db.transactionWithResult {
runBlocking(transactionContext) { runBlocking(transactionContext) {
@@ -78,8 +52,7 @@ internal suspend fun <T> AndroidDatabaseHandler.withTransaction(block: suspend (
} }
} }
} finally { } finally {
element.release() transactionElement.release()
}
} }
} }
} }
@@ -170,7 +170,6 @@
<!-- Operations --> <!-- Operations -->
<string name="loading">Loading…</string> <string name="loading">Loading…</string>
<string name="calculating">Calculating…</string>
<string name="internal_error">InternalError: Check crash logs for further information</string> <string name="internal_error">InternalError: Check crash logs for further information</string>
<!-- Shortcuts--> <!-- Shortcuts-->