Reapply "Fix thread starvation caused by not yielding or using an inappropriate thread pool (#2955)"
This reverts commit 1d7c838ae64e624d9dd0884722f0c6ae5d18e386. # Conflicts: # CHANGELOG.md # app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt # app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt # app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt # app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt
This commit is contained in:
@@ -303,7 +303,10 @@ 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) }
|
||||||
val cacheReadableSize = remember(cacheReadableSizeSema) { chapterCache.readableSize }
|
var cacheReadableSize by remember { mutableStateOf(context.stringResource(MR.strings.calculating)) }
|
||||||
|
LaunchedEffect(cacheReadableSizeSema) {
|
||||||
|
cacheReadableSize = chapterCache.getReadableSize()
|
||||||
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
val pagePreviewCache = remember { Injekt.get<PagePreviewCache>() }
|
val pagePreviewCache = remember { Injekt.get<PagePreviewCache>() }
|
||||||
|
|||||||
@@ -9,12 +9,18 @@ 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
|
||||||
@@ -45,10 +51,24 @@ private fun StorageInfo(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val available = remember(file) { DiskUtil.getAvailableStorageSpace(file) }
|
var available by remember(file) { mutableStateOf(-1L) }
|
||||||
val availableText = remember(available) { Formatter.formatFileSize(context, available) }
|
var total by remember(file) { mutableStateOf(-1L) }
|
||||||
val total = remember(file) { DiskUtil.getTotalStorageSpace(file) }
|
|
||||||
val totalText = remember(total) { Formatter.formatFileSize(context, total) }
|
LaunchedEffect(file) {
|
||||||
|
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),
|
||||||
@@ -58,6 +78,7 @@ 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)
|
||||||
@@ -65,6 +86,7 @@ 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,6 +13,7 @@ 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
|
||||||
@@ -63,17 +64,13 @@ 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.
|
||||||
*/
|
*/
|
||||||
val readableSize: String
|
suspend fun getReadableSize(): String = withContext(Dispatchers.IO) {
|
||||||
get() = Formatter.formatFileSize(context, realSize)
|
val size = DiskUtil.getDirectorySize(cacheDir)
|
||||||
|
Formatter.formatFileSize(context, size)
|
||||||
|
}
|
||||||
|
|
||||||
// --> EH
|
// --> EH
|
||||||
// Cache size is in MB
|
// Cache size is in MB
|
||||||
|
|||||||
@@ -109,10 +109,10 @@ class DownloadManager(
|
|||||||
return queueState.value.find { it.chapter.id == chapterId }
|
return queueState.value.find { it.chapter.id == chapterId }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDownloadNow(chapterId: Long) {
|
suspend 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 ?: runBlocking { Download.fromChapterId(chapterId) } ?: return
|
val toAdd = existingDownload ?: 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.
|
||||||
*/
|
*/
|
||||||
fun restore(): List<Download> {
|
suspend 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) {
|
||||||
runBlocking { getManga.await(mangaId) }
|
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 = runBlocking { getChapter.await(chapterId) } ?: continue
|
val chapter = getChapter.await(chapterId) ?: continue
|
||||||
downloads.add(Download(source, manga, chapter))
|
downloads.add(Download(source, manga, chapter))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,9 +121,9 @@ class Downloader(
|
|||||||
var isPaused: Boolean = false
|
var isPaused: Boolean = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
launchNow {
|
scope.launch {
|
||||||
val chapters = async { store.restore() }
|
val chapters = store.restore()
|
||||||
addAllToQueue(chapters.await())
|
addAllToQueue(chapters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ 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
|
||||||
@@ -84,11 +85,18 @@ 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 -> {
|
||||||
@@ -153,9 +161,10 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
* @param mangaId id of manga
|
* @param mangaId id of manga
|
||||||
* @param chapterId id of chapter
|
* @param chapterId id of chapter
|
||||||
*/
|
*/
|
||||||
private fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
|
private suspend fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
|
||||||
val manga = runBlocking { getManga.await(mangaId) }
|
val manga = getManga.await(mangaId)
|
||||||
val chapter = runBlocking { getChapter.await(chapterId) }
|
val chapter = 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
|
||||||
@@ -165,6 +174,7 @@ 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,6 +30,7 @@ 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
|
||||||
@@ -140,6 +141,7 @@ 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
|
||||||
@@ -155,6 +157,7 @@ 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,6 +18,7 @@ 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
|
||||||
@@ -114,7 +115,7 @@ internal object ExtensionLoader {
|
|||||||
*
|
*
|
||||||
* @param context The application context.
|
* @param context The application context.
|
||||||
*/
|
*/
|
||||||
fun loadExtensions(context: Context): List<LoadResult> {
|
suspend 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) {
|
||||||
@@ -160,11 +161,10 @@ 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 runBlocking {
|
return coroutineScope {
|
||||||
val deferred = extPkgs.map {
|
extPkgs.map {
|
||||||
async { loadExtension(context, it) }
|
async { loadExtension(context, it) }
|
||||||
}
|
}.awaitAll()
|
||||||
deferred.awaitAll()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -163,13 +163,6 @@ 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()
|
||||||
@@ -182,6 +175,12 @@ 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)) }
|
||||||
@@ -309,7 +308,7 @@ class MainActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
var showChangelog by remember { mutableStateOf(didMigration && !BuildConfig.DEBUG) }
|
var showChangelog by remember { mutableStateOf(didMigration == true && !BuildConfig.DEBUG) }
|
||||||
if (showChangelog) {
|
if (showChangelog) {
|
||||||
// SY -->
|
// SY -->
|
||||||
WhatsNewDialog(onDismissRequest = { showChangelog = false })
|
WhatsNewDialog(onDismissRequest = { showChangelog = false })
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ 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
|
||||||
@@ -78,6 +79,7 @@ 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
|
||||||
@@ -101,6 +103,8 @@ 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
|
||||||
@@ -121,6 +125,7 @@ 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
|
||||||
@@ -394,29 +399,37 @@ class ReaderActivity : BaseActivity() {
|
|||||||
|
|
||||||
is ReaderViewModel.Dialog.ChapterList -> {
|
is ReaderViewModel.Dialog.ChapterList -> {
|
||||||
var chapters by remember {
|
var chapters by remember {
|
||||||
mutableStateOf(viewModel.getChapters().toImmutableList())
|
mutableStateOf<ImmutableList<ReaderChapterItem>?>(null)
|
||||||
}
|
}
|
||||||
|
LaunchedEffect(state.dialog) {
|
||||||
|
withIOContext {
|
||||||
|
chapters = viewModel.getChapters().toImmutableList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chapters != null) {
|
||||||
ChapterListDialog(
|
ChapterListDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
screenModel = settingsScreenModel,
|
screenModel = settingsScreenModel,
|
||||||
chapters = chapters,
|
chapters = chapters ?: persistentListOf(),
|
||||||
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,
|
||||||
@@ -591,8 +604,9 @@ class ReaderActivity : BaseActivity() {
|
|||||||
} else {
|
} else {
|
||||||
cropBorderContinuousVertical
|
cropBorderContinuousVertical
|
||||||
}
|
}
|
||||||
val readerBottomButtons by readerPreferences.readerBottomButtons().changes().map { it.toImmutableSet() }
|
val readerBottomButtons by remember {
|
||||||
.collectAsState(persistentSetOf())
|
readerPreferences.readerBottomButtons().changes().map { it.toImmutableSet() }
|
||||||
|
}.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,7 +59,6 @@ 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
|
||||||
@@ -183,19 +182,26 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
|
|
||||||
private var chapterToDownload: Download? = null
|
private var chapterToDownload: Download? = null
|
||||||
|
|
||||||
private val unfilteredChapterList by lazy {
|
private var unfilteredChapterListCache: List<tachiyomi.domain.chapter.model.Chapter>? = null
|
||||||
|
private suspend fun getUnfilteredChapterList(): List<tachiyomi.domain.chapter.model.Chapter> {
|
||||||
|
if (unfilteredChapterListCache == null) {
|
||||||
val manga = manga!!
|
val manga = manga!!
|
||||||
runBlocking { getChaptersByMangaId.await(manga.id, applyScanlatorFilter = false) }
|
unfilteredChapterListCache = 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 val chapterList by lazy {
|
private var chapterListCache: List<ReaderChapter>? = null
|
||||||
|
private suspend fun getChapterList(): List<ReaderChapter> {
|
||||||
|
chapterListCache?.let { return it }
|
||||||
|
|
||||||
val manga = manga!!
|
val manga = manga!!
|
||||||
// SY -->
|
// SY -->
|
||||||
val (chapters, mangaMap) = runBlocking {
|
val (chapters, mangaMap) =
|
||||||
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)
|
||||||
@@ -203,7 +209,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(
|
||||||
@@ -253,7 +259,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
else -> chapters
|
else -> chapters
|
||||||
}
|
}
|
||||||
|
|
||||||
chaptersForReader
|
val result = chaptersForReader
|
||||||
.sortedWith(getChapterSort(manga, sortDescending = false))
|
.sortedWith(getChapterSort(manga, sortDescending = false))
|
||||||
.run {
|
.run {
|
||||||
if (readerPreferences.skipDupe().get()) {
|
if (readerPreferences.skipDupe().get()) {
|
||||||
@@ -271,6 +277,8 @@ 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) }
|
||||||
@@ -409,7 +417,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
|
|
||||||
loadChapter(
|
loadChapter(
|
||||||
loader!!,
|
loader!!,
|
||||||
chapterList.first { chapterId == it.chapter.id },
|
getChapterList().first { chapterId == it.chapter.id },
|
||||||
/* SY --> */page, /* SY <-- */
|
/* SY --> */page, /* SY <-- */
|
||||||
)
|
)
|
||||||
Result.success(true)
|
Result.success(true)
|
||||||
@@ -427,10 +435,10 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
fun getChapters(): List<ReaderChapterItem> {
|
suspend fun getChapters(): List<ReaderChapterItem> {
|
||||||
val currentChapter = getCurrentChapter()
|
val currentChapter = getCurrentChapter()
|
||||||
|
|
||||||
return chapterList.map {
|
return getChapterList().map {
|
||||||
ReaderChapterItem(
|
ReaderChapterItem(
|
||||||
chapter = it.chapter.toDomainChapter()!!,
|
chapter = it.chapter.toDomainChapter()!!,
|
||||||
manga = manga!!,
|
manga = manga!!,
|
||||||
@@ -454,6 +462,7 @@ 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,
|
||||||
@@ -503,7 +512,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
|
|
||||||
fun loadNewChapterFromDialog(chapter: Chapter) {
|
fun loadNewChapterFromDialog(chapter: Chapter) {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val newChapter = chapterList.firstOrNull { it.chapter.id == chapter.id } ?: return@launchIO
|
val newChapter = getChapterList().firstOrNull { it.chapter.id == chapter.id } ?: return@launchIO
|
||||||
loadAdjacent(newChapter)
|
loadAdjacent(newChapter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -665,11 +674,12 @@ 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 fun deleteChapterIfNeeded(currentChapter: ReaderChapter) {
|
private suspend 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)
|
||||||
|
|
||||||
@@ -739,7 +749,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
// SY -->
|
// SY -->
|
||||||
if (manga?.isEhBasedManga() == true) {
|
if (manga?.isEhBasedManga() == true) {
|
||||||
viewModelScope.launchNonCancellable {
|
viewModelScope.launchNonCancellable {
|
||||||
val chapterUpdates = unfilteredChapterList
|
val chapterUpdates = getUnfilteredChapterList()
|
||||||
.filter { it.sourceOrder > readerChapter.chapter.source_order }
|
.filter { it.sourceOrder > readerChapter.chapter.source_order }
|
||||||
.map { chapter ->
|
.map { chapter ->
|
||||||
ChapterUpdate(
|
ChapterUpdate(
|
||||||
@@ -759,7 +769,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 = unfilteredChapterList
|
val duplicateUnreadChapters = getUnfilteredChapterList()
|
||||||
.mapNotNull { chapter ->
|
.mapNotNull { chapter ->
|
||||||
if (
|
if (
|
||||||
!chapter.read &&
|
!chapter.read &&
|
||||||
@@ -774,7 +784,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
updateChapter.awaitAll(duplicateUnreadChapters)
|
updateChapter.awaitAll(duplicateUnreadChapters)
|
||||||
// SY -->
|
// SY -->
|
||||||
duplicateUnreadChapters.forEach { chapterUpdate ->
|
duplicateUnreadChapters.forEach { chapterUpdate ->
|
||||||
val chapter = unfilteredChapterList.first { it.id == chapterUpdate.id }
|
val chapter = getUnfilteredChapterList().first { it.id == chapterUpdate.id }
|
||||||
deleteChapterIfNeeded(ReaderChapter(chapter))
|
deleteChapterIfNeeded(ReaderChapter(chapter))
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
@@ -863,9 +873,9 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
fun toggleBookmark(chapterId: Long, bookmarked: Boolean) {
|
fun toggleBookmark(chapterId: Long, bookmarked: Boolean) {
|
||||||
val chapter = chapterList.find { it.chapter.id == chapterId }?.chapter ?: return
|
|
||||||
chapter.bookmark = bookmarked
|
|
||||||
viewModelScope.launchNonCancellable {
|
viewModelScope.launchNonCancellable {
|
||||||
|
val chapter = getChapterList().find { it.chapter.id == chapterId }?.chapter ?: return@launchNonCancellable
|
||||||
|
chapter.bookmark = bookmarked
|
||||||
updateChapter.await(
|
updateChapter.await(
|
||||||
ChapterUpdate(
|
ChapterUpdate(
|
||||||
id = chapterId,
|
id = chapterId,
|
||||||
@@ -900,7 +910,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
*/
|
*/
|
||||||
fun setMangaReadingMode(readingMode: ReadingMode) {
|
fun setMangaReadingMode(readingMode: ReadingMode) {
|
||||||
val manga = manga ?: return
|
val manga = manga ?: return
|
||||||
runBlocking(Dispatchers.IO) {
|
viewModelScope.launchIO {
|
||||||
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 fun startDownloadingNow(chapterId: Long) {
|
private suspend fun startDownloadingNow(chapterId: Long) {
|
||||||
downloadManager.startDownloadNow(chapterId)
|
downloadManager.startDownloadNow(chapterId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ object Migrator {
|
|||||||
result = null
|
result = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun awaitAndRelease(): Boolean = runBlocking {
|
suspend fun awaitAndRelease(): Boolean {
|
||||||
await().also { release() }
|
return await().also { release() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ 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
|
||||||
@@ -17,6 +19,10 @@ 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.
|
||||||
*/
|
*/
|
||||||
@@ -39,12 +45,32 @@ 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 {
|
||||||
// Use inherited transaction context if available, this allows nested suspending transactions.
|
val transactionElement = coroutineContext[TransactionElement]
|
||||||
val transactionContext =
|
|
||||||
coroutineContext[TransactionElement]?.transactionDispatcher ?: createTransactionContext()
|
// If we are already in a transaction, we don't need to lock the Mutex.
|
||||||
return withContext(transactionContext) {
|
// We just reuse the existing thread/context.
|
||||||
val transactionElement = coroutineContext[TransactionElement]!!
|
if (transactionElement != null) {
|
||||||
|
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) {
|
||||||
@@ -52,7 +78,8 @@ internal suspend fun <T> AndroidDatabaseHandler.withTransaction(block: suspend (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
transactionElement.release()
|
element.release()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,6 +170,7 @@
|
|||||||
|
|
||||||
<!-- 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-->
|
||||||
|
|||||||
Reference in New Issue
Block a user