Sfoglia il codice sorgente

修复书源校验通知消失的问题,修复锁屏书源校验异常退出的问题,修复书源校验时文字显示不全的问题,修复在书源管理页面进行架构迁移时,遗留的其余问题

a328929 3 mesi fa
parent
commit
af30c992da

+ 3 - 0
app/src/main/AndroidManifest.xml

@@ -448,6 +448,9 @@
         <service
             android:name=".service.CacheBookService"
             android:foregroundServiceType="dataSync" />
+        <service
+            android:name=".service.BookSourceCheckService"
+            android:foregroundServiceType="dataSync" />
         <service
             android:name=".service.ExportBookService"
             android:foregroundServiceType="dataSync" />

+ 13 - 0
app/src/main/java/io/legado/app/App.kt

@@ -19,6 +19,7 @@ import com.jeremyliao.liveeventbus.logger.DefaultLogger
 import com.script.rhino.ReadOnlyJavaObject
 import com.script.rhino.RhinoScriptEngine
 import com.script.rhino.RhinoWrapFactory
+import io.legado.app.constant.AppConst.channelIdBookSourceCheck
 import io.legado.app.constant.AppConst.channelIdDownload
 import io.legado.app.constant.AppConst.channelIdReadAloud
 import io.legado.app.constant.AppConst.channelIdWeb
@@ -300,6 +301,17 @@ class App : Application(), ImageLoaderFactory {
             lockscreenVisibility = Notification.VISIBILITY_PUBLIC
         }
 
+        val bookSourceCheckChannel = NotificationChannel(
+            channelIdBookSourceCheck,
+            getString(R.string.check_book_source),
+            NotificationManager.IMPORTANCE_DEFAULT
+        ).apply {
+            enableLights(false)
+            enableVibration(false)
+            setSound(null, null)
+            lockscreenVisibility = Notification.VISIBILITY_PUBLIC
+        }
+
         val webChannel = NotificationChannel(
             channelIdWeb,
             getString(R.string.web_service),
@@ -315,6 +327,7 @@ class App : Application(), ImageLoaderFactory {
         notificationManager.createNotificationChannels(
             listOf(
                 downloadChannel,
+                bookSourceCheckChannel,
                 readAloudChannel,
                 webChannel
             )

+ 1 - 0
app/src/main/java/io/legado/app/constant/AppConst.kt

@@ -17,6 +17,7 @@ object AppConst {
     const val APP_TAG = "Legado"
 
     const val channelIdDownload = "channel_download"
+    const val channelIdBookSourceCheck = "channel_book_source_check"
     const val channelIdReadAloud = "channel_read_aloud"
     const val channelIdWeb = "channel_web"
 

+ 1 - 0
app/src/main/java/io/legado/app/constant/NotificationId.kt

@@ -12,6 +12,7 @@ object NotificationId {
     const val ExportBookService = 104
     const val WebService = 105
     const val DownloadService = 106
+    const val BookSourceCheckService = 107
     const val Download = 10000
     const val ExportBook = 201
 

+ 103 - 14
app/src/main/java/io/legado/app/data/repository/BookSourceCheckRepository.kt

@@ -5,7 +5,9 @@ import io.legado.app.constant.BookSourceType
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookSource
 import io.legado.app.domain.gateway.BookSourceCheckGateway
+import io.legado.app.domain.gateway.BookSourceCheckResult
 import io.legado.app.domain.gateway.BookSourceCheckState
+import io.legado.app.domain.gateway.BookSourceCheckStatus
 import io.legado.app.domain.gateway.CheckSourceSettingsGateway
 import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
 import io.legado.app.exception.ContentEmptyException
@@ -36,26 +38,76 @@ class BookSourceCheckRepository(
     override val state = _state.asStateFlow()
     override suspend fun check(sourceIds: Set<String>, keyword: String) {
         if (sourceIds.isEmpty() || _state.value.isRunning) return
-        _state.value = BookSourceCheckState(isRunning = true, total = sourceIds.size)
+        _state.value = BookSourceCheckState(
+            isRunning = true,
+            total = sourceIds.size,
+            results = sourceIds.associateWith {
+                BookSourceCheckResult(BookSourceCheckStatus.Pending, "等待校验")
+            },
+        )
         val dispatcher = Executors.newFixedThreadPool(
             downloadCacheSettingsGateway.currentSettings.threadCount.coerceAtLeast(1)
         ).asCoroutineDispatcher()
+        var completedNormally = false
         try {
             coroutineScope {
                 sourceIds.map { id ->
-                    async(dispatcher) { checkOne(id, keyword.ifBlank { "我的" }) }
+                    async(dispatcher) {
+                        runCatching { checkOne(id, keyword.ifBlank { "我的" }) }
+                            .onFailure { error ->
+                                coroutineContext.ensureActive()
+                                updateResult(
+                                    id = id,
+                                    name = "",
+                                    status = BookSourceCheckStatus.Failed,
+                                    message = "校验失败:${error.displayMessage()}",
+                                )
+                            }
+                    }
                 }.awaitAll()
             }
+            completedNormally = true
         } finally {
             dispatcher.close()
-            _state.update { it.copy(isRunning = false, currentSourceName = "") }
+            val unfinishedStatus = if (completedNormally) {
+                BookSourceCheckStatus.Failed
+            } else {
+                BookSourceCheckStatus.Cancelled
+            }
+            val unfinishedMessage = if (completedNormally) "校验失败:任务未完成" else "校验已取消"
+            _state.update { state ->
+                val results = state.results.mapValues { (_, result) ->
+                    if (result.status.isTerminal) result
+                    else BookSourceCheckResult(unfinishedStatus, unfinishedMessage)
+                }
+                state.copy(
+                    isRunning = false,
+                    completed = results.values.count { it.status.isTerminal },
+                    currentSourceName = "",
+                    results = results,
+                )
+            }
         }
     }
 
     private suspend fun checkOne(sourceId: String, keyword: String) {
-        val source = bookSourceRepository.getBookSource(sourceId) ?: return
+        val source = bookSourceRepository.getBookSource(sourceId)
+        if (source == null) {
+            updateResult(
+                id = sourceId,
+                name = "",
+                status = BookSourceCheckStatus.Failed,
+                message = "校验失败:书源不存在",
+            )
+            return
+        }
         val startedAt = System.currentTimeMillis()
-        updateResult(sourceId, source.bookSourceName, "开始校验")
+        updateResult(
+            sourceId,
+            source.bookSourceName,
+            BookSourceCheckStatus.Running,
+            "正在校验",
+        )
         val result = runCatching {
             withTimeout(settingsGateway.currentSettings.timeoutMillis) {
                 checkSource(source, keyword)
@@ -63,7 +115,6 @@ class BookSourceCheckRepository(
         }
         result.onSuccess {
             source.respondTime = System.currentTimeMillis() - startedAt
-            updateResult(sourceId, source.bookSourceName, "校验成功", completed = true)
         }.onFailure { error ->
             coroutineContext.ensureActive()
             when (error) {
@@ -74,27 +125,50 @@ class BookSourceCheckRepository(
             source.addErrorComment(error)
             source.respondTime =
                 settingsGateway.currentSettings.timeoutMillis + System.currentTimeMillis() - startedAt
-            updateResult(
+        }
+        val saveResult = runCatching { bookSourceRepository.updateSources(source) }
+        coroutineContext.ensureActive()
+        when {
+            result.isFailure -> updateResult(
                 sourceId,
                 source.bookSourceName,
-                "校验失败:${error.localizedMessage}",
-                completed = true
+                BookSourceCheckStatus.Failed,
+                "校验失败:${result.exceptionOrNull().displayMessage()}",
+            )
+
+            saveResult.isFailure -> updateResult(
+                sourceId,
+                source.bookSourceName,
+                BookSourceCheckStatus.Failed,
+                "校验失败:无法保存结果,${saveResult.exceptionOrNull().displayMessage()}",
+            )
+
+            else -> updateResult(
+                sourceId,
+                source.bookSourceName,
+                BookSourceCheckStatus.Succeeded,
+                "校验成功",
             )
         }
-        bookSourceRepository.updateSources(source)
     }
 
     private fun updateResult(
         id: String,
         name: String,
+        status: BookSourceCheckStatus,
         message: String,
-        completed: Boolean = false
     ) {
         _state.update { state ->
+            val previous = state.results[id]
+            val completed = when {
+                previous?.status?.isTerminal == true -> state.completed
+                status.isTerminal -> state.completed + 1
+                else -> state.completed
+            }
             state.copy(
-                completed = state.completed + if (completed) 1 else 0,
-                currentSourceName = name,
-                results = state.results + (id to message),
+                completed = completed,
+                currentSourceName = name.ifBlank { state.currentSourceName },
+                results = state.results + (id to BookSourceCheckResult(status, message)),
             )
         }
     }
@@ -161,3 +235,18 @@ class BookSourceCheckRepository(
         }
     }
 }
+
+private val BookSourceCheckStatus.isTerminal: Boolean
+    get() = when (this) {
+        BookSourceCheckStatus.Pending,
+        BookSourceCheckStatus.Running -> false
+
+        BookSourceCheckStatus.Succeeded,
+        BookSourceCheckStatus.Failed,
+        BookSourceCheckStatus.Cancelled -> true
+    }
+
+private fun Throwable?.displayMessage(): String =
+    this?.localizedMessage?.takeIf { it.isNotBlank() }
+        ?: this?.javaClass?.simpleName?.takeIf { it.isNotBlank() }
+        ?: "未知错误"

+ 14 - 1
app/src/main/java/io/legado/app/domain/gateway/BookSourceCheckGateway.kt

@@ -2,12 +2,25 @@ package io.legado.app.domain.gateway
 
 import kotlinx.coroutines.flow.StateFlow
 
+enum class BookSourceCheckStatus {
+    Pending,
+    Running,
+    Succeeded,
+    Failed,
+    Cancelled,
+}
+
+data class BookSourceCheckResult(
+    val status: BookSourceCheckStatus,
+    val message: String,
+)
+
 data class BookSourceCheckState(
     val isRunning: Boolean = false,
     val total: Int = 0,
     val completed: Int = 0,
     val currentSourceName: String = "",
-    val results: Map<String, String> = emptyMap(),
+    val results: Map<String, BookSourceCheckResult> = emptyMap(),
 )
 
 interface BookSourceCheckGateway {

+ 205 - 0
app/src/main/java/io/legado/app/service/BookSourceCheckService.kt

@@ -0,0 +1,205 @@
+package io.legado.app.service
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.os.PowerManager
+import androidx.core.app.NotificationCompat
+import androidx.lifecycle.lifecycleScope
+import io.legado.app.R
+import io.legado.app.base.BaseService
+import io.legado.app.constant.AppConst
+import io.legado.app.constant.IntentAction
+import io.legado.app.constant.NotificationId
+import io.legado.app.domain.gateway.BookSourceCheckGateway
+import io.legado.app.domain.gateway.BookSourceCheckStatus
+import io.legado.app.domain.usecase.StartBookSourceCheckUseCase
+import io.legado.app.help.IntentData
+import io.legado.app.ui.main.MainActivity
+import io.legado.app.utils.activityPendingIntent
+import io.legado.app.utils.servicePendingIntent
+import io.legado.app.utils.startForegroundServiceCompat
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import org.koin.android.ext.android.inject
+import splitties.systemservices.notificationManager
+import splitties.systemservices.powerManager
+
+class BookSourceCheckService : BaseService() {
+
+    companion object {
+        private const val EXTRA_REQUEST_KEY = "bookSourceCheckRequestKey"
+
+        fun start(context: Context, ids: Set<String>, keyword: String) {
+            val requestKey = IntentData.put(CheckRequest(ids, keyword))
+            val intent = Intent(context, BookSourceCheckService::class.java).apply {
+                action = IntentAction.start
+                putExtra(EXTRA_REQUEST_KEY, requestKey)
+            }
+            context.startForegroundServiceCompat(intent)
+        }
+
+        fun stop(context: Context) {
+            context.startService(
+                Intent(context, BookSourceCheckService::class.java).apply {
+                    action = IntentAction.stop
+                }
+            )
+        }
+    }
+
+    private data class CheckRequest(
+        val ids: Set<String>,
+        val keyword: String,
+    )
+
+    private val checkGateway by inject<BookSourceCheckGateway>()
+    private val startBookSourceCheck by inject<StartBookSourceCheckUseCase>()
+    private var checkJob: Job? = null
+    private var finishedNotificationShown = false
+    private var wakeLockAcquired = false
+
+    private val wakeLock: PowerManager.WakeLock by lazy {
+        powerManager.newWakeLock(
+            PowerManager.PARTIAL_WAKE_LOCK,
+            "legado:BookSourceCheckService",
+        ).apply { setReferenceCounted(false) }
+    }
+
+    private val notificationBuilder by lazy {
+        NotificationCompat.Builder(this, AppConst.channelIdBookSourceCheck)
+            .setSmallIcon(R.drawable.ic_network_check)
+            .setContentTitle(getString(R.string.check_book_source))
+            .setContentIntent(
+                activityPendingIntent(
+                    MainActivity.createBookSourceManageIntent(this),
+                    "bookSourceCheck",
+                )
+            )
+            .setOnlyAlertOnce(true)
+            .setOngoing(true)
+            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+            .addAction(
+                R.drawable.ic_stop_black_24dp,
+                getString(R.string.cancel),
+                servicePendingIntent<BookSourceCheckService>(IntentAction.stop),
+            )
+    }
+
+    override fun onCreate() {
+        super.onCreate()
+        lifecycleScope.launch {
+            checkGateway.state.collectLatest { state ->
+                if (state.isRunning) {
+                    notificationBuilder
+                        .setContentText(
+                            getString(
+                                R.string.progress_show,
+                                state.currentSourceName,
+                                state.completed,
+                                state.total,
+                            )
+                        )
+                        .setProgress(state.total, state.completed, false)
+                    notificationManager.notify(
+                        NotificationId.BookSourceCheckService,
+                        notificationBuilder.build(),
+                    )
+                }
+            }
+        }
+    }
+
+    @SuppressLint("WakelockTimeout")
+    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        when (intent?.action) {
+            IntentAction.start -> {
+                val request = IntentData.get<CheckRequest>(
+                    intent.getStringExtra(EXTRA_REQUEST_KEY)
+                )
+                if (request == null) {
+                    stopWithoutResultNotification()
+                } else if (checkJob?.isActive != true && !checkGateway.state.value.isRunning) {
+                    finishedNotificationShown = false
+                    wakeLock.acquire()
+                    wakeLockAcquired = true
+                    checkJob = lifecycleScope.launch {
+                        try {
+                            startBookSourceCheck(request.ids, request.keyword)
+                        } finally {
+                            showFinishedNotification()
+                            releaseWakeLock()
+                            stopForeground(STOP_FOREGROUND_DETACH)
+                            stopSelf()
+                        }
+                    }
+                }
+            }
+
+            IntentAction.stop -> {
+                if (checkJob?.isActive == true) checkJob?.cancel()
+                else stopWithoutResultNotification()
+            }
+        }
+        return START_NOT_STICKY
+    }
+
+    override fun onDestroy() {
+        releaseWakeLock()
+        if (!finishedNotificationShown) {
+            notificationManager.cancel(NotificationId.BookSourceCheckService)
+        }
+        super.onDestroy()
+    }
+
+    override fun startForegroundNotification() {
+        notificationBuilder
+            .setContentText(getString(R.string.service_starting))
+            .setProgress(0, 0, true)
+        startForeground(
+            NotificationId.BookSourceCheckService,
+            notificationBuilder.build(),
+        )
+    }
+
+    private fun showFinishedNotification() {
+        val state = checkGateway.state.value
+        val succeeded = state.results.values.count {
+            it.status == BookSourceCheckStatus.Succeeded
+        }
+        val failed = state.results.values.count {
+            it.status == BookSourceCheckStatus.Failed
+        }
+        val cancelled = state.results.values.count {
+            it.status == BookSourceCheckStatus.Cancelled
+        }
+        val content = if (cancelled > 0) {
+            getString(R.string.book_source_check_cancelled, succeeded, failed, cancelled)
+        } else {
+            getString(R.string.book_source_check_completed, succeeded, failed)
+        }
+        notificationBuilder
+            .clearActions()
+            .setContentText(content)
+            .setProgress(0, 0, false)
+            .setOngoing(false)
+            .setAutoCancel(true)
+        notificationManager.notify(
+            NotificationId.BookSourceCheckService,
+            notificationBuilder.build(),
+        )
+        finishedNotificationShown = true
+    }
+
+    private fun stopWithoutResultNotification() {
+        stopForeground(STOP_FOREGROUND_REMOVE)
+        notificationManager.cancel(NotificationId.BookSourceCheckService)
+        stopSelf()
+    }
+
+    private fun releaseWakeLock() {
+        if (wakeLockAcquired && wakeLock.isHeld) wakeLock.release()
+        wakeLockAcquired = false
+    }
+}

+ 7 - 0
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceContract.kt

@@ -109,6 +109,13 @@ sealed interface BookSourceIntent {
 }
 
 sealed interface BookSourceEffect {
+    data class StartCheck(
+        val ids: Set<String>,
+        val keyword: String,
+    ) : BookSourceEffect
+
+    data object CancelCheck : BookSourceEffect
+
     data class ShowSnackbar(
         val message: String,
         val actionLabel: String? = null,

+ 31 - 4
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceScreen.kt

@@ -48,6 +48,7 @@ import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
+import io.legado.app.service.BookSourceCheckService
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.ActionItem
@@ -163,6 +164,12 @@ fun BookSourceScreen(
     LaunchedEffect(Unit) {
         effects.collectLatest { effect ->
             when (effect) {
+                is BookSourceEffect.StartCheck -> {
+                    BookSourceCheckService.start(context, effect.ids, effect.keyword)
+                }
+
+                BookSourceEffect.CancelCheck -> BookSourceCheckService.stop(context)
+
                 is BookSourceEffect.ShowSnackbar -> {
                     val result = snackbarHostState.showSnackbar(
                         message = effect.message,
@@ -607,10 +614,30 @@ fun BookSourceScreen(
                                 }
                             },
                             title = item.name,
-                            subtitle = listOfNotNull(
-                                item.group,
-                                item.checkMessage
-                            ).joinToString(" · ").ifBlank { null },
+                            supportingContent = if (
+                                item.group != null || item.checkMessage != null
+                            ) {
+                                {
+                                    Column {
+                                        item.group?.let { group ->
+                                            AppText(
+                                                text = group,
+                                                style = LegadoTheme.typography.bodySmall,
+                                                maxLines = 1,
+                                                overflow = TextOverflow.Ellipsis,
+                                            )
+                                        }
+                                        item.checkMessage?.let { message ->
+                                            AppText(
+                                                text = message,
+                                                style = LegadoTheme.typography.bodySmall,
+                                            )
+                                        }
+                                    }
+                                }
+                            } else {
+                                null
+                            },
                             isEnabled = item.enabled,
                             isSelected = item.id in selectedIds,
                             canReorder = canReorder,

+ 36 - 10
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt

@@ -12,10 +12,10 @@ import io.legado.app.data.entities.BookSourcePart
 import io.legado.app.data.repository.BookSourceRepository
 import io.legado.app.data.repository.UploadRepository
 import io.legado.app.domain.gateway.BookSourceCheckGateway
+import io.legado.app.domain.gateway.BookSourceCheckStatus
 import io.legado.app.domain.gateway.CheckSourceSettings
 import io.legado.app.domain.gateway.CheckSourceSettingsGateway
 import io.legado.app.domain.gateway.OtherSettingsGateway
-import io.legado.app.domain.usecase.StartBookSourceCheckUseCase
 import io.legado.app.help.book.ContentProcessor
 import io.legado.app.help.http.decompressed
 import io.legado.app.help.http.newCallResponseBody
@@ -36,7 +36,6 @@ import io.legado.app.utils.splitNotBlank
 import kotlinx.collections.immutable.toImmutableList
 import kotlinx.collections.immutable.toImmutableSet
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
@@ -55,7 +54,6 @@ class BookSourceViewModel(
     private val otherSettingsGateway: OtherSettingsGateway,
     private val checkGateway: BookSourceCheckGateway,
     private val checkSettingsGateway: CheckSourceSettingsGateway,
-    private val startBookSourceCheck: StartBookSourceCheckUseCase,
 ) : ViewModel() {
     companion object {
         const val FILTER_ENABLED = "@enabled"
@@ -80,14 +78,36 @@ class BookSourceViewModel(
         MutableStateFlow<BaseImportUiState<BookSource>>(BaseImportUiState.Idle)
     private val _effects = MutableSharedFlow<BookSourceEffect>(extraBufferCapacity = 16)
     val effects = _effects.asSharedFlow()
-    private var checkJob: Job? = null
 
     init {
         viewModelScope.launch {
             var wasRunning = false
             checkGateway.state.collect { state ->
                 if (wasRunning && !state.isRunning) {
-                    _effects.tryEmit(BookSourceEffect.ShowSnackbar("书源校验完成"))
+                    val succeeded = state.results.values.count {
+                        it.status == BookSourceCheckStatus.Succeeded
+                    }
+                    val failed = state.results.values.count {
+                        it.status == BookSourceCheckStatus.Failed
+                    }
+                    val cancelled = state.results.values.count {
+                        it.status == BookSourceCheckStatus.Cancelled
+                    }
+                    val message = if (cancelled > 0) {
+                        application.getString(
+                            io.legado.app.R.string.book_source_check_cancelled,
+                            succeeded,
+                            failed,
+                            cancelled,
+                        )
+                    } else {
+                        application.getString(
+                            io.legado.app.R.string.book_source_check_completed,
+                            succeeded,
+                            failed,
+                        )
+                    }
+                    _effects.tryEmit(BookSourceEffect.ShowSnackbar(message))
                 }
                 wasRunning = state.isRunning
             }
@@ -160,7 +180,7 @@ class BookSourceViewModel(
         checkSettingsGateway.settings,
     ) { state, importing, check, settings ->
         state.copy(
-            items = state.items.map { it.copy(checkMessage = check.results[it.id]) }
+            items = state.items.map { it.copy(checkMessage = check.results[it.id]?.message) }
                 .toImmutableList(),
             importState = importing,
             checkProgress = if (check.isRunning) application.getString(
@@ -229,10 +249,16 @@ class BookSourceViewModel(
             is BookSourceIntent.DeleteGroup -> updateGroup(intent.group, "")
             is BookSourceIntent.CheckSelectedInterval -> checkInterval(intent.ids)
             is BookSourceIntent.StartCheck -> {
-                if (checkJob?.isActive != true) {
-                    checkJob = viewModelScope.launch {
+                if (checkGateway.state.value.isRunning) {
+                    _effects.tryEmit(
+                        BookSourceEffect.ShowSnackbar(
+                            application.getString(io.legado.app.R.string.source_already_checking)
+                        )
+                    )
+                } else {
+                    viewModelScope.launch {
                         checkSettingsGateway.update(intent.options.toSettings())
-                        startBookSourceCheck(intent.ids, intent.keyword)
+                        _effects.emit(BookSourceEffect.StartCheck(intent.ids, intent.keyword))
                     }
                 }
             }
@@ -241,7 +267,7 @@ class BookSourceViewModel(
                 checkSettingsGateway.update(intent.options.toSettings())
             }
 
-            BookSourceIntent.CancelCheck -> checkJob?.cancel()
+            BookSourceIntent.CancelCheck -> _effects.tryEmit(BookSourceEffect.CancelCheck)
             is BookSourceIntent.Import -> importSources(intent.text)
             is BookSourceIntent.Export -> exportSources(intent.uri, intent.ids)
             is BookSourceIntent.Upload -> uploadSources(intent.ids)

+ 2 - 0
app/src/main/res/values-zh-rCN/strings.xml

@@ -361,6 +361,8 @@
     <string name="header_padding">页眉边距</string>
     <string name="footer_padding">页脚边距</string>
     <string name="check_book_source">校验书源</string>
+    <string name="book_source_check_completed">校验完成:成功 %1$d,失败 %2$d</string>
+    <string name="book_source_check_cancelled">校验已停止:成功 %1$d,失败 %2$d,取消 %3$d</string>
     <string name="check_select_source">校验所选</string>
     <string name="progress_show">%1$s      进度 %2$d/%3$d</string>
     <string name="tts_fix">请安装并选择中文 TTS!</string>

+ 2 - 0
app/src/main/res/values-zh-rHK/strings.xml

@@ -303,6 +303,8 @@
     <string name="padding_right">右邊距</string>
     <string name="padding_format">上: %1$d  下: %2$d  左: %3$d  右: %4$d</string>
     <string name="check_book_source">校驗書源</string>
+    <string name="book_source_check_completed">校驗完成:成功 %1$d,失敗 %2$d</string>
+    <string name="book_source_check_cancelled">校驗已停止:成功 %1$d,失敗 %2$d,取消 %3$d</string>
     <string name="check_select_source">校驗所選</string>
     <string name="progress_show">%1$s      進度 %2$d/%3$d</string>
     <string name="tts_fix">請安裝並選擇中文 TTS!</string>

+ 2 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -305,6 +305,8 @@
     <string name="padding_right">右邊距</string>
     <string name="padding_format">上: %1$d  下: %2$d  左: %3$d  右: %4$d</string>
     <string name="check_book_source">校驗書源</string>
+    <string name="book_source_check_completed">校驗完成:成功 %1$d,失敗 %2$d</string>
+    <string name="book_source_check_cancelled">校驗已停止:成功 %1$d,失敗 %2$d,取消 %3$d</string>
     <string name="check_select_source">校驗所選</string>
     <string name="progress_show">%1$s      進度 %2$d/%3$d</string>
     <string name="tts_fix">請安裝並選擇中文TTS!</string>

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -391,6 +391,8 @@
     <string name="header_padding">Header Padding</string>
     <string name="footer_padding">Footer Padding</string>
     <string name="check_book_source">Check book sources</string>
+    <string name="book_source_check_completed">Check completed: %1$d succeeded, %2$d failed</string>
+    <string name="book_source_check_cancelled">Check stopped: %1$d succeeded, %2$d failed, %3$d cancelled</string>
     <string name="check_select_source">Check the selected source</string>
     <string name="progress_show">%1$s      Progress %2$d/%3$d</string>
     <string name="tts_fix">Please install and select Chinese TTS!</string>