Просмотр исходного кода

feat: 补充书源管理页功能

HapeLee 3 месяцев назад
Родитель
Сommit
4a362c183e
33 измененных файлов с 946 добавлено и 1603 удалено
  1. 0 3
      app/src/main/AndroidManifest.xml
  2. 0 1
      app/src/main/java/io/legado/app/constant/NotificationId.kt
  3. 8 0
      app/src/main/java/io/legado/app/data/local/preferences/LocalPreferences.kt
  4. 163 0
      app/src/main/java/io/legado/app/data/repository/BookSourceCheckRepository.kt
  5. 44 0
      app/src/main/java/io/legado/app/data/repository/CheckSourceSettingsRepository.kt
  6. 0 79
      app/src/main/java/io/legado/app/data/repository/OtherConfigAuxiliaryRepositories.kt
  7. 17 12
      app/src/main/java/io/legado/app/di/appModule.kt
  8. 16 0
      app/src/main/java/io/legado/app/domain/gateway/BookSourceCheckGateway.kt
  9. 19 0
      app/src/main/java/io/legado/app/domain/gateway/CheckSourceSettingsGateway.kt
  10. 0 18
      app/src/main/java/io/legado/app/domain/gateway/OtherConfigAuxiliaryGateways.kt
  11. 8 0
      app/src/main/java/io/legado/app/domain/usecase/BookSourceCheckUseCases.kt
  12. 0 65
      app/src/main/java/io/legado/app/model/CheckSource.kt
  13. 8 45
      app/src/main/java/io/legado/app/model/Debug.kt
  14. 0 260
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  15. 2 6
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditScreen.kt
  16. 0 18
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  17. 0 384
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt
  18. 42 0
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceContract.kt
  19. 274 10
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceScreen.kt
  20. 286 2
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt
  21. 29 19
      app/src/main/java/io/legado/app/ui/book/source/manage/CheckSourceBottomSheet.kt
  22. 0 113
      app/src/main/java/io/legado/app/ui/config/CheckSourceConfig.kt
  23. 1 15
      app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigContract.kt
  24. 0 7
      app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigRouteScreen.kt
  25. 0 5
      app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigScreen.kt
  26. 2 80
      app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt
  27. 25 6
      app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditScreen.kt
  28. 2 0
      app/src/main/java/io/legado/app/ui/widget/components/importComponents/ImportState.kt
  29. 0 35
      app/src/main/res/layout/activity_book_source.xml
  30. 0 115
      app/src/main/res/layout/dialog_check_source_config.xml
  31. 0 131
      app/src/main/res/layout/item_book_source.xml
  32. 0 142
      app/src/main/res/menu/book_source.xml
  33. 0 32
      app/src/main/res/menu/book_source_item.xml

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

@@ -496,9 +496,6 @@
             </intent-filter>
             </intent-filter>
         </activity>
         </activity>
 
 
-        <service
-            android:name=".service.CheckSourceService"
-            android:foregroundServiceType="dataSync" />
         <service
         <service
             android:name=".service.CacheBookService"
             android:name=".service.CacheBookService"
             android:foregroundServiceType="dataSync" />
             android:foregroundServiceType="dataSync" />

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

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

+ 8 - 0
app/src/main/java/io/legado/app/data/local/preferences/LocalPreferences.kt

@@ -46,4 +46,12 @@ object LocalPreferencesKeys {
     val CHANGE_SOURCE_LOAD_TOC = booleanPreferencesKey("changeSourceLoadToc")
     val CHANGE_SOURCE_LOAD_TOC = booleanPreferencesKey("changeSourceLoadToc")
     val CHANGE_SOURCE_LOAD_WORD_COUNT = booleanPreferencesKey("changeSourceLoadWordCount")
     val CHANGE_SOURCE_LOAD_WORD_COUNT = booleanPreferencesKey("changeSourceLoadWordCount")
     val CHANGE_SOURCE_SEARCH_SCOPE = stringPreferencesKey("changeSourceSearchScope")
     val CHANGE_SOURCE_SEARCH_SCOPE = stringPreferencesKey("changeSourceSearchScope")
+
+    // Book source check options
+    val CHECK_SOURCE_TIMEOUT = longPreferencesKey("checkSourceTimeout")
+    val CHECK_SOURCE_SEARCH = booleanPreferencesKey("checkSearch")
+    val CHECK_SOURCE_DISCOVERY = booleanPreferencesKey("checkDiscovery")
+    val CHECK_SOURCE_INFO = booleanPreferencesKey("checkInfo")
+    val CHECK_SOURCE_CATEGORY = booleanPreferencesKey("checkCategory")
+    val CHECK_SOURCE_CONTENT = booleanPreferencesKey("checkContent")
 }
 }

+ 163 - 0
app/src/main/java/io/legado/app/data/repository/BookSourceCheckRepository.kt

@@ -0,0 +1,163 @@
+package io.legado.app.data.repository
+
+import com.script.ScriptException
+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.BookSourceCheckState
+import io.legado.app.domain.gateway.CheckSourceSettingsGateway
+import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
+import io.legado.app.exception.ContentEmptyException
+import io.legado.app.exception.NoStackTraceException
+import io.legado.app.exception.TocEmptyException
+import io.legado.app.help.source.exploreKinds
+import io.legado.app.model.webBook.WebBook
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.withTimeout
+import org.mozilla.javascript.WrappedException
+import java.util.concurrent.Executors
+import kotlin.coroutines.coroutineContext
+
+class BookSourceCheckRepository(
+    private val bookSourceRepository: BookSourceRepository,
+    private val settingsGateway: CheckSourceSettingsGateway,
+    private val downloadCacheSettingsGateway: DownloadCacheSettingsGateway,
+) : BookSourceCheckGateway {
+    private val _state = MutableStateFlow(BookSourceCheckState())
+    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)
+        val dispatcher = Executors.newFixedThreadPool(
+            downloadCacheSettingsGateway.currentSettings.threadCount.coerceAtLeast(1)
+        ).asCoroutineDispatcher()
+        try {
+            coroutineScope {
+                sourceIds.map { id ->
+                    async(dispatcher) { checkOne(id, keyword.ifBlank { "我的" }) }
+                }.awaitAll()
+            }
+        } finally {
+            dispatcher.close()
+            _state.update { it.copy(isRunning = false, currentSourceName = "") }
+        }
+    }
+
+    private suspend fun checkOne(sourceId: String, keyword: String) {
+        val source = bookSourceRepository.getBookSource(sourceId) ?: return
+        val startedAt = System.currentTimeMillis()
+        updateResult(sourceId, source.bookSourceName, "开始校验")
+        val result = runCatching {
+            withTimeout(settingsGateway.currentSettings.timeoutMillis) {
+                checkSource(source, keyword)
+            }
+        }
+        result.onSuccess {
+            source.respondTime = System.currentTimeMillis() - startedAt
+            updateResult(sourceId, source.bookSourceName, "校验成功", completed = true)
+        }.onFailure { error ->
+            coroutineContext.ensureActive()
+            when (error) {
+                is TimeoutCancellationException -> source.addGroup("校验超时")
+                is ScriptException, is WrappedException -> source.addGroup("js失效")
+                !is NoStackTraceException -> source.addGroup("网站失效")
+            }
+            source.addErrorComment(error)
+            source.respondTime =
+                settingsGateway.currentSettings.timeoutMillis + System.currentTimeMillis() - startedAt
+            updateResult(
+                sourceId,
+                source.bookSourceName,
+                "校验失败:${error.localizedMessage}",
+                completed = true
+            )
+        }
+        bookSourceRepository.updateSources(source)
+    }
+
+    private fun updateResult(
+        id: String,
+        name: String,
+        message: String,
+        completed: Boolean = false
+    ) {
+        _state.update { state ->
+            state.copy(
+                completed = state.completed + if (completed) 1 else 0,
+                currentSourceName = name,
+                results = state.results + (id to message),
+            )
+        }
+    }
+
+    private suspend fun checkSource(source: BookSource, keyword: String) {
+        val settings = settingsGateway.currentSettings
+        source.removeInvalidGroups()
+        source.removeErrorComment()
+        if (settings.checkSearch) {
+            val word = source.getCheckKeyword(keyword)
+            if (source.searchUrl.isNullOrBlank()) source.addGroup("搜索链接规则为空")
+            else {
+                source.removeGroup("搜索链接规则为空")
+                val books = WebBook.searchBookAwait(source, word)
+                if (books.isEmpty()) source.addGroup("搜索失效")
+                else {
+                    source.removeGroup("搜索失效"); checkBook(books.first().toBook(), source, true)
+                }
+            }
+        }
+        if (settings.checkDiscovery && !source.exploreUrl.isNullOrBlank()) {
+            val url = source.exploreKinds().firstOrNull { !it.url.isNullOrBlank() }?.url
+            if (url.isNullOrBlank()) source.addGroup("发现规则为空")
+            else {
+                source.removeGroup("发现规则为空")
+                val books = WebBook.exploreBookAwait(source, url)
+                if (books.isEmpty()) source.addGroup("发现失效")
+                else {
+                    source.removeGroup("发现失效"); checkBook(books.first().toBook(), source, false)
+                }
+            }
+        }
+        source.getInvalidGroupNames().takeIf { it.isNotBlank() }
+            ?.let { throw NoStackTraceException(it) }
+    }
+
+    private suspend fun checkBook(book: Book, source: BookSource, searchBook: Boolean) {
+        val settings = settingsGateway.currentSettings
+        runCatching {
+            if (!settings.checkInfo) return
+            if (book.tocUrl.isBlank()) WebBook.getBookInfoAwait(source, book)
+            if (!settings.checkCategory || source.bookSourceType == BookSourceType.file) return
+            val toc = WebBook.getChapterListAwait(source, book).getOrThrow().asSequence()
+                .filter { !(it.isVolume && it.url.startsWith(it.title)) }.take(2).toList()
+            val nextUrl = toc.getOrNull(1)?.url ?: toc.first().url
+            if (settings.checkContent) WebBook.getContentAwait(
+                source,
+                book,
+                toc.first(),
+                nextUrl,
+                false
+            )
+        }.onFailure { error ->
+            val type = if (searchBook) "搜索" else "发现"
+            when (error) {
+                is ContentEmptyException -> source.addGroup("${type}正文失效")
+                is TocEmptyException -> source.addGroup("${type}目录失效")
+                else -> throw error
+            }
+        }.onSuccess {
+            val type = if (searchBook) "搜索" else "发现"
+            source.removeGroup("${type}目录失效")
+            source.removeGroup("${type}正文失效")
+        }
+    }
+}

+ 44 - 0
app/src/main/java/io/legado/app/data/repository/CheckSourceSettingsRepository.kt

@@ -0,0 +1,44 @@
+package io.legado.app.data.repository
+
+import androidx.datastore.preferences.core.Preferences
+import io.legado.app.data.local.preferences.LocalPreferencesKeys
+import io.legado.app.domain.gateway.CheckSourceSettings
+import io.legado.app.domain.gateway.CheckSourceSettingsGateway
+import io.legado.app.help.config.AppConfigStore
+import io.legado.app.help.config.compatDsValue
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+
+class CheckSourceSettingsRepository : CheckSourceSettingsGateway {
+
+    override val currentSettings: CheckSourceSettings
+        get() = AppConfigStore.preferences.toSettings()
+
+    override val settings = AppConfigStore.preferencesFlow
+        .map(Preferences::toSettings)
+        .distinctUntilChanged()
+
+    override suspend fun update(settings: CheckSourceSettings) {
+        require(settings.timeoutMillis > 0L)
+        require(settings.checkSearch || settings.checkDiscovery)
+        AppConfigStore.putAllAndAwait(
+            mapOf(
+                LocalPreferencesKeys.CHECK_SOURCE_TIMEOUT.name to settings.timeoutMillis,
+                LocalPreferencesKeys.CHECK_SOURCE_SEARCH.name to settings.checkSearch,
+                LocalPreferencesKeys.CHECK_SOURCE_DISCOVERY.name to settings.checkDiscovery,
+                LocalPreferencesKeys.CHECK_SOURCE_INFO.name to settings.checkInfo,
+                LocalPreferencesKeys.CHECK_SOURCE_CATEGORY.name to settings.checkCategory,
+                LocalPreferencesKeys.CHECK_SOURCE_CONTENT.name to settings.checkContent,
+            )
+        )
+    }
+}
+
+private fun Preferences.toSettings() = CheckSourceSettings(
+    timeoutMillis = compatDsValue(LocalPreferencesKeys.CHECK_SOURCE_TIMEOUT, 180_000L),
+    checkSearch = compatDsValue(LocalPreferencesKeys.CHECK_SOURCE_SEARCH, true),
+    checkDiscovery = compatDsValue(LocalPreferencesKeys.CHECK_SOURCE_DISCOVERY, true),
+    checkInfo = compatDsValue(LocalPreferencesKeys.CHECK_SOURCE_INFO, true),
+    checkCategory = compatDsValue(LocalPreferencesKeys.CHECK_SOURCE_CATEGORY, true),
+    checkContent = compatDsValue(LocalPreferencesKeys.CHECK_SOURCE_CONTENT, true),
+)

+ 0 - 79
app/src/main/java/io/legado/app/data/repository/OtherConfigAuxiliaryRepositories.kt

@@ -3,97 +3,18 @@ package io.legado.app.data.repository
 import android.content.ComponentName
 import android.content.ComponentName
 import android.content.Context
 import android.content.Context
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager
-import io.legado.app.R
-import io.legado.app.constant.PreferKey
 import io.legado.app.data.local.preferences.LocalPreferencesKeys
 import io.legado.app.data.local.preferences.LocalPreferencesKeys
-import io.legado.app.domain.gateway.CheckSourceSettings
-import io.legado.app.domain.gateway.CheckSourceSettingsGateway
 import io.legado.app.domain.gateway.DirectLinkRule
 import io.legado.app.domain.gateway.DirectLinkRule
 import io.legado.app.domain.gateway.DirectLinkSettingsGateway
 import io.legado.app.domain.gateway.DirectLinkSettingsGateway
 import io.legado.app.domain.gateway.LocalPasswordGateway
 import io.legado.app.domain.gateway.LocalPasswordGateway
 import io.legado.app.domain.gateway.OtherConfigSystemGateway
 import io.legado.app.domain.gateway.OtherConfigSystemGateway
-import io.legado.app.help.CacheManager
 import io.legado.app.help.DirectLinkUpload
 import io.legado.app.help.DirectLinkUpload
 import io.legado.app.help.config.AppConfigStore
 import io.legado.app.help.config.AppConfigStore
 import io.legado.app.help.webView.WebViewDataCleaner
 import io.legado.app.help.webView.WebViewDataCleaner
 import io.legado.app.receiver.SharedReceiverActivity
 import io.legado.app.receiver.SharedReceiverActivity
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.NonCancellable
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withContext
 
 
-class CheckSourceSettingsRepository(
-    private val context: Context,
-) : CheckSourceSettingsGateway {
-
-    private val state = MutableStateFlow(loadSettings())
-
-    override val currentSettings: CheckSourceSettings
-        get() = state.value
-
-    override val settings = state.asStateFlow()
-
-    override suspend fun update(settings: CheckSourceSettings) {
-        require(settings.timeoutMillis > 0L)
-        require(settings.checkSearch || settings.checkDiscovery)
-
-        val previous = state.value
-        try {
-            withContext(Dispatchers.IO) { persistCache(settings) }
-            AppConfigStore.putAllAndAwait(
-                mapOf(PreferKey.checkSource to buildSummary(context, settings))
-            )
-        } catch (error: Exception) {
-            withContext(NonCancellable + Dispatchers.IO) { persistCache(previous) }
-            throw error
-        }
-        state.value = settings
-    }
-
-    private fun persistCache(settings: CheckSourceSettings) {
-        CacheManager.put(CHECK_SOURCE_TIMEOUT, settings.timeoutMillis)
-        CacheManager.put(CHECK_SEARCH, settings.checkSearch)
-        CacheManager.put(CHECK_DISCOVERY, settings.checkDiscovery)
-        CacheManager.put(CHECK_INFO, settings.checkInfo)
-        CacheManager.put(CHECK_CATEGORY, settings.checkCategory)
-        CacheManager.put(CHECK_CONTENT, settings.checkContent)
-    }
-
-    private fun loadSettings() = CheckSourceSettings(
-        timeoutMillis = CacheManager.getLong(CHECK_SOURCE_TIMEOUT) ?: 180_000L,
-        checkSearch = CacheManager.get(CHECK_SEARCH)?.toBoolean() ?: true,
-        checkDiscovery = CacheManager.get(CHECK_DISCOVERY)?.toBoolean() ?: true,
-        checkInfo = CacheManager.get(CHECK_INFO)?.toBoolean() ?: true,
-        checkCategory = CacheManager.get(CHECK_CATEGORY)?.toBoolean() ?: true,
-        checkContent = CacheManager.get(CHECK_CONTENT)?.toBoolean() ?: true,
-    )
-
-    private fun buildSummary(context: Context, settings: CheckSourceSettings): String {
-        val checkedItems = buildList {
-            if (settings.checkSearch) add(context.getString(R.string.search))
-            if (settings.checkDiscovery) add(context.getString(R.string.discovery))
-            if (settings.checkInfo) add(context.getString(R.string.source_tab_info))
-            if (settings.checkCategory) add(context.getString(R.string.chapter_list))
-            if (settings.checkContent) add(context.getString(R.string.main_body))
-        }.joinToString(separator = " ")
-        return context.getString(
-            R.string.check_source_config_summary,
-            (settings.timeoutMillis / 1_000L).toString(),
-            checkedItems,
-        )
-    }
-
-    private companion object {
-        const val CHECK_SOURCE_TIMEOUT = "checkSourceTimeout"
-        const val CHECK_SEARCH = "checkSearch"
-        const val CHECK_DISCOVERY = "checkDiscovery"
-        const val CHECK_INFO = "checkInfo"
-        const val CHECK_CATEGORY = "checkCategory"
-        const val CHECK_CONTENT = "checkContent"
-    }
-}
-
 class DirectLinkSettingsRepository : DirectLinkSettingsGateway {
 class DirectLinkSettingsRepository : DirectLinkSettingsGateway {
 
 
     override suspend fun loadRule(): DirectLinkRule = withContext(Dispatchers.IO) {
     override suspend fun loadRule(): DirectLinkRule = withContext(Dispatchers.IO) {

+ 17 - 12
app/src/main/java/io/legado/app/di/appModule.kt

@@ -19,9 +19,8 @@ import io.legado.app.data.repository.AppStartupRepository
 import io.legado.app.data.repository.AppUiConfigurationRepository
 import io.legado.app.data.repository.AppUiConfigurationRepository
 import io.legado.app.data.repository.BackupRestoreRepository
 import io.legado.app.data.repository.BackupRestoreRepository
 import io.legado.app.data.repository.BackupSettingsRepository
 import io.legado.app.data.repository.BackupSettingsRepository
-import io.legado.app.data.repository.BookCacheManageRepository
 import io.legado.app.data.repository.BookCacheCleanupRepository
 import io.legado.app.data.repository.BookCacheCleanupRepository
-import io.legado.app.data.repository.BookmarkRepository
+import io.legado.app.data.repository.BookCacheManageRepository
 import io.legado.app.data.repository.BookContentProcessRepository
 import io.legado.app.data.repository.BookContentProcessRepository
 import io.legado.app.data.repository.BookDomainRepositoryImpl
 import io.legado.app.data.repository.BookDomainRepositoryImpl
 import io.legado.app.data.repository.BookExportSettingsRepository
 import io.legado.app.data.repository.BookExportSettingsRepository
@@ -31,7 +30,9 @@ import io.legado.app.data.repository.BookImportRepository
 import io.legado.app.data.repository.BookKnowledgeRepository
 import io.legado.app.data.repository.BookKnowledgeRepository
 import io.legado.app.data.repository.BookRepository
 import io.legado.app.data.repository.BookRepository
 import io.legado.app.data.repository.BookSourceCallbackRepository
 import io.legado.app.data.repository.BookSourceCallbackRepository
+import io.legado.app.data.repository.BookSourceCheckRepository
 import io.legado.app.data.repository.BookSourceRepository
 import io.legado.app.data.repository.BookSourceRepository
+import io.legado.app.data.repository.BookmarkRepository
 import io.legado.app.data.repository.BookshelfRepository
 import io.legado.app.data.repository.BookshelfRepository
 import io.legado.app.data.repository.BookshelfSettingsRepository
 import io.legado.app.data.repository.BookshelfSettingsRepository
 import io.legado.app.data.repository.CacheBookDownloadRepository
 import io.legado.app.data.repository.CacheBookDownloadRepository
@@ -113,6 +114,7 @@ import io.legado.app.domain.gateway.BookGroupMutationGateway
 import io.legado.app.domain.gateway.BookKnowledgeGateway
 import io.legado.app.domain.gateway.BookKnowledgeGateway
 import io.legado.app.domain.gateway.BookSearchGateway
 import io.legado.app.domain.gateway.BookSearchGateway
 import io.legado.app.domain.gateway.BookSourceCallbackGateway
 import io.legado.app.domain.gateway.BookSourceCallbackGateway
+import io.legado.app.domain.gateway.BookSourceCheckGateway
 import io.legado.app.domain.gateway.BookshelfSettingsGateway
 import io.legado.app.domain.gateway.BookshelfSettingsGateway
 import io.legado.app.domain.gateway.ChangeSourceSettingsGateway
 import io.legado.app.domain.gateway.ChangeSourceSettingsGateway
 import io.legado.app.domain.gateway.ChapterSpeechGateway
 import io.legado.app.domain.gateway.ChapterSpeechGateway
@@ -184,12 +186,11 @@ import io.legado.app.domain.usecase.SaveBookContentProcessUseCase
 import io.legado.app.domain.usecase.SaveSearchBooksUseCase
 import io.legado.app.domain.usecase.SaveSearchBooksUseCase
 import io.legado.app.domain.usecase.SearchBooksUseCase
 import io.legado.app.domain.usecase.SearchBooksUseCase
 import io.legado.app.domain.usecase.ShrinkDatabaseUseCase
 import io.legado.app.domain.usecase.ShrinkDatabaseUseCase
+import io.legado.app.domain.usecase.StartBookSourceCheckUseCase
 import io.legado.app.domain.usecase.SyncReadAloudVoicesUseCase
 import io.legado.app.domain.usecase.SyncReadAloudVoicesUseCase
 import io.legado.app.domain.usecase.TranslateChapterUseCase
 import io.legado.app.domain.usecase.TranslateChapterUseCase
 import io.legado.app.domain.usecase.UpdateBooksGroupUseCase
 import io.legado.app.domain.usecase.UpdateBooksGroupUseCase
 import io.legado.app.domain.usecase.UploadReadingProgressUseCase
 import io.legado.app.domain.usecase.UploadReadingProgressUseCase
-import io.legado.app.model.ReaderSession
-import io.legado.app.model.LegacyReaderSession
 import io.legado.app.domain.usecase.WebDavBackupUseCase
 import io.legado.app.domain.usecase.WebDavBackupUseCase
 import io.legado.app.domain.usecase.readRecord.GetReadRecordOverviewUseCase
 import io.legado.app.domain.usecase.readRecord.GetReadRecordOverviewUseCase
 import io.legado.app.help.coil.CoverFetcher
 import io.legado.app.help.coil.CoverFetcher
@@ -197,7 +198,9 @@ import io.legado.app.help.coil.CoverInterceptor
 import io.legado.app.help.config.ThemePackageManager
 import io.legado.app.help.config.ThemePackageManager
 import io.legado.app.help.http.okHttpClient
 import io.legado.app.help.http.okHttpClient
 import io.legado.app.help.http.okHttpClientManga
 import io.legado.app.help.http.okHttpClientManga
+import io.legado.app.model.LegacyReaderSession
 import io.legado.app.model.ReadAloudSessionStore
 import io.legado.app.model.ReadAloudSessionStore
+import io.legado.app.model.ReaderSession
 import io.legado.app.ui.about.AboutViewModel
 import io.legado.app.ui.about.AboutViewModel
 import io.legado.app.ui.ai.chat.AiChatViewModel
 import io.legado.app.ui.ai.chat.AiChatViewModel
 import io.legado.app.ui.association.ImportBookSourceViewModel
 import io.legado.app.ui.association.ImportBookSourceViewModel
@@ -206,6 +209,7 @@ import io.legado.app.ui.association.ImportHttpTtsViewModel
 import io.legado.app.ui.association.ImportReplaceRuleViewModel
 import io.legado.app.ui.association.ImportReplaceRuleViewModel
 import io.legado.app.ui.association.ImportRssSourceViewModel
 import io.legado.app.ui.association.ImportRssSourceViewModel
 import io.legado.app.ui.association.ImportTxtTocRuleViewModel
 import io.legado.app.ui.association.ImportTxtTocRuleViewModel
+import io.legado.app.ui.book.audio.AudioPlayViewModel
 import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
 import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
 import io.legado.app.ui.book.cache.manage.BookCacheManageViewModel
 import io.legado.app.ui.book.cache.manage.BookCacheManageViewModel
 import io.legado.app.ui.book.changecover.ChangeCoverViewModel
 import io.legado.app.ui.book.changecover.ChangeCoverViewModel
@@ -218,11 +222,8 @@ import io.legado.app.ui.book.import.local.ImportBookViewModel
 import io.legado.app.ui.book.import.remote.RemoteBookViewModel
 import io.legado.app.ui.book.import.remote.RemoteBookViewModel
 import io.legado.app.ui.book.import.remote.ServerConfigViewModel
 import io.legado.app.ui.book.import.remote.ServerConfigViewModel
 import io.legado.app.ui.book.import.remote.ServersViewModel
 import io.legado.app.ui.book.import.remote.ServersViewModel
-import io.legado.app.ui.book.audio.AudioPlayViewModel
 import io.legado.app.ui.book.info.BookInfoViewModel
 import io.legado.app.ui.book.info.BookInfoViewModel
 import io.legado.app.ui.book.info.edit.BookInfoEditViewModel
 import io.legado.app.ui.book.info.edit.BookInfoEditViewModel
-import io.legado.app.ui.login.SourceLoginViewModel
-import io.legado.app.ui.browser.WebViewModel
 import io.legado.app.ui.book.knowledge.BookCharacterDetailViewModel
 import io.legado.app.ui.book.knowledge.BookCharacterDetailViewModel
 import io.legado.app.ui.book.knowledge.BookCharacterListViewModel
 import io.legado.app.ui.book.knowledge.BookCharacterListViewModel
 import io.legado.app.ui.book.knowledge.BookCharacterNetworkViewModel
 import io.legado.app.ui.book.knowledge.BookCharacterNetworkViewModel
@@ -242,14 +243,13 @@ import io.legado.app.ui.book.readaloud.player.ReadAloudPlayerCoordinator
 import io.legado.app.ui.book.readaloud.player.ReadAloudPlayerViewModel
 import io.legado.app.ui.book.readaloud.player.ReadAloudPlayerViewModel
 import io.legado.app.ui.book.search.SearchViewModel
 import io.legado.app.ui.book.search.SearchViewModel
 import io.legado.app.ui.book.searchContent.SearchContentViewModel
 import io.legado.app.ui.book.searchContent.SearchContentViewModel
-import io.legado.app.ui.book.source.edit.BookSourceEditViewModel
 import io.legado.app.ui.book.source.debug.BookSourceDebugViewModel
 import io.legado.app.ui.book.source.debug.BookSourceDebugViewModel
+import io.legado.app.ui.book.source.edit.BookSourceEditViewModel
 import io.legado.app.ui.book.source.manage.BookSourceViewModel
 import io.legado.app.ui.book.source.manage.BookSourceViewModel
-import io.legado.app.ui.rss.source.edit.RssSourceEditViewModel
-import io.legado.app.ui.rss.source.debug.RssSourceDebugViewModel
 import io.legado.app.ui.book.toc.TocViewModel
 import io.legado.app.ui.book.toc.TocViewModel
 import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel
 import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel
 import io.legado.app.ui.book.toc.rule.preview.TxtTocRulePreviewViewModel
 import io.legado.app.ui.book.toc.rule.preview.TxtTocRulePreviewViewModel
+import io.legado.app.ui.browser.WebViewModel
 import io.legado.app.ui.config.ai.AiConfigViewModel
 import io.legado.app.ui.config.ai.AiConfigViewModel
 import io.legado.app.ui.config.ai.AiModelEditViewModel
 import io.legado.app.ui.config.ai.AiModelEditViewModel
 import io.legado.app.ui.config.ai.AiProviderEditViewModel
 import io.legado.app.ui.config.ai.AiProviderEditViewModel
@@ -271,6 +271,7 @@ import io.legado.app.ui.config.translation.TranslationConfigViewModel
 import io.legado.app.ui.dict.DictViewModel
 import io.legado.app.ui.dict.DictViewModel
 import io.legado.app.ui.dict.rule.DictRuleViewModel
 import io.legado.app.ui.dict.rule.DictRuleViewModel
 import io.legado.app.ui.highlightTagRule.HighlightTagRuleViewModel
 import io.legado.app.ui.highlightTagRule.HighlightTagRuleViewModel
+import io.legado.app.ui.login.SourceLoginViewModel
 import io.legado.app.ui.main.MainRouteSearchContent
 import io.legado.app.ui.main.MainRouteSearchContent
 import io.legado.app.ui.main.MainViewModel
 import io.legado.app.ui.main.MainViewModel
 import io.legado.app.ui.main.bookshelf.BookshelfViewModel
 import io.legado.app.ui.main.bookshelf.BookshelfViewModel
@@ -286,6 +287,8 @@ import io.legado.app.ui.rss.article.RssArticlesViewModel
 import io.legado.app.ui.rss.article.RssSortViewModel
 import io.legado.app.ui.rss.article.RssSortViewModel
 import io.legado.app.ui.rss.favorites.RssFavoritesViewModel
 import io.legado.app.ui.rss.favorites.RssFavoritesViewModel
 import io.legado.app.ui.rss.read.ReadRssViewModel
 import io.legado.app.ui.rss.read.ReadRssViewModel
+import io.legado.app.ui.rss.source.debug.RssSourceDebugViewModel
+import io.legado.app.ui.rss.source.edit.RssSourceEditViewModel
 import io.legado.app.ui.rss.source.manage.RssSourceViewModel
 import io.legado.app.ui.rss.source.manage.RssSourceViewModel
 import io.legado.app.ui.rss.subscription.RuleSubViewModel
 import io.legado.app.ui.rss.subscription.RuleSubViewModel
 import io.legado.app.ui.tagGroupRule.TagGroupRuleViewModel
 import io.legado.app.ui.tagGroupRule.TagGroupRuleViewModel
@@ -342,7 +345,10 @@ val appModule = module {
         )
         )
     }
     }
     single<OtherSettingsGateway> { OtherSettingsRepository() }
     single<OtherSettingsGateway> { OtherSettingsRepository() }
-    single<CheckSourceSettingsGateway> { CheckSourceSettingsRepository(get()) }
+    single<CheckSourceSettingsGateway> { CheckSourceSettingsRepository() }
+    single { BookSourceCheckRepository(get(), get(), get()) }
+    single<BookSourceCheckGateway> { get<BookSourceCheckRepository>() }
+    singleOf(::StartBookSourceCheckUseCase)
     single<DirectLinkSettingsGateway> { DirectLinkSettingsRepository() }
     single<DirectLinkSettingsGateway> { DirectLinkSettingsRepository() }
     single<LocalPasswordGateway> { LocalPasswordRepository() }
     single<LocalPasswordGateway> { LocalPasswordRepository() }
     single<OtherConfigSystemGateway> { OtherConfigSystemRepository(get()) }
     single<OtherConfigSystemGateway> { OtherConfigSystemRepository(get()) }
@@ -528,7 +534,6 @@ val appModule = module {
             readAloudSettingsGateway = get(),
             readAloudSettingsGateway = get(),
             otherSettingsGateway = get(),
             otherSettingsGateway = get(),
             downloadCacheSettingsGateway = get(),
             downloadCacheSettingsGateway = get(),
-            checkSourceSettingsGateway = get(),
             directLinkSettingsGateway = get(),
             directLinkSettingsGateway = get(),
             localPasswordGateway = get(),
             localPasswordGateway = get(),
             systemGateway = get(),
             systemGateway = get(),

+ 16 - 0
app/src/main/java/io/legado/app/domain/gateway/BookSourceCheckGateway.kt

@@ -0,0 +1,16 @@
+package io.legado.app.domain.gateway
+
+import kotlinx.coroutines.flow.StateFlow
+
+data class BookSourceCheckState(
+    val isRunning: Boolean = false,
+    val total: Int = 0,
+    val completed: Int = 0,
+    val currentSourceName: String = "",
+    val results: Map<String, String> = emptyMap(),
+)
+
+interface BookSourceCheckGateway {
+    val state: StateFlow<BookSourceCheckState>
+    suspend fun check(sourceIds: Set<String>, keyword: String)
+}

+ 19 - 0
app/src/main/java/io/legado/app/domain/gateway/CheckSourceSettingsGateway.kt

@@ -0,0 +1,19 @@
+package io.legado.app.domain.gateway
+
+import kotlinx.coroutines.flow.Flow
+
+data class CheckSourceSettings(
+    val timeoutMillis: Long = 180_000L,
+    val checkSearch: Boolean = true,
+    val checkDiscovery: Boolean = true,
+    val checkInfo: Boolean = true,
+    val checkCategory: Boolean = true,
+    val checkContent: Boolean = true,
+)
+
+interface CheckSourceSettingsGateway {
+    val currentSettings: CheckSourceSettings
+    val settings: Flow<CheckSourceSettings>
+
+    suspend fun update(settings: CheckSourceSettings)
+}

+ 0 - 18
app/src/main/java/io/legado/app/domain/gateway/OtherConfigAuxiliaryGateways.kt

@@ -1,23 +1,5 @@
 package io.legado.app.domain.gateway
 package io.legado.app.domain.gateway
 
 
-import kotlinx.coroutines.flow.Flow
-
-data class CheckSourceSettings(
-    val timeoutMillis: Long = 180_000L,
-    val checkSearch: Boolean = true,
-    val checkDiscovery: Boolean = true,
-    val checkInfo: Boolean = true,
-    val checkCategory: Boolean = true,
-    val checkContent: Boolean = true,
-)
-
-interface CheckSourceSettingsGateway {
-    val currentSettings: CheckSourceSettings
-    val settings: Flow<CheckSourceSettings>
-
-    suspend fun update(settings: CheckSourceSettings)
-}
-
 data class DirectLinkRule(
 data class DirectLinkRule(
     val uploadUrl: String,
     val uploadUrl: String,
     val downloadUrlRule: String,
     val downloadUrlRule: String,

+ 8 - 0
app/src/main/java/io/legado/app/domain/usecase/BookSourceCheckUseCases.kt

@@ -0,0 +1,8 @@
+package io.legado.app.domain.usecase
+
+import io.legado.app.domain.gateway.BookSourceCheckGateway
+
+class StartBookSourceCheckUseCase(private val gateway: BookSourceCheckGateway) {
+    suspend operator fun invoke(sourceIds: Set<String>, keyword: String) =
+        gateway.check(sourceIds, keyword)
+}

+ 0 - 65
app/src/main/java/io/legado/app/model/CheckSource.kt

@@ -1,65 +0,0 @@
-package io.legado.app.model
-
-import android.content.Context
-import io.legado.app.R
-import io.legado.app.constant.IntentAction
-import io.legado.app.data.entities.BookSourcePart
-import io.legado.app.domain.gateway.CheckSourceSettingsGateway
-import io.legado.app.help.IntentData
-import io.legado.app.service.CheckSourceService
-import io.legado.app.utils.startService
-import org.koin.core.context.GlobalContext
-import splitties.init.appCtx
-
-object CheckSource {
-    var keyword = "我的"
-
-    private val settingsGateway: CheckSourceSettingsGateway
-        get() = GlobalContext.get().get()
-    private val settings get() = settingsGateway.currentSettings
-
-    // Legacy services keep synchronous reads; writes are owned by CheckSourceSettingsGateway.
-    val timeout get() = settings.timeoutMillis
-    val checkSearch get() = settings.checkSearch
-    val checkDiscovery get() = settings.checkDiscovery
-    val checkInfo get() = settings.checkInfo
-    val checkCategory get() = settings.checkCategory
-    val checkContent get() = settings.checkContent
-    val summary get() = upSummary()
-
-    fun start(context: Context, sources: List<BookSourcePart>) {
-        val selectedIds = sources.map {
-            it.bookSourceUrl
-        }
-        IntentData.put("checkSourceSelectedIds", selectedIds)
-        context.startService<CheckSourceService> {
-            action = IntentAction.start
-        }
-    }
-
-    fun stop(context: Context) {
-        context.startService<CheckSourceService> {
-            action = IntentAction.stop
-        }
-    }
-
-    fun resume(context: Context) {
-        context.startService<CheckSourceService> {
-            action = IntentAction.resume
-        }
-    }
-
-    private fun upSummary(): String {
-        var checkItem = ""
-        if (checkSearch) checkItem = "$checkItem ${appCtx.getString(R.string.search)}"
-        if (checkDiscovery) checkItem = "$checkItem ${appCtx.getString(R.string.discovery)}"
-        if (checkInfo) checkItem = "$checkItem ${appCtx.getString(R.string.source_tab_info)}"
-        if (checkCategory) checkItem = "$checkItem ${appCtx.getString(R.string.chapter_list)}"
-        if (checkContent) checkItem = "$checkItem ${appCtx.getString(R.string.main_body)}"
-        return appCtx.getString(
-            R.string.check_source_config_summary,
-            (timeout / 1000).toString(),
-            checkItem
-        )
-    }
-}

+ 8 - 45
app/src/main/java/io/legado/app/model/Debug.kt

@@ -3,8 +3,11 @@ package io.legado.app.model
 import android.annotation.SuppressLint
 import android.annotation.SuppressLint
 import android.util.Log
 import android.util.Log
 import io.legado.app.BuildConfig
 import io.legado.app.BuildConfig
-import io.legado.app.constant.AppPattern
-import io.legado.app.data.entities.*
+import io.legado.app.data.entities.Book
+import io.legado.app.data.entities.BookChapter
+import io.legado.app.data.entities.BookSource
+import io.legado.app.data.entities.RssArticle
+import io.legado.app.data.entities.RssSource
 import io.legado.app.help.book.isWebFile
 import io.legado.app.help.book.isWebFile
 import io.legado.app.help.coroutine.CompositeCoroutine
 import io.legado.app.help.coroutine.CompositeCoroutine
 import io.legado.app.help.source.sortUrls
 import io.legado.app.help.source.sortUrls
@@ -17,18 +20,16 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.receiveAsFlow
 import kotlinx.coroutines.flow.receiveAsFlow
-import java.util.concurrent.atomic.AtomicLong
 import java.text.SimpleDateFormat
 import java.text.SimpleDateFormat
-import java.util.*
+import java.util.Date
+import java.util.Locale
+import java.util.concurrent.atomic.AtomicLong
 
 
 object Debug {
 object Debug {
     private val nextSessionId = AtomicLong()
     private val nextSessionId = AtomicLong()
     private var activeSession: Session? = null
     private var activeSession: Session? = null
     private var debugSource: String? = null
     private var debugSource: String? = null
     private val tasks: CompositeCoroutine = CompositeCoroutine()
     private val tasks: CompositeCoroutine = CompositeCoroutine()
-    val debugMessageMap = HashMap<String, String>()
-    private val debugTimeMap = HashMap<String, Long>()
-    var isChecking: Boolean = false
 
 
     @SuppressLint("ConstantLocale")
     @SuppressLint("ConstantLocale")
     private val debugTimeFormat = SimpleDateFormat("[mm:ss.SSS]", Locale.getDefault())
     private val debugTimeFormat = SimpleDateFormat("[mm:ss.SSS]", Locale.getDefault())
@@ -70,19 +71,6 @@ object Debug {
             val structuredEvent = event.copy(message = printMsg)
             val structuredEvent = event.copy(message = printMsg)
             activeSession?.emit(structuredEvent)
             activeSession?.emit(structuredEvent)
         }
         }
-        if (isChecking && sourceUrl != null && (msg).length < 30) {
-            var printMsg = msg
-            if (isHtml) {
-                printMsg = HtmlFormatter.format(msg)
-            }
-            if (showTime && debugTimeMap[sourceUrl] != null) {
-                val time =
-                    debugTimeFormat.format(Date(System.currentTimeMillis() - debugTimeMap[sourceUrl]!!))
-                printMsg = printMsg.replace(AppPattern.debugMessageSymbolRegex, "")
-
-                debugMessageMap[sourceUrl] = "$time $printMsg"
-            }
-        }
     }
     }
 
 
     @Synchronized
     @Synchronized
@@ -111,31 +99,6 @@ object Debug {
     val hasActiveSession: Boolean
     val hasActiveSession: Boolean
         @Synchronized get() = activeSession != null
         @Synchronized get() = activeSession != null
 
 
-    fun startChecking(source: BookSource) {
-        isChecking = true
-        debugTimeMap[source.bookSourceUrl] = System.currentTimeMillis()
-        debugMessageMap[source.bookSourceUrl] = "${debugTimeFormat.format(Date(0))} 开始校验"
-    }
-
-    fun finishChecking() {
-        isChecking = false
-    }
-
-    fun getRespondTime(sourceUrl: String): Long {
-        val responseTime = debugTimeMap[sourceUrl]
-        return if (responseTime == null) CheckSource.timeout else responseTime
-    }
-
-    fun updateFinalMessage(sourceUrl: String, state: String) {
-        if (debugTimeMap[sourceUrl] != null && debugMessageMap[sourceUrl] != null) {
-            val spendingTime = System.currentTimeMillis() - debugTimeMap[sourceUrl]!!
-            debugTimeMap[sourceUrl] =
-                if (state == "校验成功") spendingTime else CheckSource.timeout + spendingTime
-            val printTime = debugTimeFormat.format(Date(spendingTime))
-            debugMessageMap[sourceUrl] = "$printTime $state"
-        }
-    }
-
     suspend fun startDebug(scope: CoroutineScope, rssSource: RssSource): Session {
     suspend fun startDebug(scope: CoroutineScope, rssSource: RssSource): Session {
         val session = replaceSession(rssSource.sourceUrl)
         val session = replaceSession(rssSource.sourceUrl)
         log(debugSource, "︾开始解析")
         log(debugSource, "︾开始解析")

+ 0 - 260
app/src/main/java/io/legado/app/service/CheckSourceService.kt

@@ -1,260 +0,0 @@
-package io.legado.app.service
-
-import android.content.Intent
-import androidx.core.app.NotificationCompat
-import androidx.lifecycle.lifecycleScope
-import com.script.ScriptException
-import io.legado.app.R
-import io.legado.app.base.BaseService
-import io.legado.app.constant.AppConst
-import io.legado.app.constant.BookSourceType
-import io.legado.app.constant.EventBus
-import io.legado.app.constant.IntentAction
-import io.legado.app.constant.NotificationId
-import io.legado.app.data.appDb
-import io.legado.app.data.entities.Book
-import io.legado.app.data.entities.BookSource
-import io.legado.app.exception.ContentEmptyException
-import io.legado.app.exception.NoStackTraceException
-import io.legado.app.exception.TocEmptyException
-import io.legado.app.help.IntentData
-import io.legado.app.help.source.exploreKinds
-import io.legado.app.model.CheckSource
-import io.legado.app.model.Debug
-import io.legado.app.model.webBook.WebBook
-import io.legado.app.ui.book.source.manage.BookSourceActivity
-import io.legado.app.ui.config.otherConfig.OtherConfig
-import io.legado.app.utils.activityPendingIntent
-import io.legado.app.utils.onEachParallel
-import io.legado.app.utils.postEvent
-import io.legado.app.utils.servicePendingIntent
-import io.legado.app.utils.toastOnUi
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.TimeoutCancellationException
-import kotlinx.coroutines.asCoroutineDispatcher
-import kotlinx.coroutines.ensureActive
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.onCompletion
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withTimeout
-import org.mozilla.javascript.WrappedException
-import splitties.init.appCtx
-import splitties.systemservices.notificationManager
-import java.util.concurrent.Executors
-import kotlin.coroutines.coroutineContext
-
-/**
- * 校验书源
- */
-class CheckSourceService : BaseService() {
-    private var threadCount = OtherConfig.threadCount
-    private var searchCoroutine =
-        Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher()
-    private var notificationMsg = appCtx.getString(R.string.service_starting)
-    private var checkJob: Job? = null
-    private var originSize = 0
-    private var finishCount = 0
-
-    private val notificationBuilder by lazy {
-        NotificationCompat.Builder(this, AppConst.channelIdReadAloud)
-            .setSmallIcon(R.drawable.ic_network_check)
-            .setOngoing(true)
-            .setOnlyAlertOnce(true)
-            .setContentTitle(getString(R.string.check_book_source))
-            .setContentIntent(
-                activityPendingIntent<BookSourceActivity>("activity")
-            )
-            .addAction(
-                R.drawable.ic_stop_black_24dp,
-                getString(R.string.cancel),
-                servicePendingIntent<CheckSourceService>(IntentAction.stop)
-            )
-            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
-    }
-
-    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
-        when (intent?.action) {
-            IntentAction.start -> IntentData.get<List<String>>("checkSourceSelectedIds")?.let {
-                check(it)
-            }
-
-            IntentAction.resume -> upNotification()
-            IntentAction.stop -> stopSelf()
-        }
-        return super.onStartCommand(intent, flags, startId)
-    }
-
-    override fun onDestroy() {
-        super.onDestroy()
-        Debug.finishChecking()
-        searchCoroutine.close()
-        postEvent(EventBus.CHECK_SOURCE_DONE, 0)
-        notificationManager.cancel(NotificationId.CheckSourceService)
-    }
-
-    private fun check(ids: List<String>) {
-        if (checkJob?.isActive == true) {
-            toastOnUi("已有书源在校验,等完成后再试")
-            return
-        }
-        checkJob = lifecycleScope.launch(searchCoroutine) {
-            flow {
-                for (origin in ids) {
-                    appDb.bookSourceDao.getBookSource(origin)?.let {
-                        emit(it)
-                    }
-                }
-            }.onStart {
-                originSize = ids.size
-                finishCount = 0
-                notificationMsg = getString(R.string.progress_show, "", 0, originSize)
-                upNotification()
-            }.onEachParallel(threadCount) {
-                checkSource(it)
-            }.onEach {
-                finishCount++
-                notificationMsg = getString(
-                    R.string.progress_show,
-                    it.bookSourceName,
-                    finishCount,
-                    originSize
-                )
-                upNotification()
-                appDb.bookSourceDao.update(it)
-            }.onCompletion {
-                stopSelf()
-            }.collect()
-        }
-    }
-
-    private suspend fun checkSource(source: BookSource) {
-        kotlin.runCatching {
-            withTimeout(CheckSource.timeout) {
-                doCheckSource(source)
-            }
-        }.onSuccess {
-            Debug.updateFinalMessage(source.bookSourceUrl, "校验成功")
-        }.onFailure {
-            coroutineContext.ensureActive()
-            when (it) {
-                is TimeoutCancellationException -> source.addGroup("校验超时")
-                is ScriptException, is WrappedException -> source.addGroup("js失效")
-                !is NoStackTraceException -> source.addGroup("网站失效")
-            }
-            source.addErrorComment(it)
-            Debug.updateFinalMessage(source.bookSourceUrl, "校验失败:${it.localizedMessage}")
-        }
-        source.respondTime = Debug.getRespondTime(source.bookSourceUrl)
-    }
-
-    private suspend fun doCheckSource(source: BookSource) {
-        Debug.startChecking(source)
-        source.removeInvalidGroups()
-        source.removeErrorComment()
-        //校验搜索书籍
-        if (CheckSource.checkSearch) {
-            val searchWord = source.getCheckKeyword(CheckSource.keyword)
-            if (!source.searchUrl.isNullOrBlank()) {
-                source.removeGroup("搜索链接规则为空")
-                val searchBooks = WebBook.searchBookAwait(source, searchWord)
-                if (searchBooks.isEmpty()) {
-                    source.addGroup("搜索失效")
-                } else {
-                    source.removeGroup("搜索失效")
-                    checkBook(searchBooks.first().toBook(), source)
-                }
-            } else {
-                source.addGroup("搜索链接规则为空")
-            }
-        }
-        //校验发现书籍
-        if (CheckSource.checkDiscovery && !source.exploreUrl.isNullOrBlank()) {
-            val url = source.exploreKinds().firstOrNull {
-                !it.url.isNullOrBlank()
-            }?.url
-            if (url.isNullOrBlank()) {
-                source.addGroup("发现规则为空")
-            } else {
-                source.removeGroup("发现规则为空")
-                val exploreBooks = WebBook.exploreBookAwait(source, url)
-                if (exploreBooks.isEmpty()) {
-                    source.addGroup("发现失效")
-                } else {
-                    source.removeGroup("发现失效")
-                    checkBook(exploreBooks.first().toBook(), source, false)
-                }
-            }
-        }
-        val finalCheckMessage = source.getInvalidGroupNames()
-        if (finalCheckMessage.isNotBlank()) {
-            throw NoStackTraceException(finalCheckMessage)
-        }
-    }
-
-    /**
-     *校验书源的详情目录正文
-     */
-    private suspend fun checkBook(book: Book, source: BookSource, isSearchBook: Boolean = true) {
-        kotlin.runCatching {
-            if (!CheckSource.checkInfo) {
-                return
-            }
-            //校验详情
-            if (book.tocUrl.isBlank()) {
-                WebBook.getBookInfoAwait(source, book)
-            }
-            if (!CheckSource.checkCategory || source.bookSourceType == BookSourceType.file) {
-                return
-            }
-            //校验目录
-            val toc = WebBook.getChapterListAwait(source, book).getOrThrow().asSequence()
-                .filter { !(it.isVolume && it.url.startsWith(it.title)) }
-                .take(2)
-                .toList()
-            val nextChapterUrl = toc.getOrNull(1)?.url ?: toc.first().url
-            if (!CheckSource.checkContent) {
-                return
-            }
-            //校验正文
-            WebBook.getContentAwait(
-                bookSource = source,
-                book = book,
-                bookChapter = toc.first(),
-                nextChapterUrl = nextChapterUrl,
-                needSave = false
-            )
-        }.onFailure {
-            val bookType = if (isSearchBook) "搜索" else "发现"
-            when (it) {
-                is ContentEmptyException -> source.addGroup("${bookType}正文失效")
-                is TocEmptyException -> source.addGroup("${bookType}目录失效")
-                else -> throw it
-            }
-        }.onSuccess {
-            val bookType = if (isSearchBook) "搜索" else "发现"
-            source.removeGroup("${bookType}目录失效")
-            source.removeGroup("${bookType}正文失效")
-        }
-    }
-
-    private fun upNotification() {
-        notificationBuilder.setContentText(notificationMsg)
-        notificationBuilder.setProgress(originSize, finishCount, false)
-        postEvent(EventBus.CHECK_SOURCE, notificationMsg)
-        notificationManager.notify(NotificationId.CheckSourceService, notificationBuilder.build())
-    }
-
-    /**
-     * 更新通知
-     */
-    override fun startForegroundNotification() {
-        notificationBuilder.setContentText(notificationMsg)
-        notificationBuilder.setProgress(originSize, finishCount, false)
-        postEvent(EventBus.CHECK_SOURCE, notificationMsg)
-        startForeground(NotificationId.CheckSourceService, notificationBuilder.build())
-    }
-
-}

+ 2 - 6
app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditScreen.kt

@@ -25,7 +25,6 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
@@ -55,7 +54,6 @@ import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.TopBarActionButton
 import io.legado.app.ui.widget.components.topbar.TopBarActionButton
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
-import kotlinx.coroutines.launch
 
 
 @OptIn(ExperimentalMaterial3Api::class)
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 @Composable
@@ -69,17 +67,16 @@ fun BookSourceEditScreen(
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
     val tabs = BookSourceEditTab.entries
     val tabs = BookSourceEditTab.entries
     val pagerState = rememberPagerState(initialPage = state.selectedTab.ordinal) { tabs.size }
     val pagerState = rememberPagerState(initialPage = state.selectedTab.ordinal) { tabs.size }
-    val pagerScope = rememberCoroutineScope()
     var editingFieldPath by remember { mutableStateOf<String?>(null) }
     var editingFieldPath by remember { mutableStateOf<String?>(null) }
     val editingField =
     val editingField =
         state.fieldGroups.values.flatten().firstOrNull { it.path == editingFieldPath }
         state.fieldGroups.values.flatten().firstOrNull { it.path == editingFieldPath }
     androidx.compose.runtime.LaunchedEffect(state.selectedTab) {
     androidx.compose.runtime.LaunchedEffect(state.selectedTab) {
-        if (pagerState.currentPage != state.selectedTab.ordinal) {
+        if (pagerState.settledPage != state.selectedTab.ordinal) {
             pagerState.animateScrollToPage(state.selectedTab.ordinal)
             pagerState.animateScrollToPage(state.selectedTab.ordinal)
         }
         }
     }
     }
     androidx.compose.runtime.LaunchedEffect(pagerState) {
     androidx.compose.runtime.LaunchedEffect(pagerState) {
-        snapshotFlow { pagerState.currentPage }.collect { page ->
+        snapshotFlow { pagerState.settledPage }.collect { page ->
             tabs.getOrNull(page)?.let { onIntent(BookSourceEditIntent.SelectTab(it)) }
             tabs.getOrNull(page)?.let { onIntent(BookSourceEditIntent.SelectTab(it)) }
         }
         }
     }
     }
@@ -116,7 +113,6 @@ fun BookSourceEditScreen(
                     selectedTabIndex = state.selectedTab.ordinal,
                     selectedTabIndex = state.selectedTab.ordinal,
                     onTabSelected = { page ->
                     onTabSelected = { page ->
                         onIntent(BookSourceEditIntent.SelectTab(tabs[page]))
                         onIntent(BookSourceEditIntent.SelectTab(tabs[page]))
-                        pagerScope.launch { pagerState.animateScrollToPage(page) }
                     },
                     },
                     modifier = Modifier.fillMaxWidth(),
                     modifier = Modifier.fillMaxWidth(),
                 )
                 )

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

@@ -1,24 +1,16 @@
 package io.legado.app.ui.book.source.manage
 package io.legado.app.ui.book.source.manage
 
 
-import androidx.activity.result.contract.ActivityResultContracts
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import io.legado.app.base.BaseComposeActivity
 import io.legado.app.base.BaseComposeActivity
-import io.legado.app.ui.association.ImportBookSourceDialog
 import io.legado.app.ui.book.search.SearchActivity
 import io.legado.app.ui.book.search.SearchActivity
 import io.legado.app.ui.book.search.SearchScope
 import io.legado.app.ui.book.search.SearchScope
 import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
 import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
 import io.legado.app.ui.book.source.edit.BookSourceEditActivity
 import io.legado.app.ui.book.source.edit.BookSourceEditActivity
 import io.legado.app.ui.login.SourceLoginActivity
 import io.legado.app.ui.login.SourceLoginActivity
-import io.legado.app.utils.showDialogFragment
 import io.legado.app.utils.startActivity
 import io.legado.app.utils.startActivity
 
 
 /** Legacy entry point retained for callers outside the Compose navigation graph. */
 /** Legacy entry point retained for callers outside the Compose navigation graph. */
 class BookSourceActivity : BaseComposeActivity() {
 class BookSourceActivity : BaseComposeActivity() {
-    private val importDocument =
-        registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
-            uri?.let { showDialogFragment(ImportBookSourceDialog(it.toString())) }
-    }
-
     @Composable
     @Composable
     override fun Content() {
     override fun Content() {
         BookSourceRouteScreen(
         BookSourceRouteScreen(
@@ -43,16 +35,6 @@ class BookSourceActivity : BaseComposeActivity() {
                     putExtra("key", sourceUrl)
                     putExtra("key", sourceUrl)
                 }
                 }
             },
             },
-            onImportLocal = {
-                importDocument.launch(
-                    arrayOf(
-                        "application/json",
-                        "text/plain",
-                        "text/*"
-                    )
-                )
-            },
-            onImportOnline = { text -> showDialogFragment(ImportBookSourceDialog(text)) },
         )
         )
     }
     }
 }
 }

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

@@ -1,384 +0,0 @@
-package io.legado.app.ui.book.source.manage
-
-//import io.legado.app.lib.theme.backgroundColor
-import android.content.Context
-import android.os.Bundle
-import android.view.View
-import android.view.ViewGroup
-import android.widget.PopupMenu
-import androidx.core.content.ContextCompat
-import androidx.core.os.bundleOf
-import androidx.core.view.doOnLayout
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.RecyclerView
-import com.google.android.material.card.MaterialCardView
-import io.legado.app.R
-import io.legado.app.base.adapter.ItemViewHolder
-import io.legado.app.base.adapter.RecyclerAdapter
-import io.legado.app.data.entities.BookSourcePart
-import io.legado.app.databinding.ItemBookSourceBinding
-import io.legado.app.model.Debug
-import io.legado.app.ui.login.SourceLoginActivity
-import io.legado.app.ui.widget.recycler.DragSelectTouchHelper
-import io.legado.app.ui.widget.recycler.ItemTouchCallback
-import io.legado.app.utils.buildMainHandler
-import io.legado.app.utils.gone
-import io.legado.app.utils.invisible
-import io.legado.app.utils.startActivity
-import io.legado.app.utils.visible
-import java.util.Collections
-
-
-class BookSourceAdapter(
-    context: Context,
-    private val callBack: CallBack,
-    private val recyclerView: RecyclerView
-) : RecyclerAdapter<BookSourcePart, ItemBookSourceBinding>(context),
-    ItemTouchCallback.Callback {
-
-    private val selected = linkedSetOf<BookSourcePart>()
-    private val finalMessageRegex = Regex("成功|失败")
-    private val handler = buildMainHandler()
-    var showSourceHost = false
-
-    val selection: List<BookSourcePart>
-        get() {
-            return getItems().filter {
-                selected.contains(it)
-            }
-        }
-
-    val diffItemCallback = object : DiffUtil.ItemCallback<BookSourcePart>() {
-
-        override fun areItemsTheSame(oldItem: BookSourcePart, newItem: BookSourcePart): Boolean {
-            return oldItem.bookSourceUrl == newItem.bookSourceUrl
-        }
-
-        override fun areContentsTheSame(oldItem: BookSourcePart, newItem: BookSourcePart): Boolean {
-            return oldItem.bookSourceName == newItem.bookSourceName
-                    && oldItem.bookSourceGroup == newItem.bookSourceGroup
-                    && oldItem.enabled == newItem.enabled
-                    && oldItem.enabledExplore == newItem.enabledExplore
-                    && oldItem.hasExploreUrl == newItem.hasExploreUrl
-        }
-
-        override fun getChangePayload(oldItem: BookSourcePart, newItem: BookSourcePart): Any? {
-            val payload = Bundle()
-            if (oldItem.bookSourceName != newItem.bookSourceName
-                || oldItem.bookSourceGroup != newItem.bookSourceGroup
-            ) {
-                payload.putBoolean("upName", true)
-            }
-            if (oldItem.enabled != newItem.enabled) {
-                payload.putBoolean("enabled", newItem.enabled)
-            }
-            if (oldItem.enabledExplore != newItem.enabledExplore ||
-                oldItem.hasExploreUrl != newItem.hasExploreUrl
-            ) {
-                payload.putBoolean("upExplore", true)
-            }
-            if (payload.isEmpty) {
-                return null
-            }
-            return payload
-        }
-
-    }
-
-    override fun getViewBinding(parent: ViewGroup): ItemBookSourceBinding {
-        return ItemBookSourceBinding.inflate(inflater, parent, false)
-    }
-
-    override fun convert(
-        holder: ItemViewHolder,
-        binding: ItemBookSourceBinding,
-        item: BookSourcePart,
-        payloads: MutableList<Any>
-    ) {
-        binding.run {
-            if (payloads.isEmpty()) {
-                //root.setBackgroundColor(ColorUtils.withAlpha(context.backgroundColor, 0.5f))
-                cbBookSource.text = item.getDisPlayNameGroup()
-                swtEnabled.isChecked = item.enabled
-                cbBookSource.isChecked = selected.contains(item)
-                upCheckSourceMessage(binding, item)
-                upShowExplore(ivExplore, item)
-                upSourceHost(binding, holder.layoutPosition)
-            } else {
-                for (i in payloads.indices) {
-                    val bundle = payloads[i] as Bundle
-                    bundle.keySet().forEach {
-                        when (it) {
-                            "enabled" -> swtEnabled.isChecked = bundle.getBoolean("enabled")
-                            "upName" -> cbBookSource.text = item.getDisPlayNameGroup()
-                            "upExplore" -> upShowExplore(ivExplore, item)
-                            "selected" -> cbBookSource.isChecked = selected.contains(item)
-                            "checkSourceMessage" -> upCheckSourceMessage(binding, item)
-                            "upSourceHost" -> upSourceHost(binding, holder.layoutPosition)
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    override fun registerListener(holder: ItemViewHolder, binding: ItemBookSourceBinding) {
-        binding.apply {
-            swtEnabled.setOnCheckedChangeListener { view, checked ->
-                getItem(holder.layoutPosition)?.let {
-                    if (view.isPressed) {
-                        it.enabled = checked
-                        callBack.enable(checked, it)
-                    }
-                }
-            }
-            cbBookSource.setOnCheckedChangeListener { view, checked ->
-                getItem(holder.layoutPosition)?.let {
-                    if (view.isPressed) {
-                        if (checked) {
-                            selected.add(it)
-                        } else {
-                            selected.remove(it)
-                        }
-                        callBack.upCountView()
-                    }
-                }
-            }
-            ivEdit.setOnClickListener {
-                getItem(holder.layoutPosition)?.let {
-                    callBack.edit(it)
-                }
-            }
-            ivMenuMore.setOnClickListener {
-                showMenu(ivMenuMore, holder.layoutPosition)
-            }
-        }
-    }
-
-    override fun onCurrentListChanged() {
-        callBack.upCountView()
-        recyclerView.doOnLayout {
-            handler.post {
-                notifyItemRangeChanged(0, itemCount, bundleOf("upSourceHost" to null))
-            }
-        }
-    }
-
-    private fun showMenu(view: View, position: Int) {
-        val source = getItem(position) ?: return
-        val popupMenu = PopupMenu(context, view)
-        popupMenu.inflate(R.menu.book_source_item)
-        popupMenu.menu.findItem(R.id.menu_top).isVisible = callBack.sort == BookSourceSort.Default
-        popupMenu.menu.findItem(R.id.menu_bottom).isVisible =
-            callBack.sort == BookSourceSort.Default
-        val qyMenu = popupMenu.menu.findItem(R.id.menu_enable_explore)
-        if (!source.hasExploreUrl) {
-            qyMenu.isVisible = false
-        } else {
-            if (source.enabledExplore) {
-                qyMenu.setTitle(R.string.disable_explore)
-            } else {
-                qyMenu.setTitle(R.string.enable_explore)
-            }
-        }
-        val loginMenu = popupMenu.menu.findItem(R.id.menu_login)
-        loginMenu.isVisible = source.hasLoginUrl
-        popupMenu.setOnMenuItemClickListener { menuItem ->
-            when (menuItem.itemId) {
-                R.id.menu_top -> callBack.toTop(source)
-                R.id.menu_bottom -> callBack.toBottom(source)
-                R.id.menu_login -> context.startActivity<SourceLoginActivity> {
-                    putExtra("type", "bookSource")
-                    putExtra("key", source.bookSourceUrl)
-                }
-
-                R.id.menu_search -> callBack.searchBook(source)
-                R.id.menu_debug_source -> callBack.debug(source)
-                R.id.menu_del -> {
-                    callBack.del(source)
-                    selected.remove(source)
-                }
-
-                R.id.menu_enable_explore -> {
-                    callBack.enableExplore(!source.enabledExplore, source)
-                }
-            }
-            true
-        }
-        popupMenu.show()
-    }
-
-    private fun upShowExplore(iv: MaterialCardView, source: BookSourcePart) {
-        when {
-            !source.hasExploreUrl -> {
-                iv.invisible()
-            }
-
-            source.enabledExplore -> {
-                iv.setCardBackgroundColor(ContextCompat.getColor(context, R.color.md_green_500))
-                iv.visible()
-                iv.contentDescription = context.getString(R.string.tag_explore_enabled)
-            }
-
-            else -> {
-                iv.setCardBackgroundColor(ContextCompat.getColor(context, R.color.md_red_400))
-                iv.visible()
-                iv.contentDescription = context.getString(R.string.tag_explore_disabled)
-            }
-        }
-    }
-
-    private fun upCheckSourceMessage(
-        binding: ItemBookSourceBinding,
-        item: BookSourcePart
-    ) = binding.run {
-        val msg = Debug.debugMessageMap[item.bookSourceUrl] ?: ""
-        ivDebugText.text = msg
-        val isEmpty = msg.isEmpty()
-        var isFinalMessage = msg.contains(finalMessageRegex)
-        if (!Debug.isChecking && !isFinalMessage) {
-            Debug.updateFinalMessage(item.bookSourceUrl, "校验失败")
-            ivDebugText.text = Debug.debugMessageMap[item.bookSourceUrl] ?: ""
-            isFinalMessage = true
-        }
-        ivDebugText.visibility =
-            if (!isEmpty) View.VISIBLE else View.GONE
-        ivProgressBar.visibility =
-            if (isFinalMessage || isEmpty || !Debug.isChecking) View.GONE else View.VISIBLE
-    }
-
-    private fun upSourceHost(binding: ItemBookSourceBinding, position: Int) = binding.run {
-        if (showSourceHost && isItemHeader(position)) {
-            tvHostText.text = getHeaderText(position)
-            tvHostText.visible()
-        } else {
-            tvHostText.gone()
-        }
-    }
-
-    fun selectAll() {
-        getItems().forEach {
-            selected.add(it)
-        }
-        notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null)))
-        callBack.upCountView()
-    }
-
-    fun revertSelection() {
-        getItems().forEach {
-            if (selected.contains(it)) {
-                selected.remove(it)
-            } else {
-                selected.add(it)
-            }
-        }
-        notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null)))
-        callBack.upCountView()
-    }
-
-    fun checkSelectedInterval() {
-        val selectedPosition = linkedSetOf<Int>()
-        getItems().forEachIndexed { index, it ->
-            if (selected.contains(it)) {
-                selectedPosition.add(index)
-            }
-        }
-        val minPosition = Collections.min(selectedPosition)
-        val maxPosition = Collections.max(selectedPosition)
-        val itemCount = maxPosition - minPosition + 1
-        for (i in minPosition..maxPosition) {
-            getItem(i)?.let {
-                selected.add(it)
-            }
-        }
-        notifyItemRangeChanged(minPosition, itemCount, bundleOf(Pair("selected", null)))
-        callBack.upCountView()
-    }
-
-    fun getHeaderText(position: Int): String {
-        val source = getItem(position)!!
-        return callBack.getSourceHost(source.bookSourceUrl)
-    }
-
-    fun isItemHeader(position: Int): Boolean {
-        if (position == 0) return true
-        val lastHost = getHeaderText(position - 1)
-        val curHost = getHeaderText(position)
-        return lastHost != curHost
-    }
-
-    override fun swap(srcPosition: Int, targetPosition: Int): Boolean {
-        val srcItem = getItem(srcPosition)
-        val targetItem = getItem(targetPosition)
-        if (srcItem != null && targetItem != null) {
-            val srcOrder = srcItem.customOrder
-            srcItem.customOrder = targetItem.customOrder
-            targetItem.customOrder = srcOrder
-            movedItems.add(srcItem)
-            movedItems.add(targetItem)
-        }
-        swapItem(srcPosition, targetPosition)
-        return true
-    }
-
-    private val movedItems = hashSetOf<BookSourcePart>()
-
-    override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
-        if (movedItems.isNotEmpty()) {
-            val sortNumberSet = hashSetOf<Int>()
-            movedItems.forEach {
-                sortNumberSet.add(it.customOrder)
-            }
-            if (movedItems.size > sortNumberSet.size) {
-                callBack.upOrder(getItems().mapIndexed { index, bookSourcePart ->
-                    bookSourcePart.customOrder = if (callBack.sortAscending) index else -index
-                    bookSourcePart
-                })
-            } else {
-                callBack.upOrder(movedItems.toList())
-            }
-            movedItems.clear()
-        }
-    }
-
-    val dragSelectCallback: DragSelectTouchHelper.Callback =
-        object : DragSelectTouchHelper.AdvanceCallback<BookSourcePart>(Mode.ToggleAndReverse) {
-            override fun currentSelectedId(): MutableSet<BookSourcePart> {
-                return selected
-            }
-
-            override fun getItemId(position: Int): BookSourcePart {
-                return getItem(position)!!
-            }
-
-            override fun updateSelectState(position: Int, isSelected: Boolean): Boolean {
-                getItem(position)?.let {
-                    if (isSelected) {
-                        selected.add(it)
-                    } else {
-                        selected.remove(it)
-                    }
-                    notifyItemChanged(position, bundleOf(Pair("selected", null)))
-                    callBack.upCountView()
-                    return true
-                }
-                return false
-            }
-        }
-
-    interface CallBack {
-        val sort: BookSourceSort
-        val sortAscending: Boolean
-        fun del(bookSource: BookSourcePart)
-        fun edit(bookSource: BookSourcePart)
-        fun toTop(bookSource: BookSourcePart)
-        fun toBottom(bookSource: BookSourcePart)
-        fun searchBook(bookSource: BookSourcePart)
-        fun debug(bookSource: BookSourcePart)
-        fun upOrder(items: List<BookSourcePart>)
-        fun enable(enable: Boolean, bookSource: BookSourcePart)
-        fun enableExplore(enable: Boolean, bookSource: BookSourcePart)
-        fun upCountView()
-        fun getSourceHost(origin: String): String
-    }
-}

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

@@ -1,7 +1,11 @@
 package io.legado.app.ui.book.source.manage
 package io.legado.app.ui.book.source.manage
 
 
+import android.net.Uri
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.Stable
+import io.legado.app.data.entities.BookSource
+import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
+import io.legado.app.ui.widget.components.importComponents.ImportStatus
 import io.legado.app.ui.widget.components.list.InteractionState
 import io.legado.app.ui.widget.components.list.InteractionState
 import io.legado.app.ui.widget.components.list.ListUiState
 import io.legado.app.ui.widget.components.list.ListUiState
 import io.legado.app.ui.widget.components.list.SelectableItem
 import io.legado.app.ui.widget.components.list.SelectableItem
@@ -20,10 +24,21 @@ data class BookSourceItemUi(
     val enabledExplore: Boolean,
     val enabledExplore: Boolean,
     val hasLoginUrl: Boolean,
     val hasLoginUrl: Boolean,
     val hasExploreUrl: Boolean,
     val hasExploreUrl: Boolean,
+    val checkMessage: String? = null,
     val customOrder: Int,
     val customOrder: Int,
 ) : SelectableItem<String> {
 ) : SelectableItem<String> {
 }
 }
 
 
+@Immutable
+data class BookSourceCheckOptionsUi(
+    val timeoutSeconds: Long = 180,
+    val checkSearch: Boolean = true,
+    val checkDiscovery: Boolean = true,
+    val checkInfo: Boolean = true,
+    val checkCategory: Boolean = true,
+    val checkContent: Boolean = true,
+)
+
 @Stable
 @Stable
 data class BookSourceUiState(
 data class BookSourceUiState(
     override val items: ImmutableList<BookSourceItemUi> = persistentListOf(),
     override val items: ImmutableList<BookSourceItemUi> = persistentListOf(),
@@ -35,6 +50,9 @@ data class BookSourceUiState(
     val sort: BookSourceSort = BookSourceSort.Default,
     val sort: BookSourceSort = BookSourceSort.Default,
     val sortAscending: Boolean = true,
     val sortAscending: Boolean = true,
     val groupByDomain: Boolean = false,
     val groupByDomain: Boolean = false,
+    val importState: BaseImportUiState<BookSource> = BaseImportUiState.Idle,
+    val checkProgress: String? = null,
+    val checkOptions: BookSourceCheckOptionsUi = BookSourceCheckOptionsUi(),
     val interaction: InteractionState = InteractionState(isLoading = true),
     val interaction: InteractionState = InteractionState(isLoading = true),
 ) : ListUiState<BookSourceItemUi> {
 ) : ListUiState<BookSourceItemUi> {
     override val isSearch get() = interaction.isSearchMode
     override val isSearch get() = interaction.isSearchMode
@@ -67,4 +85,28 @@ sealed interface BookSourceIntent {
     data class UpdateGroup(val old: String, val new: String) : BookSourceIntent
     data class UpdateGroup(val old: String, val new: String) : BookSourceIntent
     data class DeleteGroup(val group: String) : BookSourceIntent
     data class DeleteGroup(val group: String) : BookSourceIntent
     data class CheckSelectedInterval(val ids: Set<String>) : BookSourceIntent
     data class CheckSelectedInterval(val ids: Set<String>) : BookSourceIntent
+    data class StartCheck(
+        val ids: Set<String>,
+        val keyword: String,
+        val options: BookSourceCheckOptionsUi,
+    ) : BookSourceIntent
+
+    data class UpdateCheckOptions(val options: BookSourceCheckOptionsUi) : BookSourceIntent
+    data object CancelCheck : BookSourceIntent
+    data class Import(val text: String) : BookSourceIntent
+    data class Export(val uri: Uri, val ids: Set<String>) : BookSourceIntent
+    data class ToggleImportItem(val index: Int) : BookSourceIntent
+    data class ToggleImportAll(val selected: Boolean) : BookSourceIntent
+    data class UpdateImportItem(val index: Int, val source: BookSource) : BookSourceIntent
+    data class SelectImportStatus(val status: ImportStatus) : BookSourceIntent
+    data class SetImportKeepName(val enabled: Boolean) : BookSourceIntent
+    data class SetImportKeepGroup(val enabled: Boolean) : BookSourceIntent
+    data class SetImportKeepEnable(val enabled: Boolean) : BookSourceIntent
+    data class SetImportCustomGroup(val group: String?, val add: Boolean) : BookSourceIntent
+    data object CancelImport : BookSourceIntent
+    data object SaveImportedSources : BookSourceIntent
+}
+
+sealed interface BookSourceEffect {
+    data class ShowSnackbar(val message: String) : BookSourceEffect
 }
 }

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

@@ -1,5 +1,7 @@
 package io.legado.app.ui.book.source.manage
 package io.legado.app.ui.book.source.manage
 
 
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
 import androidx.compose.foundation.background
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Arrangement
@@ -29,6 +31,7 @@ import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 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.res.stringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.style.TextOverflow
@@ -38,6 +41,7 @@ import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.ActionItem
 import io.legado.app.ui.widget.components.ActionItem
+import io.legado.app.ui.widget.components.AppTextField
 import io.legado.app.ui.widget.components.DraggableSelectionHandler
 import io.legado.app.ui.widget.components.DraggableSelectionHandler
 import io.legado.app.ui.widget.components.GroupManageBottomSheet
 import io.legado.app.ui.widget.components.GroupManageBottomSheet
 import io.legado.app.ui.widget.components.SearchBar
 import io.legado.app.ui.widget.components.SearchBar
@@ -47,14 +51,20 @@ import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
 import io.legado.app.ui.widget.components.dialog.TextListInputDialog
 import io.legado.app.ui.widget.components.dialog.TextListInputDialog
 import io.legado.app.ui.widget.components.divider.PillDivider
 import io.legado.app.ui.widget.components.divider.PillDivider
 import io.legado.app.ui.widget.components.icon.AppIcons
 import io.legado.app.ui.widget.components.icon.AppIcons
+import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
+import io.legado.app.ui.widget.components.importComponents.BatchImportDialog
+import io.legado.app.ui.widget.components.importComponents.ImportStatus
 import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
 import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
 import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
 import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 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.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.rules.RuleListScaffold
 import io.legado.app.ui.widget.components.rules.RuleListScaffold
+import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.topbar.TopBarActionButton
 import io.legado.app.ui.widget.components.topbar.TopBarActionButton
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collectLatest
 import org.koin.androidx.compose.koinViewModel
 import org.koin.androidx.compose.koinViewModel
 import sh.calvin.reorderable.rememberReorderableLazyListState
 import sh.calvin.reorderable.rememberReorderableLazyListState
 import androidx.compose.foundation.lazy.grid.items as gridItems
 import androidx.compose.foundation.lazy.grid.items as gridItems
@@ -68,8 +78,6 @@ fun BookSourceRouteScreen(
     onLoginSource: (String) -> Unit,
     onLoginSource: (String) -> Unit,
     onSearchSource: (String) -> Unit,
     onSearchSource: (String) -> Unit,
     onDebugSource: (String) -> Unit,
     onDebugSource: (String) -> Unit,
-    onImportLocal: () -> Unit,
-    onImportOnline: (String) -> Unit,
 ) {
 ) {
     val state by viewModel.uiState.collectAsStateWithLifecycle()
     val state by viewModel.uiState.collectAsStateWithLifecycle()
     BookSourceScreen(
     BookSourceScreen(
@@ -81,8 +89,7 @@ fun BookSourceRouteScreen(
         onLoginSource,
         onLoginSource,
         onSearchSource,
         onSearchSource,
         onDebugSource,
         onDebugSource,
-        onImportLocal,
-        onImportOnline
+        viewModel.effects,
     )
     )
 }
 }
 
 
@@ -97,9 +104,9 @@ fun BookSourceScreen(
     onLoginSource: (String) -> Unit,
     onLoginSource: (String) -> Unit,
     onSearchSource: (String) -> Unit,
     onSearchSource: (String) -> Unit,
     onDebugSource: (String) -> Unit,
     onDebugSource: (String) -> Unit,
-    onImportLocal: () -> Unit,
-    onImportOnline: (String) -> Unit,
+    effects: Flow<BookSourceEffect>,
 ) {
 ) {
+    val context = LocalContext.current
     val rules = state.items
     val rules = state.items
     val selectedIds = state.selectedIds
     val selectedIds = state.selectedIds
     val listState = rememberLazyListState()
     val listState = rememberLazyListState()
@@ -109,6 +116,13 @@ fun BookSourceScreen(
     var groupManage by remember { mutableStateOf(false) }
     var groupManage by remember { mutableStateOf(false) }
     var showGroupFilterSheet by remember { mutableStateOf(false) }
     var showGroupFilterSheet by remember { mutableStateOf(false) }
     var showOnlineImport by remember { mutableStateOf(false) }
     var showOnlineImport by remember { mutableStateOf(false) }
+    var checkSourceIds by remember { mutableStateOf<Set<String>?>(null) }
+    var checkSheet by remember { mutableStateOf<CheckSheet?>(null) }
+    var checkOptionsDraft by remember { mutableStateOf(state.checkOptions) }
+    var pendingExportIds by remember { mutableStateOf<Set<String>>(emptySet()) }
+    var showImportOptions by remember { mutableStateOf(false) }
+    var showCustomGroup by remember { mutableStateOf(false) }
+    val importSuccess = state.importState as? BaseImportUiState.Success
     var dragOrder by remember { mutableStateOf<List<BookSourceItemUi>?>(null) }
     var dragOrder by remember { mutableStateOf<List<BookSourceItemUi>?>(null) }
     val displayedRules = dragOrder ?: rules
     val displayedRules = dragOrder ?: rules
     val reorderState = rememberReorderableLazyListState(listState) { from, to ->
     val reorderState = rememberReorderableLazyListState(listState) { from, to ->
@@ -130,11 +144,161 @@ fun BookSourceScreen(
             }
             }
         }
         }
     }
     }
+    val snackbarHostState = remember { SnackbarHostState() }
+    LaunchedEffect(Unit) {
+        effects.collectLatest { effect ->
+            when (effect) {
+                is BookSourceEffect.ShowSnackbar -> snackbarHostState.showSnackbar(effect.message)
+            }
+        }
+    }
+    LaunchedEffect(state.checkProgress) {
+        val progress = state.checkProgress ?: return@LaunchedEffect
+        val result = snackbarHostState.showSnackbar(
+            message = progress,
+            actionLabel = context.getString(R.string.cancel),
+            withDismissAction = false,
+            duration = androidx.compose.material3.SnackbarDuration.Indefinite,
+        )
+        if (result == androidx.compose.material3.SnackbarResult.ActionPerformed) {
+            onIntent(BookSourceIntent.CancelCheck)
+        }
+    }
+    val importDocument =
+        rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
+            uri?.let {
+                context.contentResolver.openInputStream(it)?.bufferedReader()?.use { reader ->
+                    onIntent(BookSourceIntent.Import(reader.readText()))
+                }
+            }
+        }
+    val exportDocument =
+        rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("application/json")) { uri ->
+            uri?.let { onIntent(BookSourceIntent.Export(it, pendingExportIds)) }
+        }
     SourceInputDialog(
     SourceInputDialog(
         show = showOnlineImport,
         show = showOnlineImport,
         title = stringResource(R.string.import_on_line),
         title = stringResource(R.string.import_on_line),
         onDismissRequest = { showOnlineImport = false },
         onDismissRequest = { showOnlineImport = false },
-        onConfirm = { text -> showOnlineImport = false; onImportOnline(text) })
+        onConfirm = { text -> showOnlineImport = false; onIntent(BookSourceIntent.Import(text)) })
+
+    BatchImportDialog(
+        title = stringResource(R.string.import_book_source),
+        importState = state.importState,
+        onDismissRequest = { onIntent(BookSourceIntent.CancelImport) },
+        onConfirm = { onIntent(BookSourceIntent.SaveImportedSources) },
+        onToggleItem = { onIntent(BookSourceIntent.ToggleImportItem(it)) },
+        onToggleAll = { onIntent(BookSourceIntent.ToggleImportAll(it)) },
+        onUpdateItem = { index, source ->
+            onIntent(
+                BookSourceIntent.UpdateImportItem(
+                    index,
+                    source
+                )
+            )
+        },
+        topBarActions = {
+            Box {
+                SmallPlainButton(
+                    icon = AppIcons.MoreVert,
+                    contentDescription = stringResource(R.string.menu),
+                    onClick = { showImportOptions = true },
+                )
+                RoundDropdownMenu(showImportOptions, { showImportOptions = false }) { dismiss ->
+                    RoundDropdownMenuItem(stringResource(R.string.select_new_source), onClick = {
+                        dismiss(); onIntent(BookSourceIntent.SelectImportStatus(ImportStatus.New))
+                    })
+                    RoundDropdownMenuItem(stringResource(R.string.select_update_source), onClick = {
+                        dismiss(); onIntent(BookSourceIntent.SelectImportStatus(ImportStatus.Update))
+                    })
+                    RoundDropdownMenuItem(
+                        text = stringResource(R.string.keep_original_name),
+                        isSelected = importSuccess?.keepOriginalName == true,
+                        onClick = { onIntent(BookSourceIntent.SetImportKeepName(importSuccess?.keepOriginalName != true)) },
+                    )
+                    RoundDropdownMenuItem(
+                        text = stringResource(R.string.keep_group),
+                        isSelected = importSuccess?.keepOriginalGroup == true,
+                        onClick = { onIntent(BookSourceIntent.SetImportKeepGroup(importSuccess?.keepOriginalGroup != true)) },
+                    )
+                    RoundDropdownMenuItem(
+                        text = stringResource(R.string.keep_enable),
+                        isSelected = importSuccess?.keepOriginalEnable == true,
+                        onClick = { onIntent(BookSourceIntent.SetImportKeepEnable(importSuccess?.keepOriginalEnable != true)) },
+                    )
+                    RoundDropdownMenuItem(stringResource(R.string.diy_source_group), onClick = {
+                        dismiss(); showCustomGroup = true
+                    })
+                }
+            }
+        },
+        itemTitle = { it.bookSourceName },
+        itemSubtitle = { it.bookSourceUrl },
+    )
+
+    BookSourceImportGroupDialog(
+        show = showCustomGroup,
+        initialGroup = importSuccess?.customGroup.orEmpty(),
+        initialAdd = importSuccess?.isAddGroup == true,
+        onDismissRequest = { showCustomGroup = false },
+        onConfirm = { group, add ->
+            showCustomGroup = false
+            onIntent(BookSourceIntent.SetImportCustomGroup(group, add))
+        },
+    )
+
+    CheckBookSourceSheet(
+        sourceIds = checkSourceIds.takeIf { checkSheet == CheckSheet.Run },
+        options = checkOptionsDraft,
+        onDismissRequest = { checkSheet = null },
+        onOpenSettings = { checkSheet = CheckSheet.Settings },
+        onConfirm = { ids, keyword, options ->
+            checkSheet = null
+            onIntent(BookSourceIntent.StartCheck(ids, keyword, options))
+        },
+    )
+
+    CheckSourceBottomSheet(
+        show = checkSheet == CheckSheet.Settings,
+        timeoutSeconds = checkOptionsDraft.timeoutSeconds,
+        checkSearch = checkOptionsDraft.checkSearch,
+        checkDiscovery = checkOptionsDraft.checkDiscovery,
+        checkInfo = checkOptionsDraft.checkInfo,
+        checkCategory = checkOptionsDraft.checkCategory,
+        checkContent = checkOptionsDraft.checkContent,
+        onTimeoutChange = { checkOptionsDraft = checkOptionsDraft.copy(timeoutSeconds = it) },
+        onCheckSearchChange = { enabled ->
+            checkOptionsDraft = checkOptionsDraft.copy(
+                checkSearch = enabled,
+                checkDiscovery = checkOptionsDraft.checkDiscovery || !enabled,
+            )
+        },
+        onCheckDiscoveryChange = { enabled ->
+            checkOptionsDraft = checkOptionsDraft.copy(
+                checkDiscovery = enabled,
+                checkSearch = checkOptionsDraft.checkSearch || !enabled,
+            )
+        },
+        onCheckInfoChange = { enabled ->
+            checkOptionsDraft = checkOptionsDraft.copy(
+                checkInfo = enabled,
+                checkCategory = checkOptionsDraft.checkCategory && enabled,
+                checkContent = checkOptionsDraft.checkContent && enabled,
+            )
+        },
+        onCheckCategoryChange = { enabled ->
+            checkOptionsDraft = checkOptionsDraft.copy(
+                checkCategory = enabled,
+                checkContent = checkOptionsDraft.checkContent && enabled,
+            )
+        },
+        onCheckContentChange = { checkOptionsDraft = checkOptionsDraft.copy(checkContent = it) },
+        onConfirm = {
+            onIntent(BookSourceIntent.UpdateCheckOptions(checkOptionsDraft))
+            checkSheet = CheckSheet.Run
+        },
+        onDismiss = { checkSheet = CheckSheet.Run },
+    )
 
 
     BookSourceGroupFilterSheet(
     BookSourceGroupFilterSheet(
         show = showGroupFilterSheet,
         show = showGroupFilterSheet,
@@ -199,7 +363,7 @@ fun BookSourceScreen(
                 contentDescription = stringResource(R.string.menu_action_group),
                 contentDescription = stringResource(R.string.menu_action_group),
             )
             )
         },
         },
-        snackbarHostState = remember { SnackbarHostState() },
+        snackbarHostState = snackbarHostState,
         onAddClick = onAddSource,
         onAddClick = onAddSource,
         selectionSecondaryActions = listOf(
         selectionSecondaryActions = listOf(
             ActionItem(stringResource(R.string.enable_selection)) {
             ActionItem(stringResource(R.string.enable_selection)) {
@@ -259,6 +423,16 @@ fun BookSourceScreen(
                     )
                     )
                 )
                 )
             },
             },
+            ActionItem(stringResource(R.string.check_book_source)) {
+                checkSourceIds = selectedIds
+                checkOptionsDraft = state.checkOptions
+                checkSheet = CheckSheet.Run
+                onIntent(BookSourceIntent.SetSelection(emptySet()))
+            },
+            ActionItem(stringResource(R.string.export)) {
+                pendingExportIds = selectedIds
+                exportDocument.launch("bookSource.json")
+            },
         ),
         ),
         onDeleteSelected = { deleteIds = it as Set<String> },
         onDeleteSelected = { deleteIds = it as Set<String> },
         dropDownMenuContent = { dismiss ->
         dropDownMenuContent = { dismiss ->
@@ -267,7 +441,15 @@ fun BookSourceScreen(
                 onClick = { dismiss(); groupManage = true })
                 onClick = { dismiss(); groupManage = true })
             RoundDropdownMenuItem(
             RoundDropdownMenuItem(
                 text = stringResource(R.string.import_local),
                 text = stringResource(R.string.import_local),
-                onClick = { dismiss(); onImportLocal() })
+                onClick = {
+                    dismiss(); importDocument.launch(
+                    arrayOf(
+                        "application/json",
+                        "text/plain",
+                        "text/*"
+                    )
+                )
+                })
             RoundDropdownMenuItem(
             RoundDropdownMenuItem(
                 text = stringResource(R.string.import_on_line),
                 text = stringResource(R.string.import_on_line),
                 onClick = { dismiss(); showOnlineImport = true })
                 onClick = { dismiss(); showOnlineImport = true })
@@ -346,7 +528,10 @@ fun BookSourceScreen(
                                 }
                                 }
                             },
                             },
                             title = item.name,
                             title = item.name,
-                            subtitle = item.group,
+                            subtitle = listOfNotNull(
+                                item.group,
+                                item.checkMessage
+                            ).joinToString(" · ").ifBlank { null },
                             isEnabled = item.enabled,
                             isEnabled = item.enabled,
                             isSelected = item.id in selectedIds,
                             isSelected = item.id in selectedIds,
                             canReorder = state.sort == BookSourceSort.Default && !state.groupByDomain,
                             canReorder = state.sort == BookSourceSort.Default && !state.groupByDomain,
@@ -400,6 +585,85 @@ fun BookSourceScreen(
     }
     }
 }
 }
 
 
+@Composable
+private fun BookSourceImportGroupDialog(
+    show: Boolean,
+    initialGroup: String,
+    initialAdd: Boolean,
+    onDismissRequest: () -> Unit,
+    onConfirm: (String, Boolean) -> Unit,
+) {
+    var group by remember(show, initialGroup) { mutableStateOf(initialGroup) }
+    var add by remember(show, initialAdd) { mutableStateOf(initialAdd) }
+    AppAlertDialog(
+        show = show,
+        onDismissRequest = onDismissRequest,
+        title = stringResource(R.string.diy_source_group),
+        content = {
+            Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+                AppTextField(
+                    value = group,
+                    onValueChange = { group = it },
+                    label = stringResource(R.string.group_name),
+                    modifier = Modifier.fillMaxWidth(),
+                    singleLine = true,
+                )
+                SwitchSettingItem(
+                    title = stringResource(R.string.add_group),
+                    checked = add,
+                    onCheckedChange = { add = it },
+                )
+            }
+        },
+        confirmText = stringResource(R.string.ok),
+        onConfirm = { onConfirm(group.trim(), add) },
+        dismissText = stringResource(R.string.cancel),
+        onDismiss = onDismissRequest,
+    )
+}
+
+@Composable
+private fun CheckBookSourceSheet(
+    sourceIds: Set<String>?,
+    options: BookSourceCheckOptionsUi,
+    onDismissRequest: () -> Unit,
+    onOpenSettings: () -> Unit,
+    onConfirm: (Set<String>, String, BookSourceCheckOptionsUi) -> Unit,
+) {
+    var keyword by remember(sourceIds) { mutableStateOf("我的") }
+    AppModalBottomSheet(
+        data = sourceIds,
+        onDismissRequest = onDismissRequest,
+        title = stringResource(R.string.check_book_source),
+        startAction = {
+            SmallPlainButton(
+                icon = AppIcons.Settings,
+                contentDescription = stringResource(R.string.check_source_config),
+                onClick = onOpenSettings,
+            )
+        },
+        endAction = {
+            SmallPlainButton(
+                icon = AppIcons.Check,
+                contentDescription = stringResource(R.string.check_book_source),
+                onClick = { sourceIds?.let { onConfirm(it, keyword.trim(), options) } },
+            )
+        },
+    ) {
+        AppTextField(
+            value = keyword,
+            onValueChange = { keyword = it },
+            label = stringResource(R.string.search_book_key),
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(bottom = 16.dp),
+            singleLine = true,
+        )
+    }
+}
+
+private enum class CheckSheet { Run, Settings }
+
 @Composable
 @Composable
 private fun BookSourceItemMenu(
 private fun BookSourceItemMenu(
     item: BookSourceItemUi,
     item: BookSourceItemUi,

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

@@ -4,15 +4,42 @@ import android.app.Application
 import android.text.TextUtils
 import android.text.TextUtils
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import androidx.lifecycle.viewModelScope
+import com.google.gson.JsonParser
+import io.legado.app.constant.AppConst
+import io.legado.app.constant.AppPattern
+import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.BookSourcePart
 import io.legado.app.data.entities.BookSourcePart
 import io.legado.app.data.repository.BookSourceRepository
 import io.legado.app.data.repository.BookSourceRepository
+import io.legado.app.domain.gateway.BookSourceCheckGateway
+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
+import io.legado.app.help.http.okHttpClient
+import io.legado.app.help.http.text
+import io.legado.app.help.source.SourceHelp
+import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
+import io.legado.app.ui.widget.components.importComponents.ImportItemWrapper
+import io.legado.app.ui.widget.components.importComponents.ImportStatus
+import io.legado.app.utils.GSON
 import io.legado.app.utils.NetworkUtils
 import io.legado.app.utils.NetworkUtils
+import io.legado.app.utils.fromJsonArray
+import io.legado.app.utils.fromJsonObject
+import io.legado.app.utils.isAbsUrl
+import io.legado.app.utils.isJsonArray
+import io.legado.app.utils.isJsonObject
 import io.legado.app.utils.splitNotBlank
 import io.legado.app.utils.splitNotBlank
 import kotlinx.collections.immutable.toImmutableList
 import kotlinx.collections.immutable.toImmutableList
 import kotlinx.collections.immutable.toImmutableSet
 import kotlinx.collections.immutable.toImmutableSet
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.flowOn
@@ -21,8 +48,12 @@ import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 
 
 class BookSourceViewModel(
 class BookSourceViewModel(
-    application: Application,
+    private val application: Application,
     private val repository: BookSourceRepository,
     private val repository: BookSourceRepository,
+    private val otherSettingsGateway: OtherSettingsGateway,
+    private val checkGateway: BookSourceCheckGateway,
+    private val checkSettingsGateway: CheckSourceSettingsGateway,
+    private val startBookSourceCheck: StartBookSourceCheckUseCase,
 ) : ViewModel() {
 ) : ViewModel() {
     companion object {
     companion object {
         const val FILTER_ENABLED = "@enabled"
         const val FILTER_ENABLED = "@enabled"
@@ -43,8 +74,25 @@ class BookSourceViewModel(
     private val groupByDomain = MutableStateFlow(false)
     private val groupByDomain = MutableStateFlow(false)
     private val localItems = MutableStateFlow<List<BookSourcePart>?>(null)
     private val localItems = MutableStateFlow<List<BookSourcePart>?>(null)
     private val enabledOverrides = MutableStateFlow<Map<String, Boolean>>(emptyMap())
     private val enabledOverrides = MutableStateFlow<Map<String, Boolean>>(emptyMap())
+    private val importState =
+        MutableStateFlow<BaseImportUiState<BookSource>>(BaseImportUiState.Idle)
+    private val _effects = MutableSharedFlow<BookSourceEffect>(extraBufferCapacity = 16)
+    val effects = _effects.asSharedFlow()
+    private var checkJob: Job? = null
 
 
-    val uiState = combine(
+    init {
+        viewModelScope.launch {
+            var wasRunning = false
+            checkGateway.state.collect { state ->
+                if (wasRunning && !state.isRunning) {
+                    _effects.tryEmit(BookSourceEffect.ShowSnackbar("书源校验完成"))
+                }
+                wasRunning = state.isRunning
+            }
+        }
+    }
+
+    private val listState = combine(
         repository.flowAll(),
         repository.flowAll(),
         repository.flowGroups(),
         repository.flowGroups(),
         searchKey,
         searchKey,
@@ -103,6 +151,33 @@ class BookSourceViewModel(
     }.flowOn(Dispatchers.Default)
     }.flowOn(Dispatchers.Default)
         .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), BookSourceUiState())
         .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), BookSourceUiState())
 
 
+    val uiState = combine(
+        listState,
+        importState,
+        checkGateway.state,
+        checkSettingsGateway.settings,
+    ) { state, importing, check, settings ->
+        state.copy(
+            items = state.items.map { it.copy(checkMessage = check.results[it.id]) }
+                .toImmutableList(),
+            importState = importing,
+            checkProgress = if (check.isRunning) application.getString(
+                io.legado.app.R.string.progress_show,
+                check.currentSourceName,
+                check.completed,
+                check.total,
+            ) else null,
+            checkOptions = BookSourceCheckOptionsUi(
+                timeoutSeconds = settings.timeoutMillis / 1000,
+                checkSearch = settings.checkSearch,
+                checkDiscovery = settings.checkDiscovery,
+                checkInfo = settings.checkInfo,
+                checkCategory = settings.checkCategory,
+                checkContent = settings.checkContent,
+            ),
+        )
+    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), BookSourceUiState())
+
     fun onIntent(intent: BookSourceIntent) {
     fun onIntent(intent: BookSourceIntent) {
         when (intent) {
         when (intent) {
             is BookSourceIntent.SetSearchMode -> {
             is BookSourceIntent.SetSearchMode -> {
@@ -151,6 +226,61 @@ class BookSourceViewModel(
             is BookSourceIntent.UpdateGroup -> updateGroup(intent.old, intent.new)
             is BookSourceIntent.UpdateGroup -> updateGroup(intent.old, intent.new)
             is BookSourceIntent.DeleteGroup -> updateGroup(intent.group, "")
             is BookSourceIntent.DeleteGroup -> updateGroup(intent.group, "")
             is BookSourceIntent.CheckSelectedInterval -> checkInterval(intent.ids)
             is BookSourceIntent.CheckSelectedInterval -> checkInterval(intent.ids)
+            is BookSourceIntent.StartCheck -> {
+                if (checkJob?.isActive != true) {
+                    checkJob = viewModelScope.launch {
+                        checkSettingsGateway.update(intent.options.toSettings())
+                        startBookSourceCheck(intent.ids, intent.keyword)
+                    }
+                }
+            }
+
+            is BookSourceIntent.UpdateCheckOptions -> viewModelScope.launch {
+                checkSettingsGateway.update(intent.options.toSettings())
+            }
+
+            BookSourceIntent.CancelCheck -> checkJob?.cancel()
+            is BookSourceIntent.Import -> importSources(intent.text)
+            is BookSourceIntent.Export -> exportSources(intent.uri, intent.ids)
+            is BookSourceIntent.ToggleImportItem -> updateImportItems { items ->
+                items.mapIndexed { index, item -> if (index == intent.index) item.copy(isSelected = !item.isSelected) else item }
+            }
+
+            is BookSourceIntent.ToggleImportAll -> updateImportItems { items ->
+                items.map {
+                    it.copy(
+                        isSelected = intent.selected
+                    )
+                }
+            }
+
+            is BookSourceIntent.UpdateImportItem -> updateImportItems { items ->
+                items.mapIndexed { index, item -> if (index == intent.index) item.copy(data = intent.source) else item }
+            }
+
+            is BookSourceIntent.SelectImportStatus -> selectImportStatus(intent.status)
+            is BookSourceIntent.SetImportKeepName -> updateImportOptions { it.copy(keepOriginalName = intent.enabled) }
+            is BookSourceIntent.SetImportKeepGroup -> updateImportOptions {
+                it.copy(
+                    keepOriginalGroup = intent.enabled
+                )
+            }
+
+            is BookSourceIntent.SetImportKeepEnable -> updateImportOptions {
+                it.copy(
+                    keepOriginalEnable = intent.enabled
+                )
+            }
+
+            is BookSourceIntent.SetImportCustomGroup -> updateImportOptions {
+                it.copy(
+                    customGroup = intent.group?.trim()?.takeIf(String::isNotEmpty),
+                    isAddGroup = intent.add
+                )
+            }
+
+            BookSourceIntent.CancelImport -> importState.value = BaseImportUiState.Idle
+            BookSourceIntent.SaveImportedSources -> saveImportedSources()
         }
         }
     }
     }
 
 
@@ -231,8 +361,162 @@ class BookSourceViewModel(
         if (positions.isNotEmpty()) selectedIds.value =
         if (positions.isNotEmpty()) selectedIds.value =
             items.subList(positions.min(), positions.max() + 1).map { it.id }.toSet()
             items.subList(positions.min(), positions.max() + 1).map { it.id }.toSet()
     }
     }
+
+    private fun importSources(input: String) {
+        importState.value = BaseImportUiState.Loading
+        launch {
+            runCatching {
+                val text = if (input.isAbsUrl()) {
+                    okHttpClient.newCallResponseBody {
+                        if (input.endsWith("#requestWithoutUA")) {
+                            url(input.substringBeforeLast("#requestWithoutUA")); header(
+                                AppConst.UA_NAME,
+                                "null"
+                            )
+                        } else url(input)
+                    }.decompressed().text("utf-8")
+                } else input
+                val sources = parseImportSources(text)
+                val settings = otherSettingsGateway.currentSettings
+                BaseImportUiState.Success(
+                    source = input,
+                    items = sources.map { source ->
+                        val old = repository.getBookSource(source.bookSourceUrl)
+                        val status = when {
+                            old == null -> ImportStatus.New
+                            source.lastUpdateTime > old.lastUpdateTime -> ImportStatus.Update
+                            else -> ImportStatus.Existing
+                        }
+                        ImportItemWrapper(
+                            data = source,
+                            oldData = old,
+                            isSelected = status != ImportStatus.Existing,
+                            status = status,
+                        )
+                    },
+                    keepOriginalName = settings.importKeepName,
+                    keepOriginalGroup = settings.importKeepGroup,
+                    keepOriginalEnable = settings.importKeepEnable,
+                )
+            }.onSuccess { importState.value = it }
+                .onFailure {
+                    importState.value = BaseImportUiState.Error(it.localizedMessage ?: "导入失败")
+                }
+        }
+    }
+
+    private suspend fun parseImportSources(text: String): List<BookSource> {
+        val sources = when {
+            text.isJsonArray() -> GSON.fromJsonArray<BookSource>(text).getOrThrow()
+            text.isJsonObject() -> {
+                val objectValue = JsonParser.parseString(text).asJsonObject
+                val sourceUrls = objectValue.getAsJsonArray("sourceUrls")
+                if (sourceUrls != null) {
+                    sourceUrls.flatMap { element ->
+                        val url = element.asString
+                        val sourceText = okHttpClient.newCallResponseBody {
+                            if (url.endsWith("#requestWithoutUA")) {
+                                url(url.substringBeforeLast("#requestWithoutUA")); header(
+                                    AppConst.UA_NAME,
+                                    "null"
+                                )
+                            } else url(url)
+                        }.decompressed().text("utf-8")
+                        parseImportSources(sourceText)
+                    }
+                } else listOf(GSON.fromJsonObject<BookSource>(text).getOrThrow())
+            }
+
+            else -> error("格式不正确")
+        }
+        require(sources.all { it.bookSourceUrl.isNotBlank() }) { "不是书源" }
+        return sources
+    }
+
+    private fun updateImportItems(transform: (List<ImportItemWrapper<BookSource>>) -> List<ImportItemWrapper<BookSource>>) {
+        val state = importState.value as? BaseImportUiState.Success<BookSource> ?: return
+        importState.value = state.copy(items = transform(state.items), version = state.version + 1)
+    }
+
+    private fun updateImportOptions(
+        transform: (BaseImportUiState.Success<BookSource>) -> BaseImportUiState.Success<BookSource>
+    ) {
+        val state = importState.value as? BaseImportUiState.Success<BookSource> ?: return
+        val updated = transform(state)
+        importState.value = updated
+        viewModelScope.launch {
+            otherSettingsGateway.update { settings ->
+                settings.copy(
+                    importKeepName = updated.keepOriginalName,
+                    importKeepGroup = updated.keepOriginalGroup,
+                    importKeepEnable = updated.keepOriginalEnable,
+                )
+            }
+        }
+    }
+
+    private fun selectImportStatus(status: ImportStatus) {
+        val state = importState.value as? BaseImportUiState.Success<BookSource> ?: return
+        val matching = state.items.filter { it.status == status }
+        val select = matching.any { !it.isSelected }
+        importState.value = state.copy(
+            items = state.items.map { if (it.status == status) it.copy(isSelected = select) else it }
+        )
+    }
+
+    private fun saveImportedSources() {
+        val state = importState.value as? BaseImportUiState.Success<BookSource> ?: return
+        launch {
+            val sources = state.items.filter { it.isSelected }.map { wrapper ->
+                wrapper.data.copy().apply {
+                    wrapper.oldData?.let { old ->
+                        if (state.keepOriginalName) bookSourceName = old.bookSourceName
+                        if (state.keepOriginalGroup) bookSourceGroup = old.bookSourceGroup
+                        if (state.keepOriginalEnable) {
+                            enabled = old.enabled
+                            enabledExplore = old.enabledExplore
+                        }
+                        customOrder = old.customOrder
+                    }
+                    state.customGroup?.let { group ->
+                        bookSourceGroup = if (state.isAddGroup) {
+                            linkedSetOf<String>().apply {
+                                bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)
+                                    ?.let(::addAll)
+                                add(group)
+                            }.joinToString(",")
+                        } else group
+                    }
+                }
+            }
+            SourceHelp.insertBookSource(*sources.toTypedArray())
+            ContentProcessor.upReplaceRules()
+            importState.value = BaseImportUiState.Idle
+            _effects.tryEmit(BookSourceEffect.ShowSnackbar("导入完成"))
+        }
+    }
+
+    private fun exportSources(uri: android.net.Uri, ids: Set<String>) = launch {
+        runCatching {
+            val selected = repository.getAll().filter { ids.isEmpty() || it.bookSourceUrl in ids }
+            application.contentResolver.openOutputStream(uri)?.bufferedWriter()
+                ?.use { it.write(GSON.toJson(selected)) }
+                ?: error("无法打开导出文件")
+        }.onSuccess { _effects.tryEmit(BookSourceEffect.ShowSnackbar("导出成功")) }
+            .onFailure { _effects.tryEmit(BookSourceEffect.ShowSnackbar("导出失败: ${it.localizedMessage}")) }
+    }
+
 }
 }
 
 
+private fun BookSourceCheckOptionsUi.toSettings() = CheckSourceSettings(
+    timeoutMillis = timeoutSeconds * 1000,
+    checkSearch = checkSearch,
+    checkDiscovery = checkDiscovery,
+    checkInfo = checkInfo,
+    checkCategory = checkCategory,
+    checkContent = checkContent,
+)
+
 private fun List<BookSourcePart>.filterFor(filter: String?, query: String): List<BookSourcePart> =
 private fun List<BookSourcePart>.filterFor(filter: String?, query: String): List<BookSourcePart> =
     filter { source ->
     filter { source ->
         val filterMatch = when (filter) {
         val filterMatch = when (filter) {

+ 29 - 19
app/src/main/java/io/legado/app/ui/config/otherConfig/CheckSourceBottomSheet.kt → app/src/main/java/io/legado/app/ui/book/source/manage/CheckSourceBottomSheet.kt

@@ -1,8 +1,7 @@
-package io.legado.app.ui.config.otherConfig
+package io.legado.app.ui.book.source.manage
 
 
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.padding
@@ -22,8 +21,19 @@ import io.legado.app.ui.widget.components.settingItem.SliderSettingItem
 @Composable
 @Composable
 fun CheckSourceBottomSheet(
 fun CheckSourceBottomSheet(
     show: Boolean,
     show: Boolean,
-    state: OtherConfigUiState,
-    onIntent: (OtherConfigIntent) -> Unit,
+    timeoutSeconds: Long,
+    checkSearch: Boolean,
+    checkDiscovery: Boolean,
+    checkInfo: Boolean,
+    checkCategory: Boolean,
+    checkContent: Boolean,
+    onTimeoutChange: (Long) -> Unit,
+    onCheckSearchChange: (Boolean) -> Unit,
+    onCheckDiscoveryChange: (Boolean) -> Unit,
+    onCheckInfoChange: (Boolean) -> Unit,
+    onCheckCategoryChange: (Boolean) -> Unit,
+    onCheckContentChange: (Boolean) -> Unit,
+    onConfirm: () -> Unit,
     onDismiss: () -> Unit,
     onDismiss: () -> Unit,
 ) {
 ) {
 
 
@@ -41,9 +51,9 @@ fun CheckSourceBottomSheet(
             GlassCard {
             GlassCard {
                 SliderSettingItem(
                 SliderSettingItem(
                     title = stringResource(R.string.check_source_timeout),
                     title = stringResource(R.string.check_source_timeout),
-                    value = state.checkSourceTimeoutSeconds.toFloat(),
+                    value = timeoutSeconds.toFloat(),
                     defaultValue = 180f,
                     defaultValue = 180f,
-                    onValueChange = { onIntent(OtherConfigIntent.CheckSourceTimeoutChanged(it.toLong())) },
+                    onValueChange = { onTimeoutChange(it.toLong()) },
                     valueRange = 0f..300f,
                     valueRange = 0f..300f,
                 )
                 )
             }
             }
@@ -56,38 +66,38 @@ fun CheckSourceBottomSheet(
             ) {
             ) {
                 CheckboxItem(
                 CheckboxItem(
                     title = stringResource(R.string.search),
                     title = stringResource(R.string.search),
-                    checked = state.checkSearch,
-                    onCheckedChange = { onIntent(OtherConfigIntent.CheckSearchChanged(it)) }
+                    checked = checkSearch,
+                    onCheckedChange = onCheckSearchChange
                 )
                 )
 
 
                 CheckboxItem(
                 CheckboxItem(
                     title = stringResource(R.string.discovery),
                     title = stringResource(R.string.discovery),
-                    checked = state.checkDiscovery,
-                    onCheckedChange = { onIntent(OtherConfigIntent.CheckDiscoveryChanged(it)) }
+                    checked = checkDiscovery,
+                    onCheckedChange = onCheckDiscoveryChange
                 )
                 )
 
 
 
 
 
 
                 CheckboxItem(
                 CheckboxItem(
                     title = stringResource(R.string.source_tab_info),
                     title = stringResource(R.string.source_tab_info),
-                    checked = state.checkInfo,
-                    onCheckedChange = { onIntent(OtherConfigIntent.CheckInfoChanged(it)) }
+                    checked = checkInfo,
+                    onCheckedChange = onCheckInfoChange
                 )
                 )
 
 
 
 
                 CheckboxItem(
                 CheckboxItem(
                     title = stringResource(R.string.chapter_list),
                     title = stringResource(R.string.chapter_list),
-                    checked = state.checkCategory,
-                    enabled = state.checkInfo,
-                    onCheckedChange = { onIntent(OtherConfigIntent.CheckCategoryChanged(it)) }
+                    checked = checkCategory,
+                    enabled = checkInfo,
+                    onCheckedChange = onCheckCategoryChange
                 )
                 )
 
 
 
 
                 CheckboxItem(
                 CheckboxItem(
                     title = stringResource(R.string.source_tab_content),
                     title = stringResource(R.string.source_tab_content),
-                    checked = state.checkContent,
-                    enabled = state.checkCategory,
-                    onCheckedChange = { onIntent(OtherConfigIntent.CheckContentChanged(it)) }
+                    checked = checkContent,
+                    enabled = checkCategory,
+                    onCheckedChange = onCheckContentChange
                 )
                 )
             }
             }
 
 
@@ -96,7 +106,7 @@ fun CheckSourceBottomSheet(
                     .fillMaxWidth()
                     .fillMaxWidth()
                     .padding(top = 16.dp),
                     .padding(top = 16.dp),
                 onDismiss = onDismiss,
                 onDismiss = onDismiss,
-                onConfirm = { onIntent(OtherConfigIntent.ConfirmCheckSource) },
+                onConfirm = onConfirm,
                 dismissText = stringResource(R.string.cancel),
                 dismissText = stringResource(R.string.cancel),
                 confirmText = stringResource(R.string.ok)
                 confirmText = stringResource(R.string.ok)
             )
             )

+ 0 - 113
app/src/main/java/io/legado/app/ui/config/CheckSourceConfig.kt

@@ -1,113 +0,0 @@
-package io.legado.app.ui.config
-
-import android.os.Bundle
-import android.view.View
-import androidx.lifecycle.lifecycleScope
-import io.legado.app.R
-import io.legado.app.base.BaseBottomSheetDialogFragment
-import io.legado.app.databinding.DialogCheckSourceConfigBinding
-import io.legado.app.domain.gateway.CheckSourceSettings
-import io.legado.app.domain.gateway.CheckSourceSettingsGateway
-import io.legado.app.utils.toastOnUi
-import io.legado.app.utils.viewbindingdelegate.viewBinding
-import kotlinx.coroutines.launch
-import org.koin.android.ext.android.inject
-import splitties.views.onClick
-
-class CheckSourceConfig : BaseBottomSheetDialogFragment(R.layout.dialog_check_source_config) {
-
-    private val binding by viewBinding(DialogCheckSourceConfigBinding::bind)
-    private val settingsGateway: CheckSourceSettingsGateway by inject()
-
-    //允许的最小超时时间,秒
-    private val minTimeout = 0L
-
-    override fun onStart() {
-        super.onStart()
-    }
-
-    override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
-        //binding.toolBar.setBackgroundColor(primaryColor)
-        binding.run {
-            checkSearch.onClick {
-                if (!checkSearch.isChecked && !checkDiscovery.isChecked) {
-                    checkDiscovery.isChecked = true
-                }
-            }
-            checkDiscovery.onClick {
-                if (!checkSearch.isChecked && !checkDiscovery.isChecked) {
-                    checkSearch.isChecked = true
-                }
-            }
-            checkInfo.onClick {
-                if (!checkInfo.isChecked) {
-                    checkCategory.isChecked = false
-                    checkContent.isChecked = false
-                    checkCategory.isEnabled = false
-                    checkContent.isEnabled = false
-                } else {
-                    checkCategory.isEnabled = true
-                }
-            }
-            checkCategory.onClick {
-                if (!checkCategory.isChecked) {
-                    checkContent.isChecked = false
-                    checkContent.isEnabled = false
-                } else {
-                    checkContent.isEnabled = true
-                }
-            }
-        }
-        settingsGateway.currentSettings.run {
-            binding.checkSourceTimeout.setText((timeoutMillis / 1000).toString())
-            binding.checkSearch.isChecked = checkSearch
-            binding.checkDiscovery.isChecked = checkDiscovery
-            binding.checkInfo.isChecked = checkInfo
-            binding.checkCategory.isChecked = checkCategory
-            binding.checkContent.isChecked = checkContent
-            binding.checkCategory.isEnabled = checkInfo
-            binding.checkContent.isEnabled = checkCategory
-            binding.tvCancel.onClick {
-                dismiss()
-            }
-            binding.tvOk.onClick {
-                val text = binding.checkSourceTimeout.text.toString()
-                when {
-                    text.isBlank() -> {
-                        toastOnUi("${getString(R.string.timeout)}${getString(R.string.cannot_empty)}")
-                        return@onClick
-                    }
-                    text.toLong() <= minTimeout -> {
-                        toastOnUi(
-                            "${getString(R.string.timeout)}${getString(R.string.less_than)}${minTimeout}${
-                                getString(
-                                    R.string.seconds
-                                )
-                            }"
-                        )
-                        return@onClick
-                    }
-                    else -> Unit
-                }
-                viewLifecycleOwner.lifecycleScope.launch {
-                    runCatching {
-                        settingsGateway.update(
-                            CheckSourceSettings(
-                                timeoutMillis = text.toLong() * 1000,
-                                checkSearch = binding.checkSearch.isChecked,
-                                checkDiscovery = binding.checkDiscovery.isChecked,
-                                checkInfo = binding.checkInfo.isChecked,
-                                checkCategory = binding.checkCategory.isChecked,
-                                checkContent = binding.checkContent.isChecked,
-                            )
-                        )
-                    }.onSuccess {
-                        dismiss()
-                    }.onFailure {
-                        toastOnUi(it.localizedMessage ?: getString(R.string.error))
-                    }
-                }
-            }
-        }
-    }
-}

+ 1 - 15
app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigContract.kt

@@ -1,7 +1,7 @@
 package io.legado.app.ui.config.otherConfig
 package io.legado.app.ui.config.otherConfig
 
 
-import androidx.compose.runtime.Stable
 import androidx.annotation.StringRes
 import androidx.annotation.StringRes
+import androidx.compose.runtime.Stable
 import kotlinx.collections.immutable.ImmutableList
 import kotlinx.collections.immutable.ImmutableList
 import kotlinx.collections.immutable.persistentListOf
 import kotlinx.collections.immutable.persistentListOf
 
 
@@ -29,12 +29,6 @@ data class OtherConfigUiState(
     val processText: Boolean = true,
     val processText: Boolean = true,
     val recordLog: Boolean = false,
     val recordLog: Boolean = false,
     val recordHeapDump: Boolean = false,
     val recordHeapDump: Boolean = false,
-    val checkSourceTimeoutSeconds: Long = 180,
-    val checkSearch: Boolean = true,
-    val checkDiscovery: Boolean = true,
-    val checkInfo: Boolean = true,
-    val checkCategory: Boolean = true,
-    val checkContent: Boolean = true,
     val directUploadUrl: String = "",
     val directUploadUrl: String = "",
     val directDownloadUrlRule: String = "",
     val directDownloadUrlRule: String = "",
     val directSummary: String = "",
     val directSummary: String = "",
@@ -76,7 +70,6 @@ data class OtherConfigMessage(
 
 
 sealed interface OtherConfigOverlay {
 sealed interface OtherConfigOverlay {
     data object FilePicker : OtherConfigOverlay
     data object FilePicker : OtherConfigOverlay
-    data object CheckSource : OtherConfigOverlay
     data object DirectLinkUpload : OtherConfigOverlay
     data object DirectLinkUpload : OtherConfigOverlay
     data object ClearWebViewConfirmation : OtherConfigOverlay
     data object ClearWebViewConfirmation : OtherConfigOverlay
     data object Password : OtherConfigOverlay
     data object Password : OtherConfigOverlay
@@ -105,13 +98,6 @@ sealed interface OtherConfigIntent {
     data class ProcessTextChanged(val value: Boolean) : OtherConfigIntent
     data class ProcessTextChanged(val value: Boolean) : OtherConfigIntent
     data class RecordLogChanged(val value: Boolean) : OtherConfigIntent
     data class RecordLogChanged(val value: Boolean) : OtherConfigIntent
     data class RecordHeapDumpChanged(val value: Boolean) : OtherConfigIntent
     data class RecordHeapDumpChanged(val value: Boolean) : OtherConfigIntent
-    data class CheckSourceTimeoutChanged(val value: Long) : OtherConfigIntent
-    data class CheckSearchChanged(val value: Boolean) : OtherConfigIntent
-    data class CheckDiscoveryChanged(val value: Boolean) : OtherConfigIntent
-    data class CheckInfoChanged(val value: Boolean) : OtherConfigIntent
-    data class CheckCategoryChanged(val value: Boolean) : OtherConfigIntent
-    data class CheckContentChanged(val value: Boolean) : OtherConfigIntent
-    data object ConfirmCheckSource : OtherConfigIntent
     data class DirectUploadUrlChanged(val value: String) : OtherConfigIntent
     data class DirectUploadUrlChanged(val value: String) : OtherConfigIntent
     data class DirectDownloadUrlRuleChanged(val value: String) : OtherConfigIntent
     data class DirectDownloadUrlRuleChanged(val value: String) : OtherConfigIntent
     data class DirectSummaryChanged(val value: String) : OtherConfigIntent
     data class DirectSummaryChanged(val value: String) : OtherConfigIntent

+ 0 - 7
app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigRouteScreen.kt

@@ -105,13 +105,6 @@ fun OtherConfigRouteScreen(
         },
         },
     )
     )
 
 
-    CheckSourceBottomSheet(
-        show = state.activeOverlay == OtherConfigOverlay.CheckSource,
-        state = state,
-        onIntent = viewModel::onIntent,
-        onDismiss = { viewModel.onIntent(OtherConfigIntent.DismissOverlay) },
-    )
-
     DirectLinkUploadBottomSheet(
     DirectLinkUploadBottomSheet(
         show = state.activeOverlay == OtherConfigOverlay.DirectLinkUpload,
         show = state.activeOverlay == OtherConfigOverlay.DirectLinkUpload,
         state = state,
         state = state,

+ 0 - 5
app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigScreen.kt

@@ -208,11 +208,6 @@ fun OtherConfigScreen(
                     onConfirm = { onIntent(OtherConfigIntent.SourceEditMaxLineChanged(it.toIntOrNull() ?: 500)) }
                     onConfirm = { onIntent(OtherConfigIntent.SourceEditMaxLineChanged(it.toIntOrNull() ?: 500)) }
                 )
                 )
 
 
-                ClickableSettingItem(
-                    title = stringResource(R.string.check_source_config),
-                    onClick = { onIntent(OtherConfigIntent.ShowOverlay(OtherConfigOverlay.CheckSource)) }
-                )
-
                 ClickableSettingItem(
                 ClickableSettingItem(
                     title = stringResource(R.string.direct_link_upload_rule),
                     title = stringResource(R.string.direct_link_upload_rule),
                     description = stringResource(R.string.direct_link_upload_rule_summary),
                     description = stringResource(R.string.direct_link_upload_rule_summary),

+ 2 - 80
app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt

@@ -6,8 +6,6 @@ import androidx.lifecycle.viewModelScope
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppLog
 import io.legado.app.domain.gateway.AppLocaleGateway
 import io.legado.app.domain.gateway.AppLocaleGateway
-import io.legado.app.domain.gateway.CheckSourceSettings
-import io.legado.app.domain.gateway.CheckSourceSettingsGateway
 import io.legado.app.domain.gateway.DirectLinkRule
 import io.legado.app.domain.gateway.DirectLinkRule
 import io.legado.app.domain.gateway.DirectLinkSettingsGateway
 import io.legado.app.domain.gateway.DirectLinkSettingsGateway
 import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
 import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
@@ -17,6 +15,7 @@ import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.domain.gateway.ReadAloudSettingsGateway
 import io.legado.app.domain.gateway.ReadAloudSettingsGateway
 import io.legado.app.domain.model.settings.OtherSettings
 import io.legado.app.domain.model.settings.OtherSettings
 import io.legado.app.domain.model.settings.ReadAloudSettings
 import io.legado.app.domain.model.settings.ReadAloudSettings
+import kotlinx.collections.immutable.toImmutableList
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -24,14 +23,12 @@ import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
-import kotlinx.collections.immutable.toImmutableList
 
 
 class OtherConfigViewModel(
 class OtherConfigViewModel(
     private val appLocaleGateway: AppLocaleGateway,
     private val appLocaleGateway: AppLocaleGateway,
     private val readAloudSettingsGateway: ReadAloudSettingsGateway,
     private val readAloudSettingsGateway: ReadAloudSettingsGateway,
     private val otherSettingsGateway: OtherSettingsGateway,
     private val otherSettingsGateway: OtherSettingsGateway,
     private val downloadCacheSettingsGateway: DownloadCacheSettingsGateway,
     private val downloadCacheSettingsGateway: DownloadCacheSettingsGateway,
-    private val checkSourceSettingsGateway: CheckSourceSettingsGateway,
     private val directLinkSettingsGateway: DirectLinkSettingsGateway,
     private val directLinkSettingsGateway: DirectLinkSettingsGateway,
     private val localPasswordGateway: LocalPasswordGateway,
     private val localPasswordGateway: LocalPasswordGateway,
     private val systemGateway: OtherConfigSystemGateway,
     private val systemGateway: OtherConfigSystemGateway,
@@ -43,9 +40,7 @@ class OtherConfigViewModel(
     private var nextMessageId = 0L
     private var nextMessageId = 0L
 
 
     private val _uiState = MutableStateFlow(
     private val _uiState = MutableStateFlow(
-        otherSettingsGateway.currentSettings.toUiState(initialState)
-            .let(checkSourceSettingsGateway.currentSettings::toUiState)
-            .copy(
+        otherSettingsGateway.currentSettings.toUiState(initialState).copy(
             mediaButtonOnExit = readAloudSettingsGateway.currentSettings.mediaButtonOnExit,
             mediaButtonOnExit = readAloudSettingsGateway.currentSettings.mediaButtonOnExit,
             readAloudByMediaButton =
             readAloudByMediaButton =
                 readAloudSettingsGateway.currentSettings.readAloudByMediaButton,
                 readAloudSettingsGateway.currentSettings.readAloudByMediaButton,
@@ -80,11 +75,6 @@ class OtherConfigViewModel(
                 }
                 }
             }
             }
         }
         }
-        viewModelScope.launch {
-            checkSourceSettingsGateway.settings.collect { settings ->
-                _uiState.update(settings::toUiState)
-            }
-        }
         loadDirectLinkConfiguration()
         loadDirectLinkConfiguration()
     }
     }
 
 
@@ -137,38 +127,6 @@ class OtherConfigViewModel(
                 updateOtherSetting { it.copy(recordLog = intent.value) }
                 updateOtherSetting { it.copy(recordLog = intent.value) }
             is OtherConfigIntent.RecordHeapDumpChanged ->
             is OtherConfigIntent.RecordHeapDumpChanged ->
                 updateOtherSetting { it.copy(recordHeapDump = intent.value) }
                 updateOtherSetting { it.copy(recordHeapDump = intent.value) }
-            is OtherConfigIntent.CheckSourceTimeoutChanged ->
-                _uiState.update { it.copy(checkSourceTimeoutSeconds = intent.value) }
-            is OtherConfigIntent.CheckSearchChanged -> _uiState.update {
-                it.copy(
-                    checkSearch = intent.value,
-                    checkDiscovery = if (!intent.value && !it.checkDiscovery) true else it.checkDiscovery,
-                )
-            }
-            is OtherConfigIntent.CheckDiscoveryChanged -> _uiState.update {
-                it.copy(
-                    checkDiscovery = intent.value,
-                    checkSearch = if (!intent.value && !it.checkSearch) true else it.checkSearch,
-                )
-            }
-            is OtherConfigIntent.CheckInfoChanged -> _uiState.update {
-                it.copy(
-                    checkInfo = intent.value,
-                    checkCategory = if (intent.value) it.checkCategory else false,
-                    checkContent = if (intent.value) it.checkContent else false,
-                )
-            }
-            is OtherConfigIntent.CheckCategoryChanged -> _uiState.update {
-                it.copy(
-                    checkCategory = intent.value,
-                    checkContent = if (intent.value) it.checkContent else false,
-                )
-            }
-            is OtherConfigIntent.CheckContentChanged ->
-                _uiState.update { it.copy(checkContent = intent.value) }
-            OtherConfigIntent.ConfirmCheckSource -> {
-                saveCheckSourceConfig()
-            }
             is OtherConfigIntent.DirectUploadUrlChanged ->
             is OtherConfigIntent.DirectUploadUrlChanged ->
                 _uiState.update { it.copy(directUploadUrl = intent.value) }
                 _uiState.update { it.copy(directUploadUrl = intent.value) }
             is OtherConfigIntent.DirectDownloadUrlRuleChanged ->
             is OtherConfigIntent.DirectDownloadUrlRuleChanged ->
@@ -295,32 +253,6 @@ class OtherConfigViewModel(
         updateOtherSetting { it.copy(defaultBookTreeUri = path) }
         updateOtherSetting { it.copy(defaultBookTreeUri = path) }
     }
     }
 
 
-    private fun saveCheckSourceConfig() {
-        val state = _uiState.value
-        if (state.checkSourceTimeoutSeconds <= 0L) {
-            showMessage(R.string.error)
-            return
-        }
-        viewModelScope.launch {
-            runCatching {
-                checkSourceSettingsGateway.update(
-                    CheckSourceSettings(
-                        timeoutMillis = state.checkSourceTimeoutSeconds * 1_000L,
-                        checkSearch = state.checkSearch,
-                        checkDiscovery = state.checkDiscovery,
-                        checkInfo = state.checkInfo,
-                        checkCategory = state.checkCategory,
-                        checkContent = state.checkContent,
-                    )
-                )
-            }.onSuccess {
-                _uiState.update { it.copy(activeOverlay = null) }
-            }.onFailure {
-                showMessage(it.localizedMessage ?: "设置失败")
-            }
-        }
-    }
-
     private fun updateDirectLinkRule(rule: DirectLinkRule) {
     private fun updateDirectLinkRule(rule: DirectLinkRule) {
         _uiState.update {
         _uiState.update {
             it.copy(
             it.copy(
@@ -419,16 +351,6 @@ private fun DirectLinkRule.toUi() = DirectLinkRuleUi(
     compress = compress,
     compress = compress,
 )
 )
 
 
-private fun CheckSourceSettings.toUiState(current: OtherConfigUiState): OtherConfigUiState =
-    current.copy(
-        checkSourceTimeoutSeconds = timeoutMillis / 1_000L,
-        checkSearch = checkSearch,
-        checkDiscovery = checkDiscovery,
-        checkInfo = checkInfo,
-        checkCategory = checkCategory,
-        checkContent = checkContent,
-    )
-
 private fun OtherSettings.toUiState(current: OtherConfigUiState): OtherConfigUiState =
 private fun OtherSettings.toUiState(current: OtherConfigUiState): OtherConfigUiState =
     current.copy(
     current.copy(
         updateToVariant = updateToVariant,
         updateToVariant = updateToVariant,

+ 25 - 6
app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditScreen.kt

@@ -21,7 +21,6 @@ import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
@@ -44,16 +43,36 @@ import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.TopBarActionButton
 import io.legado.app.ui.widget.components.topbar.TopBarActionButton
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
-import kotlinx.coroutines.launch
 
 
 @Composable fun RssSourceEditScreen(state:RssSourceEditUiState,onIntent:(RssSourceEditIntent)->Unit,onBack:()->Unit){
 @Composable fun RssSourceEditScreen(state:RssSourceEditUiState,onIntent:(RssSourceEditIntent)->Unit,onBack:()->Unit){
-    BackHandler{onBack()};val tabs=RssSourceEditTab.entries;val pager=rememberPagerState(state.selectedTab.ordinal){tabs.size};val scope=rememberCoroutineScope();val scroll=GlassTopAppBarDefaults.defaultScrollBehavior();var menu by remember{mutableStateOf(false)}
-    LaunchedEffect(state.selectedTab){if(pager.currentPage!=state.selectedTab.ordinal)pager.animateScrollToPage(state.selectedTab.ordinal)}
-    LaunchedEffect(pager){snapshotFlow{pager.currentPage}.collect{onIntent(RssSourceEditIntent.SelectTab(tabs[it]))}}
+    BackHandler { onBack() };
+    val tabs = RssSourceEditTab.entries;
+    val pager = rememberPagerState(state.selectedTab.ordinal) { tabs.size };
+    val scroll = GlassTopAppBarDefaults.defaultScrollBehavior();
+    var menu by remember { mutableStateOf(false) }
+    LaunchedEffect(state.selectedTab) {
+        if (pager.settledPage != state.selectedTab.ordinal) pager.animateScrollToPage(
+            state.selectedTab.ordinal
+        )
+    }
+    LaunchedEffect(pager) {
+        snapshotFlow { pager.settledPage }.collect {
+            onIntent(
+                RssSourceEditIntent.SelectTab(tabs[it])
+            )
+        }
+    }
     AppScaffold(modifier=Modifier.nestedScroll(scroll.nestedScrollConnection),topBar={Column{GlassMediumFlexibleTopAppBar(
     AppScaffold(modifier=Modifier.nestedScroll(scroll.nestedScrollConnection),topBar={Column{GlassMediumFlexibleTopAppBar(
         title=stringResource(R.string.rss_source_edit),navigationIcon={TopBarNavigationButton(onClick=onBack)},scrollBehavior=scroll,
         title=stringResource(R.string.rss_source_edit),navigationIcon={TopBarNavigationButton(onClick=onBack)},scrollBehavior=scroll,
         actions={TopBarActionButton({onIntent(RssSourceEditIntent.SaveDebug)},Icons.Default.BugReport,stringResource(R.string.debug_source));TopBarActionButton({menu=true},Icons.Default.MoreVert,stringResource(R.string.more_menu));RssEditMenu(menu,{menu=false},onIntent)}
         actions={TopBarActionButton({onIntent(RssSourceEditIntent.SaveDebug)},Icons.Default.BugReport,stringResource(R.string.debug_source));TopBarActionButton({menu=true},Icons.Default.MoreVert,stringResource(R.string.more_menu));RssEditMenu(menu,{menu=false},onIntent)}
-    );AppTabRow(tabs.map{stringResource(it.title)},state.selectedTab.ordinal,{p->onIntent(RssSourceEditIntent.SelectTab(tabs[p]));scope.launch{pager.animateScrollToPage(p)}},Modifier.fillMaxWidth())}},
+    ); AppTabRow(
+        tabs.map { stringResource(it.title) },
+        state.selectedTab.ordinal,
+        { p -> onIntent(RssSourceEditIntent.SelectTab(tabs[p])) },
+        Modifier.fillMaxWidth()
+    )
+    }
+    },
         floatingActionButton={AppFloatingActionButton({onIntent(RssSourceEditIntent.Save)},icon=Icons.Default.Save,tooltipText=stringResource(R.string.action_save))}){padding->
         floatingActionButton={AppFloatingActionButton({onIntent(RssSourceEditIntent.Save)},icon=Icons.Default.Save,tooltipText=stringResource(R.string.action_save))}){padding->
         HorizontalPager(pager,Modifier.fillMaxSize()){page->val tab=tabs[page];LazyColumn(Modifier.fillMaxSize(),contentPadding=PaddingValues(16.dp,padding.calculateTopPadding()+12.dp,16.dp,padding.calculateBottomPadding()+96.dp),verticalArrangement=Arrangement.spacedBy(10.dp)){
         HorizontalPager(pager,Modifier.fillMaxSize()){page->val tab=tabs[page];LazyColumn(Modifier.fillMaxSize(),contentPadding=PaddingValues(16.dp,padding.calculateTopPadding()+12.dp,16.dp,padding.calculateBottomPadding()+96.dp),verticalArrangement=Arrangement.spacedBy(10.dp)){
             if(tab==RssSourceEditTab.Base)item("options"){RssEditOptions(state,onIntent)}
             if(tab==RssSourceEditTab.Base)item("options"){RssEditOptions(state,onIntent)}

+ 2 - 0
app/src/main/java/io/legado/app/ui/widget/components/importComponents/ImportState.kt

@@ -27,6 +27,8 @@ sealed interface BaseImportUiState<out T> {
         val version: Int = 0,
         val version: Int = 0,
         // 导入配置项
         // 导入配置项
         val keepOriginalName: Boolean = false,
         val keepOriginalName: Boolean = false,
+        val keepOriginalGroup: Boolean = false,
+        val keepOriginalEnable: Boolean = false,
         val customGroup: String? = null,
         val customGroup: String? = null,
         val isAddGroup: Boolean = false
         val isAddGroup: Boolean = false
     ) : BaseImportUiState<T>
     ) : BaseImportUiState<T>

+ 0 - 35
app/src/main/res/layout/activity_book_source.xml

@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical">
-
-    <io.legado.app.ui.widget.TitleBar
-        android:id="@+id/title_bar"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:contentInsetStartWithNavigation="0dp"
-        app:contentLayout="@layout/view_search"
-        app:displayHomeAsUp="true"
-        app:title="@string/book_source" />
-
-    <FrameLayout
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="1">
-
-        <io.legado.app.ui.widget.recycler.scroller.FastScrollRecyclerView
-            android:id="@+id/recycler_view"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
-
-    </FrameLayout>
-
-    <io.legado.app.ui.widget.SelectActionBar
-        android:id="@+id/select_action_bar"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content" />
-
-</LinearLayout>

+ 0 - 115
app/src/main/res/layout/dialog_check_source_config.xml

@@ -1,115 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:orientation="vertical"
-    android:paddingHorizontal="16dp"
-    android:paddingBottom="8dp">
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="vertical"
-        android:gravity="center"
-        android:paddingBottom="8dp"
-        android:paddingHorizontal="16dp">
-
-        <com.google.android.material.bottomsheet.BottomSheetDragHandleView
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:paddingVertical="16dp"
-            android:minHeight="0dp"
-            android:layout_gravity="center_horizontal" />
-
-        <TextView
-            android:id="@+id/bottom_sheet_title"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/check_source_config"
-            android:textSize="18sp"
-            android:textStyle="bold" />
-
-    </LinearLayout>
-
-    <com.google.android.material.textfield.TextInputLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:paddingTop="8dp">
-
-        <com.google.android.material.textfield.TextInputEditText
-            android:id="@+id/check_source_timeout"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:hint="@string/check_source_timeout"
-            android:digits="1234567890"
-            tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
-
-    </com.google.android.material.textfield.TextInputLayout>
-
-    <TextView
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:paddingVertical="8dp"
-        android:text="@string/check_source_item" />
-
-    <com.google.android.flexbox.FlexboxLayout
-        android:layout_width="match_parent"
-
-        android:layout_height="wrap_content"
-        app:flexWrap="wrap"
-        app:justifyContent="space_between">
-
-        <com.google.android.material.checkbox.MaterialCheckBox
-            android:id="@+id/check_search"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/search" />
-        <com.google.android.material.checkbox.MaterialCheckBox
-            android:id="@+id/check_discovery"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/discovery" />
-        <com.google.android.material.checkbox.MaterialCheckBox
-            android:id="@+id/check_info"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/source_tab_info" />
-        <com.google.android.material.checkbox.MaterialCheckBox
-            android:id="@+id/check_category"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/chapter_list" />
-        <com.google.android.material.checkbox.MaterialCheckBox
-            android:id="@+id/check_content"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/main_body" />
-
-    </com.google.android.flexbox.FlexboxLayout>
-
-    <com.google.android.flexbox.FlexboxLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:justifyContent="flex_end">
-
-        <com.google.android.material.button.MaterialButton
-            android:id="@+id/tv_cancel"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            style="@style/Widget.Material3Expressive.Button.TonalButton"
-            android:layout_marginEnd="8dp"
-            android:text="@string/cancel"
-            tools:ignore="RtlHardcoded" />
-
-        <com.google.android.material.button.MaterialButton
-            android:id="@+id/tv_ok"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            style="@style/Widget.Material3Expressive.Button"
-            android:text="@string/ok"
-            tools:ignore="RtlHardcoded" />
-
-    </com.google.android.flexbox.FlexboxLayout>
-</LinearLayout>

+ 0 - 131
app/src/main/res/layout/item_book_source.xml

@@ -1,131 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:background="?attr/selectableItemBackground"
-    android:clipToPadding="false"
-    android:paddingHorizontal="8dp"
-    android:paddingVertical="16dp"
-    android:scrollbars="none">
-
-    <TextView
-        android:id="@+id/tv_host_text"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:scrollbars="none"
-        android:singleLine="true"
-        android:textSize="16sp"
-        android:visibility="gone"
-        app:layout_constraintBottom_toTopOf="@id/barrier"
-        app:layout_constraintLeft_toLeftOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        tools:text="host"
-        tools:visibility="visible" />
-
-    <androidx.constraintlayout.widget.Barrier
-        android:id="@+id/barrier"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        app:barrierDirection="top"
-        app:constraint_referenced_ids="cb_book_source,swt_enabled,iv_edit,iv_menu_more" />
-
-    <io.legado.app.lib.theme.view.ThemeCheckBox
-        android:id="@+id/cb_book_source"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:minHeight="48dp"
-        android:layout_marginStart="4dp"
-        android:layout_marginEnd="12dp"
-        android:scrollbars="none"
-        android:singleLine="true"
-        app:layout_constraintBottom_toTopOf="@id/iv_debug_text"
-        app:layout_constraintLeft_toLeftOf="parent"
-        app:layout_constraintRight_toLeftOf="@id/swt_enabled"
-        app:layout_constraintTop_toBottomOf="@id/barrier"
-        tools:text="@string/book_source" />
-
-    <io.legado.app.lib.theme.view.ThemeSwitch
-        android:id="@+id/swt_enabled"
-        android:name="@string/enable"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:minHeight="48dp"
-        android:layout_marginEnd="12dp"
-        android:scrollbars="none"
-        app:layout_constraintBottom_toTopOf="@id/iv_debug_text"
-        app:layout_constraintRight_toLeftOf="@id/iv_edit"
-        app:layout_constraintTop_toBottomOf="@id/barrier"
-        tools:ignore="RtlHardcoded,RtlSymmetry" />
-
-    <androidx.appcompat.widget.AppCompatImageView
-        android:id="@+id/iv_edit"
-        android:layout_width="48dp"
-        android:layout_height="48dp"
-        android:layout_marginEnd="8dp"
-        android:background="?attr/selectableItemBackgroundBorderless"
-        android:layout_gravity="center"
-        android:padding="12dp"
-        android:contentDescription="@string/edit"
-        android:scrollbars="none"
-        android:src="@drawable/ic_edit"
-        app:layout_constraintBottom_toTopOf="@id/iv_debug_text"
-        app:layout_constraintRight_toLeftOf="@id/iv_menu_more"
-        app:layout_constraintTop_toBottomOf="@id/barrier" />
-
-    <androidx.appcompat.widget.AppCompatImageView
-        android:id="@+id/iv_menu_more"
-        android:layout_width="48dp"
-        android:layout_height="48dp"
-        android:padding="12dp"
-        android:background="?attr/selectableItemBackgroundBorderless"
-        android:contentDescription="@string/more_menu"
-        android:scrollbars="none"
-        android:src="@drawable/ic_more_vert"
-        app:layout_constraintBottom_toTopOf="@id/iv_debug_text"
-        app:layout_constraintRight_toRightOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/barrier"
-        tools:ignore="RtlHardcoded" />
-
-    <com.google.android.material.card.MaterialCardView
-        android:id="@+id/iv_explore"
-        android:layout_width="8dp"
-        android:layout_height="8dp"
-        app:strokeWidth="0dp"
-        android:scaleType="centerCrop"
-        android:scrollbars="none"
-        android:src="@color/md_green_600"
-        app:layout_constraintRight_toRightOf="@id/iv_menu_more"
-        app:layout_constraintTop_toTopOf="@id/iv_menu_more" />
-
-    <TextView
-        android:id="@+id/iv_debug_text"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="6dp"
-        android:scrollbars="none"
-        android:singleLine="true"
-        android:visibility="gone"
-        android:layout_marginStart="4dp"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintStart_toEndOf="@id/iv_progressBar"
-        app:layout_constraintTop_toBottomOf="@id/cb_book_source"
-        app:layout_constraintEnd_toEndOf="parent"
-        tools:text="@string/debug_hint"
-        tools:visibility="visible" />
-
-    <com.google.android.material.loadingindicator.LoadingIndicator
-        android:id="@+id/iv_progressBar"
-        android:layout_width="24dp"
-        android:layout_height="24dp"
-        android:layout_marginStart="8dp"
-        app:indicatorSize="24dp"
-        android:scrollbars="none"
-        android:visibility="gone"
-        app:layout_constraintBottom_toBottomOf="@id/iv_debug_text"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="@id/iv_debug_text"
-        tools:visibility="visible" />
-
-</androidx.constraintlayout.widget.ConstraintLayout>

+ 0 - 142
app/src/main/res/menu/book_source.xml

@@ -1,142 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools">
-
-    <item
-        android:id="@+id/action_sort"
-        android:title="@string/sort"
-        android:icon="@drawable/ic_sort"
-        app:showAsAction="always"
-        tools:ignore="AlwaysShowAction">
-
-        <menu>
-
-            <item
-                android:id="@+id/menu_sort_desc"
-                android:checkable="true"
-                android:title="@string/sort_desc" />
-
-            <group
-                android:id="@+id/menu_group_sort"
-                android:checkableBehavior="single">
-
-                <item
-                    android:id="@+id/menu_sort_manual"
-                    android:checkable="true"
-                    android:checked="true"
-                    android:title="@string/sort_manual" />
-
-                <item
-                    android:id="@+id/menu_sort_auto"
-                    android:checkable="true"
-                    android:title="@string/sort_auto" />
-
-                <item
-                    android:id="@+id/menu_sort_name"
-                    android:checkable="true"
-                    android:title="@string/sort_by_name" />
-
-                <item
-                    android:id="@+id/menu_sort_url"
-                    android:checkable="true"
-                    android:title="@string/sort_by_url" />
-
-                <item
-                    android:id="@+id/menu_sort_time"
-                    android:checkable="true"
-                    android:title="@string/sort_by_lastUpdateTime" />
-
-                <item
-                    android:id="@+id/menu_sort_respondTime"
-                    android:checkable="true"
-                    android:title="@string/sort_by_respondTime" />
-
-                <item
-                    android:id="@+id/menu_sort_enable"
-                    android:checkable="true"
-                    android:title="@string/is_enabled" />
-
-            </group>
-
-        </menu>
-
-    </item>
-
-    <item
-        android:id="@+id/menu_group"
-        android:icon="@drawable/ic_groups"
-        android:title="@string/menu_action_group"
-        app:showAsAction="always"
-        tools:ignore="AlwaysShowAction">
-
-        <menu>
-
-            <item
-                android:id="@+id/menu_group_manage"
-                android:title="@string/group_manage" />
-
-            <item
-                android:id="@+id/menu_enabled_group"
-                android:title="@string/enabled" />
-
-            <item
-                android:id="@+id/menu_disabled_group"
-                android:title="@string/disabled" />
-
-            <item
-                android:id="@+id/menu_group_login"
-                android:title="@string/need_login" />
-
-            <item
-                android:id="@+id/menu_group_null"
-                android:title="@string/no_group" />
-
-            <item
-                android:id="@+id/menu_enabled_explore_group"
-                android:title="@string/enabled_explore" />
-
-            <item
-                android:id="@+id/menu_disabled_explore_group"
-                android:title="@string/disabled_explore" />
-
-            <group android:id="@+id/source_group">
-
-            </group>
-
-        </menu>
-
-    </item>
-
-    <item
-        android:id="@+id/menu_add_book_source"
-        android:title="@string/add_book_source"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/menu_import_local"
-        android:title="@string/import_local"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/menu_import_onLine"
-        android:title="@string/import_on_line"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/menu_import_qr"
-        android:title="@string/import_by_qr_code"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/menu_group_sources_by_domain"
-        android:checkable="true"
-        android:title="@string/group_sources_by_domain"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/menu_help"
-        android:title="@string/help"
-        app:showAsAction="never" />
-
-</menu>

+ 0 - 32
app/src/main/res/menu/book_source_item.xml

@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <item
-        android:id="@+id/menu_top"
-        android:title="@string/to_top" />
-
-    <item
-        android:id="@+id/menu_bottom"
-        android:title="@string/to_bottom" />
-
-    <item
-        android:id="@+id/menu_login"
-        android:title="@string/login" />
-
-    <item
-        android:id="@+id/menu_search"
-        android:title="@string/search" />
-
-    <item
-        android:id="@+id/menu_debug_source"
-        android:title="@string/debug" />
-
-    <item
-        android:id="@+id/menu_del"
-        android:title="@string/delete" />
-
-    <item
-        android:id="@+id/menu_enable_explore"
-        android:title="@string/enable_explore" />
-
-</menu>