HapeLee 5 месяцев назад
Родитель
Сommit
cfd21da161

+ 33 - 22
app/src/main/java/io/legado/app/model/CacheBook.kt

@@ -17,6 +17,7 @@ import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.utils.LogUtils
 import io.legado.app.utils.onEachParallel
 import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.currentCoroutineContext
@@ -34,6 +35,7 @@ import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withTimeout
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.atomic.AtomicInteger
@@ -180,16 +182,14 @@ object CacheBook {
         _downloadSummaryFlow.value = buildSummary(stats)
     }
 
-    @Synchronized
-    fun getOrCreate(bookUrl: String): CacheBookModel? {
-        val book = appDb.bookDao.getBook(bookUrl) ?: return null
-        val source = appDb.bookSourceDao.getBookSource(book.origin) ?: return null
-        return getOrCreate(source, book)
+    suspend fun getOrCreate(bookUrl: String): CacheBookModel? = withContext(Dispatchers.IO) {
+        val book = appDb.bookDao.getBook(bookUrl) ?: return@withContext null
+        val source = appDb.bookSourceDao.getBookSource(book.origin) ?: return@withContext null
+        getOrCreate(source, book)
     }
 
     @Synchronized
     fun getOrCreate(bookSource: BookSource, book: Book): CacheBookModel {
-        updateBookSource(bookSource)
         cacheBookMap[book.bookUrl]?.let { model ->
             model.bookSource = bookSource
             model.book = book
@@ -202,14 +202,16 @@ object CacheBook {
     }
 
     private fun updateBookSource(newBookSource: BookSource) {
-        cacheBookMap.forEach { (_, model) ->
-            if (model.bookSource.bookSourceUrl == newBookSource.bookSourceUrl) {
+        // 只有在必要时才更新,且避免在 getOrCreate 中高频调用
+        val sourceUrl = newBookSource.bookSourceUrl
+        cacheBookMap.values.forEach { model ->
+            if (model.bookSource.bookSourceUrl == sourceUrl && model.bookSource != newBookSource) {
                 model.bookSource = newBookSource
             }
         }
     }
 
-    fun start(context: Context, book: Book, selectedIndices: List<Int>) {
+    suspend fun start(context: Context, book: Book, selectedIndices: List<Int>) {
         start(
             context = context,
             request = CacheDownloadRequest(
@@ -220,7 +222,7 @@ object CacheBook {
         )
     }
 
-    fun start(context: Context, book: Book, startIndex: Int, endIndex: Int) {
+    suspend fun start(context: Context, book: Book, startIndex: Int, endIndex: Int) {
         start(
             context = context,
             request = CacheDownloadRequest(
@@ -241,19 +243,28 @@ object CacheBook {
         }
     }
 
-    fun start(context: Context, requests: List<CacheDownloadRequest>) {
-        requests.asSequence()
-            .filter { it.hasValidSelection() }
-            .filter { request ->
-                appDb.bookDao.getBook(request.bookUrl)?.isLocal != true
-            }
-            .forEach { request ->
-                isPaused = false
-                startCacheBookService(context) {
-                    action = IntentAction.start
-                    putRequestExtras(request)
-                }
+    suspend fun start(context: Context, requests: List<CacheDownloadRequest>) = withContext(Dispatchers.IO) {
+        val validRequests = requests.filter { it.hasValidSelection() }
+        if (validRequests.isEmpty()) return@withContext
+        
+        val urls = validRequests.map { it.bookUrl }.toSet()
+        val localBookUrls = appDb.bookDao.getCacheableBooks(urls)
+            .filter { it.isLocal }
+            .map { it.bookUrl }
+            .toSet()
+
+        val finalRequests = validRequests.filterNot { it.bookUrl in localBookUrls }
+        if (finalRequests.isEmpty()) return@withContext
+
+        isPaused = false
+        // 如果请求较多,可以通过 Intent 传递一个特殊的标志让 Service 自己去检查队列,
+        // 或者分批发送。这里我们先简单处理,但确保不在主线程做数据库查询。
+        finalRequests.forEach { request ->
+            startCacheBookService(context) {
+                action = IntentAction.start
+                putRequestExtras(request)
             }
+        }
     }
 
     private fun android.content.Intent.putRequestExtras(request: CacheDownloadRequest) {

+ 15 - 6
app/src/main/java/io/legado/app/model/cache/CacheDownloadStateStore.kt

@@ -116,12 +116,21 @@ class CacheDownloadStateStore {
     }
 
     private fun CacheDownloadState.recalculate(): CacheDownloadState {
-        val totalWaiting = books.values.sumOf { it.waitingCount }
-        val totalRunning = books.values.sumOf { it.runningIndices.size }
-        val totalPaused = books.values.sumOf { it.pausedIndices.size }
-        val totalFailure = books.values.sumOf { it.failedIndices.size } +
-                books.values.count { it.failureMessage != null }
-        val totalSuccess = books.values.sumOf { it.successCount }
+        var totalWaiting = 0
+        var totalRunning = 0
+        var totalPaused = 0
+        var totalFailure = 0
+        var totalSuccess = 0
+
+        books.values.forEach { bookState ->
+            totalWaiting += bookState.waitingCount
+            totalRunning += bookState.runningIndices.size
+            totalPaused += bookState.pausedIndices.size
+            totalFailure += bookState.failedIndices.size
+            if (bookState.failureMessage != null) totalFailure++
+            totalSuccess += bookState.successCount
+        }
+
         return copy(
             isRunning = totalWaiting > totalPaused || totalRunning > 0,
             totalWaiting = totalWaiting,

+ 11 - 4
app/src/main/java/io/legado/app/service/CacheBookService.kt

@@ -150,7 +150,7 @@ class CacheBookService : BaseService() {
                         serviceCommandMutex.withLock {
                             CacheBook.pauseAllFromService()
                             notificationContent = CacheBook.downloadSummary
-                            upCacheBookNotification()
+                            upCacheBookNotification(force = true)
                         }
                     }
                 }
@@ -160,7 +160,7 @@ class CacheBookService : BaseService() {
                             CacheBook.resumeFromService()
                             ensureDownloadJob()
                             notificationContent = CacheBook.downloadSummary
-                            upCacheBookNotification()
+                            upCacheBookNotification(force = true)
                         }
                     }
                 }
@@ -364,7 +364,8 @@ class CacheBookService : BaseService() {
         }
 
         notificationContent = CacheBook.downloadSummary
-        upCacheBookNotification()
+        // 移除这里的直接调用,依靠 onCreate 的循环更新
+        // upCacheBookNotification()
     }
 
     private fun AdmissionRequest.isCurrent(): Boolean {
@@ -521,7 +522,13 @@ class CacheBookService : BaseService() {
         }
     }
 
-    private fun upCacheBookNotification() {
+    private var lastNotificationTime = 0L
+
+    private fun upCacheBookNotification(force: Boolean = false) {
+        val now = System.currentTimeMillis()
+        if (!force && now - lastNotificationTime < 500L) return
+        lastNotificationTime = now
+
         val total = CacheBook.totalCount
         val progress = CacheBook.completedCount
         val pendingBookCount = synchronized(admissionQueue) { admissionQueue.size }

+ 5 - 1
app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt

@@ -13,6 +13,7 @@ import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.doOnAttach
 import androidx.core.view.isVisible
 import androidx.core.view.updateLayoutParams
+import androidx.lifecycle.lifecycleScope
 import com.google.android.material.datepicker.MaterialDatePicker
 import io.legado.app.R
 import io.legado.app.base.VMBaseActivity
@@ -48,6 +49,7 @@ import io.legado.app.utils.isTv
 import io.legado.app.utils.setLightStatusBar
 import io.legado.app.utils.setNavigationBarColorAuto
 import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat
+import kotlinx.coroutines.launch
 import io.legado.app.utils.showDialogFragment
 import io.legado.app.utils.themeColor
 import io.legado.app.utils.viewbindingdelegate.viewBinding
@@ -304,7 +306,9 @@ abstract class BaseReadBookActivity :
                         val end = editEnd.text!!.toString().let {
                             if (it.isEmpty()) book.totalChapterNum else it.toInt()
                         }
-                        CacheBook.start(this@BaseReadBookActivity, book, start - 1, end - 1)
+                        lifecycleScope.launch {
+                            CacheBook.start(this@BaseReadBookActivity, book, start - 1, end - 1)
+                        }
                     }
                 }
                 cancelButton()

+ 1 - 0
app/src/main/java/io/legado/app/ui/widget/components/rules/RuleEditSheet.kt

@@ -38,6 +38,7 @@ import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.AppFloatingActionButton
 import io.legado.app.ui.widget.components.AppTextField
 import io.legado.app.ui.widget.components.button.MediumIconButton
+import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet