Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 509ace1a38 | |||
| 170358f88e |
+2
-5
@@ -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,15 +58,13 @@ 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)
|
.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.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()
|
openChapter(
|
||||||
launchIO {
|
context,
|
||||||
try {
|
intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
||||||
openChapter(
|
intent.getLongExtra(EXTRA_CHAPTER_ID, -1),
|
||||||
context,
|
)
|
||||||
intent.getLongExtra(EXTRA_MANGA_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,18 +153,16 @@ 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
|
|
||||||
}
|
|
||||||
context.startActivity(intent)
|
|
||||||
} else {
|
|
||||||
context.toast(MR.strings.chapter_error)
|
|
||||||
}
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
} else {
|
||||||
|
context.toast(MR.strings.chapter_error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,22 +140,20 @@ 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
|
||||||
.filterIsInstance<LoadResult.Success>()
|
.filterIsInstance<LoadResult.Success>()
|
||||||
.associate { it.extension.pkgName to it.extension }
|
.associate { it.extension.pkgName to it.extension }
|
||||||
|
|
||||||
untrustedExtensionMapFlow.value = extensions
|
untrustedExtensionMapFlow.value = extensions
|
||||||
.filterIsInstance<LoadResult.Untrusted>()
|
.filterIsInstance<LoadResult.Untrusted>()
|
||||||
.associate { it.extension.pkgName to it.extension }
|
.associate { it.extension.pkgName to it.extension }
|
||||||
// SY -->
|
// SY -->
|
||||||
.filterNotBlacklisted()
|
.filterNotBlacklisted()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
_isInitialized.value = true
|
_isInitialized.value = true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXH -->
|
// EXH -->
|
||||||
|
|||||||
@@ -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,36 +394,28 @@ 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(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
screenModel = settingsScreenModel,
|
|
||||||
chapters = chapters ?: persistentListOf(),
|
|
||||||
onClickChapter = {
|
|
||||||
viewModel.loadNewChapterFromDialog(it)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
onBookmark = { chapter ->
|
|
||||||
viewModel.toggleBookmark(chapter.id, !chapter.bookmark)
|
|
||||||
chapters = chapters?.map {
|
|
||||||
if (it.chapter.id == chapter.id) {
|
|
||||||
it.copy(chapter = chapter.copy(bookmark = !chapter.bookmark))
|
|
||||||
} else {
|
|
||||||
it
|
|
||||||
}
|
|
||||||
}?.toImmutableList()
|
|
||||||
},
|
|
||||||
state.dateRelativeTime,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
ChapterListDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
screenModel = settingsScreenModel,
|
||||||
|
chapters = chapters,
|
||||||
|
onClickChapter = {
|
||||||
|
viewModel.loadNewChapterFromDialog(it)
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
onBookmark = { chapter ->
|
||||||
|
viewModel.toggleBookmark(chapter.id, !chapter.bookmark)
|
||||||
|
chapters = chapters.map {
|
||||||
|
if (it.chapter.id == chapter.id) {
|
||||||
|
it.copy(chapter = chapter.copy(bookmark = !chapter.bookmark))
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}.toImmutableList()
|
||||||
|
},
|
||||||
|
state.dateRelativeTime,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// SY -->
|
// SY -->
|
||||||
ReaderViewModel.Dialog.AutoScrollHelp -> AlertDialog(
|
ReaderViewModel.Dialog.AutoScrollHelp -> AlertDialog(
|
||||||
@@ -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> {
|
val manga = manga!!
|
||||||
if (unfilteredChapterListCache == null) {
|
runBlocking { getChaptersByMangaId.await(manga.id, applyScanlatorFilter = false) }
|
||||||
val manga = manga!!
|
|
||||||
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 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) {
|
||||||
|
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,
|
||||||
@@ -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,41 +39,20 @@ 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 {
|
||||||
try {
|
db.transactionWithResult {
|
||||||
db.transactionWithResult {
|
runBlocking(transactionContext) {
|
||||||
runBlocking(transactionElement.transactionDispatcher) {
|
block()
|
||||||
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 {
|
|
||||||
db.transactionWithResult {
|
|
||||||
runBlocking(transactionContext) {
|
|
||||||
block()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
element.release()
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
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-->
|
||||||
|
|||||||
Reference in New Issue
Block a user