Ver código fonte

refactor: 消除编译弃用警告(421→72)

- 九个弃用 *Config 门面(AppConfig/ReadConfig/ThemeConfig/OtherConfig/
  BackupConfig/ChangeSourceConfig/BookshelfConfig/CoverConfig/ReadMangaConfig)
  的全部调用点迁移至对应 SettingsGateway.currentSettings:
  ViewModel 构造注入,其余按既有约定 GlobalContext 查找
- DatabaseMigrations migrate 参数名统一为 db(36 处)
- 弃用图标替换为 AutoMirrored 变体(16 处)
- Compose M3 弃用:ListItem 尾随 lambda 重载、TextFieldLabelPosition.Inside、
  ExposedDropdownMenuAnchorType、LocalClipboard(suspend 版)
- Kotlin 清理:多余安全调用/非空断言、恒真条件、PersistentMap.putting、
  OVERRIDE_DEPRECATION 补注
- Java 平台类型不匹配真修(AbsCallBack/ObsoleteUrlFactory/CronetLoader/RequestManager)

剩余 72 条警告均为约定排除项:androidx.media Compat(61,待 media3 迁移)
与平台 API 弃用(11,待 WindowInsetsController 等改造)。

验证::app:compileAppDebugKotlin 全量重编译 0 错误;
testAppDebugUnitTest 927 例仅存量 3 例失败(stash 基线确认与本次无关);
verifyConfigArchitecture 通过;git diff --check 通过。
HapeLee 2 meses atrás
pai
commit
78079d9ac9
100 arquivos alterados com 824 adições e 572 exclusões
  1. 15 5
      app/src/main/java/io/legado/app/App.kt
  2. 5 2
      app/src/main/java/io/legado/app/api/controller/BookController.kt
  3. 5 2
      app/src/main/java/io/legado/app/base/AppContextWrapper.kt
  4. 13 5
      app/src/main/java/io/legado/app/base/BaseActivity.kt
  5. 6 3
      app/src/main/java/io/legado/app/base/BaseDialogFragment.kt
  6. 5 2
      app/src/main/java/io/legado/app/base/BasePrefDialogFragment.kt
  7. 5 2
      app/src/main/java/io/legado/app/constant/AppLog.kt
  8. 141 141
      app/src/main/java/io/legado/app/data/DatabaseMigrations.kt
  9. 2 1
      app/src/main/java/io/legado/app/di/appModule.kt
  10. 1 0
      app/src/main/java/io/legado/app/domain/model/AiMessageParts.kt
  11. 6 3
      app/src/main/java/io/legado/app/help/AppFreezeMonitor.kt
  12. 14 12
      app/src/main/java/io/legado/app/help/AppWebDav.kt
  13. 10 3
      app/src/main/java/io/legado/app/help/CrashHandler.kt
  14. 5 2
      app/src/main/java/io/legado/app/help/DispatchersMonitor.kt
  15. 14 9
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  16. 12 9
      app/src/main/java/io/legado/app/help/WebDavManager.kt
  17. 16 9
      app/src/main/java/io/legado/app/help/book/BookExtensions.kt
  18. 11 7
      app/src/main/java/io/legado/app/help/book/BookHelp.kt
  19. 13 8
      app/src/main/java/io/legado/app/help/book/ContentProcessor.kt
  20. 1 2
      app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt
  21. 22 5
      app/src/main/java/io/legado/app/help/config/ReadStyleResolver.kt
  22. 41 18
      app/src/main/java/io/legado/app/help/config/ThemeConfigStore.kt
  23. 3 2
      app/src/main/java/io/legado/app/help/glide/LegadoGlideModule.kt
  24. 3 2
      app/src/main/java/io/legado/app/help/http/BackstageWebView.kt
  25. 4 3
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  26. 2 2
      app/src/main/java/io/legado/app/help/http/ObsoleteUrlFactory.kt
  27. 2 0
      app/src/main/java/io/legado/app/help/readaloud/playback/SystemTtsFileSynthesizer.kt
  28. 1 1
      app/src/main/java/io/legado/app/help/readaloud/playback/SystemTtsVoiceCatalog.kt
  29. 8 5
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  30. 1 2
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  31. 3 2
      app/src/main/java/io/legado/app/help/update/AppUpdateGitHub.kt
  32. 3 3
      app/src/main/java/io/legado/app/lib/cronet/AbsCallBack.kt
  33. 1 1
      app/src/main/java/io/legado/app/lib/cronet/CronetLoader.kt
  34. 1 2
      app/src/main/java/io/legado/app/lib/permission/RequestManager.kt
  35. 3 2
      app/src/main/java/io/legado/app/lib/prefs/EditTextPreferenceDialog.kt
  36. 3 2
      app/src/main/java/io/legado/app/lib/prefs/ListPreferenceDialog.kt
  37. 3 2
      app/src/main/java/io/legado/app/lib/prefs/MultiSelectListPreferenceDialog.kt
  38. 2 3
      app/src/main/java/io/legado/app/lib/prefs/ThemeCardPreference.kt
  39. 8 3
      app/src/main/java/io/legado/app/model/AudioPlay.kt
  40. 20 9
      app/src/main/java/io/legado/app/model/BookCover.kt
  41. 5 2
      app/src/main/java/io/legado/app/model/CacheBook.kt
  42. 8 3
      app/src/main/java/io/legado/app/model/ImageProvider.kt
  43. 3 3
      app/src/main/java/io/legado/app/model/ReadAloud.kt
  44. 21 17
      app/src/main/java/io/legado/app/model/ReadBook.kt
  45. 4 2
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  46. 6 4
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  47. 17 11
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  48. 6 3
      app/src/main/java/io/legado/app/model/localBook/TextFile.kt
  49. 5 2
      app/src/main/java/io/legado/app/model/remote/RemoteBookWebDav.kt
  50. 13 7
      app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt
  51. 7 4
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  52. 6 3
      app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt
  53. 10 7
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  54. 4 2
      app/src/main/java/io/legado/app/service/CacheBookService.kt
  55. 9 8
      app/src/main/java/io/legado/app/ui/about/AboutViewModel.kt
  56. 2 3
      app/src/main/java/io/legado/app/ui/about/CrashLogsDialog.kt
  57. 3 2
      app/src/main/java/io/legado/app/ui/about/UpdateDialog.kt
  58. 9 4
      app/src/main/java/io/legado/app/ui/ai/chat/AiChatScreen.kt
  59. 2 2
      app/src/main/java/io/legado/app/ui/ai/chat/AiChatViewModel.kt
  60. 3 4
      app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt
  61. 5 4
      app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt
  62. 7 8
      app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt
  63. 3 2
      app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt
  64. 4 2
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceAdapter.kt
  65. 13 10
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt
  66. 15 12
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt
  67. 3 4
      app/src/main/java/io/legado/app/ui/book/info/BookInfoBackdropStyle.kt
  68. 1 2
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  69. 10 6
      app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageScreenViewModel.kt
  70. 1 1
      app/src/main/java/io/legado/app/ui/book/manga/MangaReaderViewModel.kt
  71. 12 10
      app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt
  72. 2 2
      app/src/main/java/io/legado/app/ui/book/read/ReadBookFloatingActionBar.kt
  73. 3 3
      app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBarFloating.kt
  74. 3 3
      app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBarTitle.kt
  75. 2 2
      app/src/main/java/io/legado/app/ui/book/read/sheet/CharsetConfigSheet.kt
  76. 3 3
      app/src/main/java/io/legado/app/ui/book/read/sheet/ReaderMoreActionsSheet.kt
  77. 14 14
      app/src/main/java/io/legado/app/ui/book/read/sheet/TypographyTabs.kt
  78. 2 3
      app/src/main/java/io/legado/app/ui/book/readaloud/player/ReadAloudPlayerCoordinator.kt
  79. 3 2
      app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt
  80. 3 3
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentAdapter.kt
  81. 7 8
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt
  82. 1 1
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceScreen.kt
  83. 3 3
      app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt
  84. 1 0
      app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt
  85. 1 1
      app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleViewModel.kt
  86. 6 2
      app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfManageScreenConfig.kt
  87. 13 8
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
  88. 8 9
      app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt
  89. 1 1
      app/src/main/java/io/legado/app/ui/main/homepage/modules/ButtonGroupModule.kt
  90. 7 3
      app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt
  91. 3 3
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt
  92. 2 2
      app/src/main/java/io/legado/app/ui/rss/read/RssReadRouteScreen.kt
  93. 2 2
      app/src/main/java/io/legado/app/ui/widget/BatteryViewOrgin.kt
  94. 3 3
      app/src/main/java/io/legado/app/ui/widget/components/AppTextField.kt
  95. 8 9
      app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt
  96. 14 3
      app/src/main/java/io/legado/app/ui/widget/dialog/BottomWebViewDialog.kt
  97. 27 14
      app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt
  98. 7 3
      app/src/main/java/io/legado/app/utils/AnimationExtensions.kt
  99. 17 10
      app/src/main/java/io/legado/app/utils/ContextExtensions.kt
  100. 7 2
      app/src/main/java/io/legado/app/utils/CustomExportUtils.kt

+ 15 - 5
app/src/main/java/io/legado/app/App.kt

@@ -39,7 +39,10 @@ import io.legado.app.di.appModule
 import io.legado.app.domain.gateway.AppLocaleGateway
 import io.legado.app.domain.gateway.AppShellSettingsGateway
 import io.legado.app.domain.gateway.BackupSettingsGateway
+import io.legado.app.domain.gateway.OtherSettingsGateway
+import io.legado.app.domain.gateway.ReadSettingsGateway
 import io.legado.app.domain.gateway.ReadStyleGateway
+import io.legado.app.domain.gateway.ThemeSettingsGateway
 import io.legado.app.help.AppFreezeMonitor
 import io.legado.app.help.AppWebDav
 import io.legado.app.help.CrashHandler
@@ -48,8 +51,8 @@ import io.legado.app.help.DispatchersMonitor
 import io.legado.app.help.LifecycleHelp
 import io.legado.app.help.RuleBigDataHelp
 import io.legado.app.help.book.BookHelp
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfigStore
+import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.config.ThemeConfigStore
@@ -87,6 +90,11 @@ import java.util.logging.Level
 
 class App : Application(), SingletonImageLoader.Factory {
 
+    private val themeGateway get() = get<ThemeSettingsGateway>()
+    private val otherGateway get() = get<OtherSettingsGateway>()
+    private val backupGateway get() = get<BackupSettingsGateway>()
+    private val readGateway get() = get<ReadSettingsGateway>()
+
     override fun newImageLoader(context: Context): ImageLoader {
         return get()
     }
@@ -107,6 +115,7 @@ class App : Application(), SingletonImageLoader.Factory {
             androidContext(this@App)
             modules(appDatabaseModule, appModule)
         }
+        @Suppress("DEPRECATION")
         AppConfig.initialize(
             shellGateway = get(),
             themeGateway = get(),
@@ -129,7 +138,7 @@ class App : Application(), SingletonImageLoader.Factory {
         }
         applyDayNightInit(this)
         if (getPrefString("app_theme", "0") == "12") {
-            if (AppConfig.customMode == "accent")
+            if (themeGateway.currentSettings.customMode == "accent")
                 setTheme(R.style.ThemeOverlay_WhiteBackground)
 
             val colorImagePath = getPrefString(PreferKey.colorImage)
@@ -196,7 +205,7 @@ class App : Application(), SingletonImageLoader.Factory {
             LiveEventBus.config()
                 .lifecycleObserverAlwaysActive(true)
                 .autoClear(false)
-                .enableLogger(BuildConfig.DEBUG || AppConfig.recordLog)
+                .enableLogger(BuildConfig.DEBUG || otherGateway.currentSettings.recordLog)
                 .setLogger(EventLogger())
             DefaultData.upVersion()
             AppFreezeMonitor.init(this@App)
@@ -218,7 +227,7 @@ class App : Application(), SingletonImageLoader.Factory {
             get<ReadStyleGateway>().clearUnusedBackgrounds()
             ThemeConfigStore.clearBg()
             //初始化简繁转换引擎
-            when (AppConfig.chineseConverterType) {
+            when (readGateway.currentSettings.chineseConverterType) {
                 1 -> {
                     ChineseUtils.fixT2sDict()
                     ChineseUtils.preLoad(true, TransType.TRADITIONAL_TO_SIMPLE)
@@ -229,7 +238,7 @@ class App : Application(), SingletonImageLoader.Factory {
             //调整排序序号
             SourceHelp.adjustSortNumber()
             //同步阅读记录
-            if (AppConfig.syncBookProgress) {
+            if (backupGateway.currentSettings.syncBookProgress) {
                 AppWebDav.upConfig()
                 AppWebDav.downloadAllBookProgress()
             }
@@ -329,6 +338,7 @@ class App : Application(), SingletonImageLoader.Factory {
     }
 
     private fun initRhino() {
+        @Suppress("UNUSED_EXPRESSION")
         RhinoScriptEngine
         RhinoWrapFactory.register(BookSource::class.java, NativeBaseSource.factory)
         RhinoWrapFactory.register(RssSource::class.java, NativeBaseSource.factory)

+ 5 - 2
app/src/main/java/io/legado/app/api/controller/BookController.kt

@@ -14,7 +14,7 @@ import io.legado.app.help.CacheManager
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.ContentProcessor
 import io.legado.app.help.book.isLocal
-import io.legado.app.help.config.AppConfig
+import io.legado.app.domain.gateway.BookshelfSettingsGateway
 import io.legado.app.help.glide.ImageLoader
 import io.legado.app.model.BookCover
 import io.legado.app.model.ImageProvider
@@ -29,12 +29,15 @@ import io.legado.app.utils.stackTraceStr
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.runBlocking
 import splitties.init.appCtx
+import org.koin.core.context.GlobalContext
 import java.io.File
 import java.util.WeakHashMap
 import java.util.concurrent.TimeUnit
 
 object BookController {
 
+    private val bookshelfGateway by lazy { GlobalContext.get().get<BookshelfSettingsGateway>() }
+
     private lateinit var book: Book
     private var bookSource: BookSource? = null
     private var bookUrl: String = ""
@@ -50,7 +53,7 @@ object BookController {
             return if (books.isEmpty()) {
                 returnData.setErrorMsg("还没有添加小说")
             } else {
-                val data = when (AppConfig.bookshelfSort) {
+                val data = when (bookshelfGateway.currentSettings.bookshelfSort) {
                     1 -> books.sortedByDescending { it.latestChapterTime }
                     2 -> books.sortedWith { o1, o2 ->
                         o1.name.cnCompare(o2.name)

+ 5 - 2
app/src/main/java/io/legado/app/base/AppContextWrapper.kt

@@ -3,13 +3,16 @@ package io.legado.app.base
 import android.app.Activity
 import android.content.Context
 import android.content.res.Configuration
-import io.legado.app.ui.config.themeConfig.ThemeConfig
+import io.legado.app.domain.gateway.AppShellSettingsGateway
 import io.legado.app.ui.theme.resolveAppFontScale
+import org.koin.core.context.GlobalContext
 
 
 @Suppress("unused")
 object AppContextWrapper {
 
+    private val shellGateway get() = GlobalContext.get().get<AppShellSettingsGateway>()
+
     fun applyFont(activity: Activity) {
         val config = activity.resources.configuration
         val fontScale = getFontScale(activity)
@@ -22,6 +25,6 @@ object AppContextWrapper {
     }
 
     fun getFontScale(context: Context): Float =
-        resolveAppFontScale(ThemeConfig.fontScale)
+        resolveAppFontScale(shellGateway.currentSettings.fontScale)
 
 }

+ 13 - 5
app/src/main/java/io/legado/app/base/BaseActivity.kt

@@ -30,10 +30,11 @@ import io.legado.app.BuildConfig
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.PreferKey
 import io.legado.app.constant.Theme
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ThemeConfigStore
 import io.legado.app.domain.gateway.AppLocaleGateway
+import io.legado.app.domain.gateway.AppShellSettingsGateway
 import io.legado.app.domain.gateway.AppUiConfigurationGateway
+import io.legado.app.domain.gateway.ThemeSettingsGateway
 import io.legado.app.domain.model.settings.AppUiConfiguration
 import io.legado.app.domain.model.settings.AppUiConfigurationDiff
 import io.legado.app.domain.model.settings.diffFrom
@@ -47,6 +48,7 @@ import io.legado.app.utils.getPrefString
 import io.legado.app.utils.hideSoftInput
 import io.legado.app.utils.LogUtils
 import io.legado.app.utils.setStatusBarColorAuto
+import io.legado.app.utils.sysConfiguration
 import io.legado.app.utils.themeColor
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.windowSize
@@ -67,6 +69,8 @@ abstract class BaseActivity<VB : ViewBinding>(
 
     private val appLocaleGateway by inject<AppLocaleGateway>()
     private val appUiConfigurationGateway by inject<AppUiConfigurationGateway>()
+    private val shellGateway by inject<AppShellSettingsGateway>()
+    private val themeGateway by inject<ThemeSettingsGateway>()
     private var lastUiConfiguration: AppUiConfiguration? = null
     private var lastPlatformConfiguration: Configuration? = null
     private var pendingControlledRecreate = false
@@ -104,7 +108,7 @@ abstract class BaseActivity<VB : ViewBinding>(
         window.decorView.disableAutoFill()
         AppContextWrapper.applyFont(this)
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-            val enable = !AppConfig.isPredictiveBackEnabled
+            val enable = !shellGateway.currentSettings.predictiveBackEnabled
             if (enable) {
                 onBackInvokedDispatcher.registerOnBackInvokedCallback(
                     OnBackInvokedDispatcher.PRIORITY_DEFAULT
@@ -155,7 +159,11 @@ abstract class BaseActivity<VB : ViewBinding>(
             true,
             fullScreen
         )
-        val isDarkTheme = AppConfig.isNightTheme
+        val isDarkTheme = when (shellGateway.currentSettings.themeMode) {
+            "1" -> false
+            "2" -> true
+            else -> sysConfiguration.isNightMode
+        }
         WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = !isDarkTheme
     }
 
@@ -276,14 +284,14 @@ abstract class BaseActivity<VB : ViewBinding>(
                 }
 
                 // 必须在动态取色之后应用,否则会被动态配色的 surface/background 覆盖
-                if (AppConfig.customMode == "accent")
+                if (themeGateway.currentSettings.customMode == "accent")
                     setTheme(R.style.ThemeOverlay_WhiteBackground)
             }
 
             "13" -> setTheme(R.style.AppTheme_Transparent)
         }
 
-        if (AppConfig.pureBlack)
+        if (themeGateway.currentSettings.isPureBlack)
             setTheme(R.style.ThemeOverlay_PureBlack)
     }
 

+ 6 - 3
app/src/main/java/io/legado/app/base/BaseDialogFragment.kt

@@ -12,11 +12,12 @@ import androidx.lifecycle.lifecycleScope
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import io.legado.app.R
 import io.legado.app.constant.AppLog
-import io.legado.app.help.config.AppConfig
+import io.legado.app.domain.gateway.ThemeSettingsGateway
 import io.legado.app.help.coroutine.Coroutine
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlin.coroutines.CoroutineContext
+import org.koin.core.context.GlobalContext
 
 
 abstract class BaseDialogFragment(
@@ -24,6 +25,8 @@ abstract class BaseDialogFragment(
     private val adaptationSoftKeyboard: Boolean = false
 ) : DialogFragment(layoutID) {
 
+    private val themeGateway get() = GlobalContext.get().get<ThemeSettingsGateway>()
+
     private var onDismissListener: OnDismissListener? = null
 
     private lateinit var dialogView: View
@@ -45,7 +48,7 @@ abstract class BaseDialogFragment(
         super.onStart()
 //        if (adaptationSoftKeyboard) {
 //            dialog?.window?.setBackgroundDrawableResource(R.color.transparent)
-//        } else if (AppConfig.isEInkMode) {
+//        } else if (themeGateway.currentSettings.appTheme == "4") {
 //            dialog?.window?.let {
 //                it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
 //                val attr = it.attributes
@@ -80,7 +83,7 @@ abstract class BaseDialogFragment(
         if (adaptationSoftKeyboard) {
             view.findViewById<View>(R.id.vw_bg)?.setOnClickListener(null)
             view.setOnClickListener { dismiss() }
-        } else if (!AppConfig.isEInkMode) {
+        } else if (themeGateway.currentSettings.appTheme != "4") {
             //view.setBackgroundColor(ThemeStore.backgroundColor())
         }
         onFragmentCreated(view, savedInstanceState)

+ 5 - 2
app/src/main/java/io/legado/app/base/BasePrefDialogFragment.kt

@@ -2,15 +2,18 @@ package io.legado.app.base
 
 import androidx.lifecycle.LifecycleEventObserver
 import com.google.android.material.bottomsheet.BottomSheetDialogFragment
-import io.legado.app.help.config.AppConfig
+import io.legado.app.domain.gateway.ThemeSettingsGateway
+import org.koin.core.context.GlobalContext
 
 
 abstract class BasePrefDialogFragment(
 ) : BottomSheetDialogFragment() {
 
+    private val themeGateway get() = GlobalContext.get().get<ThemeSettingsGateway>()
+
     override fun onStart() {
         super.onStart()
-        if (AppConfig.isEInkMode) {
+        if (themeGateway.currentSettings.appTheme == "4") {
 //            dialog?.window?.let {
 //                it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
 //                val attr = it.attributes

+ 5 - 2
app/src/main/java/io/legado/app/constant/AppLog.kt

@@ -2,13 +2,16 @@ package io.legado.app.constant
 
 import android.util.Log
 import io.legado.app.BuildConfig
-import io.legado.app.help.config.AppConfig
+import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.utils.LogUtils
 import io.legado.app.utils.toastOnUi
+import org.koin.core.context.GlobalContext
 import splitties.init.appCtx
 
 object AppLog {
 
+    private val otherGateway by lazy { GlobalContext.get().get<OtherSettingsGateway>() }
+
     private val mLogs = arrayListOf<Triple<Long, String, Throwable?>>()
 
     val logs get() = mLogs.toList()
@@ -56,7 +59,7 @@ object AppLog {
     }
 
     fun putDebug(message: String?, throwable: Throwable? = null) {
-        if (AppConfig.recordLog) {
+        if (otherGateway.currentSettings.recordLog) {
             put(message, throwable)
         }
     }

+ 141 - 141
app/src/main/java/io/legado/app/data/DatabaseMigrations.kt

@@ -26,9 +26,9 @@ object DatabaseMigrations {
     }
 
     private val migration_10_11 = object : Migration(10, 11) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("DROP TABLE txtTocRules")
-            database.execSQL(
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("DROP TABLE txtTocRules")
+            db.execSQL(
                 """CREATE TABLE txtTocRules(id INTEGER NOT NULL, 
                     name TEXT NOT NULL, rule TEXT NOT NULL, serialNumber INTEGER NOT NULL, 
                     enable INTEGER NOT NULL, PRIMARY KEY (id))"""
@@ -37,20 +37,20 @@ object DatabaseMigrations {
     }
 
     private val migration_11_12 = object : Migration(11, 12) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE rssSources ADD style TEXT ")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE rssSources ADD style TEXT ")
         }
     }
 
     private val migration_12_13 = object : Migration(12, 13) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE rssSources ADD articleStyle INTEGER NOT NULL DEFAULT 0 ")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE rssSources ADD articleStyle INTEGER NOT NULL DEFAULT 0 ")
         }
     }
 
     private val migration_13_14 = object : Migration(13, 14) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL(
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL(
                 """CREATE TABLE IF NOT EXISTS `books_new` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL,
                     `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, 
                     `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, 
@@ -59,57 +59,57 @@ object DatabaseMigrations {
                     `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL, 
                     `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))"""
             )
-            database.execSQL("INSERT INTO books_new select * from books ")
-            database.execSQL("DROP TABLE books")
-            database.execSQL("ALTER TABLE books_new RENAME TO books")
-            database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `books` (`name`, `author`) ")
+            db.execSQL("INSERT INTO books_new select * from books ")
+            db.execSQL("DROP TABLE books")
+            db.execSQL("ALTER TABLE books_new RENAME TO books")
+            db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `books` (`name`, `author`) ")
         }
     }
 
     private val migration_14_15 = object : Migration(14, 15) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE bookmarks ADD bookAuthor TEXT NOT NULL DEFAULT ''")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE bookmarks ADD bookAuthor TEXT NOT NULL DEFAULT ''")
         }
     }
 
     private val migration_15_17 = object : Migration(15, 17) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("CREATE TABLE IF NOT EXISTS `readRecord` (`bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`bookName`))")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("CREATE TABLE IF NOT EXISTS `readRecord` (`bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`bookName`))")
         }
     }
 
     private val migration_17_18 = object : Migration(17, 18) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("CREATE TABLE IF NOT EXISTS `httpTTS` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("CREATE TABLE IF NOT EXISTS `httpTTS` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))")
         }
     }
 
     private val migration_18_19 = object : Migration(18, 19) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL(
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL(
                 """CREATE TABLE IF NOT EXISTS `readRecordNew` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, 
                     PRIMARY KEY(`androidId`, `bookName`))"""
             )
-            database.execSQL("INSERT INTO readRecordNew(androidId, bookName, readTime) select '${AppConst.androidId}' as androidId, bookName, readTime from readRecord")
-            database.execSQL("DROP TABLE readRecord")
-            database.execSQL("ALTER TABLE readRecordNew RENAME TO readRecord")
+            db.execSQL("INSERT INTO readRecordNew(androidId, bookName, readTime) select '${AppConst.androidId}' as androidId, bookName, readTime from readRecord")
+            db.execSQL("DROP TABLE readRecord")
+            db.execSQL("ALTER TABLE readRecordNew RENAME TO readRecord")
         }
     }
     private val migration_19_20 = object : Migration(19, 20) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE book_sources ADD bookSourceComment TEXT")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE book_sources ADD bookSourceComment TEXT")
         }
     }
 
     private val migration_20_21 = object : Migration(20, 21) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE book_groups ADD show INTEGER NOT NULL DEFAULT 1")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE book_groups ADD show INTEGER NOT NULL DEFAULT 1")
         }
     }
 
     private val migration_21_22 = object : Migration(21, 22) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL(
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL(
                 """CREATE TABLE IF NOT EXISTS `books_new` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, 
                     `originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, 
                     `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, 
@@ -118,35 +118,35 @@ object DatabaseMigrations {
                     `durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, 
                     `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))"""
             )
-            database.execSQL(
+            db.execSQL(
                 """INSERT INTO books_new select `bookUrl`, `tocUrl`, `origin`, `originName`, `name`, `author`, `kind`, `customTag`, `coverUrl`, 
                     `customCoverUrl`, `intro`, `customIntro`, `charset`, `type`, `group`, `latestChapterTitle`, `latestChapterTime`, `lastCheckTime`, 
                     `lastCheckCount`, `totalChapterNum`, `durChapterTitle`, `durChapterIndex`, `durChapterPos`, `durChapterTime`, `wordCount`, `canUpdate`, 
                     `order`, `originOrder`, `variable`, null
                     from books"""
             )
-            database.execSQL("DROP TABLE books")
-            database.execSQL("ALTER TABLE books_new RENAME TO books")
-            database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `books` (`name`, `author`) ")
+            db.execSQL("DROP TABLE books")
+            db.execSQL("ALTER TABLE books_new RENAME TO books")
+            db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `books` (`name`, `author`) ")
         }
     }
 
     private val migration_22_23 = object : Migration(22, 23) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE chapters ADD baseUrl TEXT NOT NULL DEFAULT ''")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE chapters ADD baseUrl TEXT NOT NULL DEFAULT ''")
         }
     }
 
     private val migration_23_24 = object : Migration(23, 24) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("CREATE TABLE IF NOT EXISTS `caches` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))")
-            database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `caches` (`key`)")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("CREATE TABLE IF NOT EXISTS `caches` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))")
+            db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `caches` (`key`)")
         }
     }
 
     private val migration_24_25 = object : Migration(24, 25) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL(
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL(
                 """CREATE TABLE IF NOT EXISTS `sourceSubs` 
                     (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, 
                     PRIMARY KEY(`id`))"""
@@ -155,86 +155,86 @@ object DatabaseMigrations {
     }
 
     private val migration_25_26 = object : Migration(25, 26) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL(
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL(
                 """CREATE TABLE IF NOT EXISTS `ruleSubs` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, 
                     `customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))"""
             )
-            database.execSQL(" insert into `ruleSubs` select *, 0, 0 from `sourceSubs` ")
-            database.execSQL("DROP TABLE `sourceSubs`")
+            db.execSQL(" insert into `ruleSubs` select *, 0, 0 from `sourceSubs` ")
+            db.execSQL("DROP TABLE `sourceSubs`")
         }
     }
 
     private val migration_26_27 = object : Migration(26, 27) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL(" ALTER TABLE rssSources ADD singleUrl INTEGER NOT NULL DEFAULT 0 ")
-            database.execSQL(
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL(" ALTER TABLE rssSources ADD singleUrl INTEGER NOT NULL DEFAULT 0 ")
+            db.execSQL(
                 """CREATE TABLE IF NOT EXISTS `bookmarks1` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL, 
                         `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, 
                         `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))"""
             )
-            database.execSQL(
+            db.execSQL(
                 """insert into `bookmarks1` 
                         select `time`, `bookUrl`, `bookName`, `bookAuthor`, `chapterIndex`, `pageIndex`, `chapterName`, '', `content` 
                         from bookmarks"""
             )
-            database.execSQL(" DROP TABLE `bookmarks` ")
-            database.execSQL(" ALTER TABLE bookmarks1 RENAME TO bookmarks ")
-            database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `bookmarks` (`time`)")
+            db.execSQL(" DROP TABLE `bookmarks` ")
+            db.execSQL(" ALTER TABLE bookmarks1 RENAME TO bookmarks ")
+            db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `bookmarks` (`time`)")
         }
     }
 
     private val migration_27_28 = object : Migration(27, 28) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE rssArticles ADD variable TEXT")
-            database.execSQL("ALTER TABLE rssStars ADD variable TEXT")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE rssArticles ADD variable TEXT")
+            db.execSQL("ALTER TABLE rssStars ADD variable TEXT")
         }
     }
 
     private val migration_28_29 = object : Migration(28, 29) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE rssSources ADD sourceComment TEXT")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE rssSources ADD sourceComment TEXT")
         }
     }
 
     private val migration_29_30 = object : Migration(29, 30) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE chapters ADD `startFragmentId` TEXT")
-            database.execSQL("ALTER TABLE chapters ADD `endFragmentId` TEXT")
-            database.execSQL(
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE chapters ADD `startFragmentId` TEXT")
+            db.execSQL("ALTER TABLE chapters ADD `endFragmentId` TEXT")
+            db.execSQL(
                 """
                     CREATE TABLE IF NOT EXISTS `epubChapters` 
                     (`bookUrl` TEXT NOT NULL, `href` TEXT NOT NULL, `parentHref` TEXT, 
                     PRIMARY KEY(`bookUrl`, `href`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )
                 """
             )
-            database.execSQL("CREATE INDEX IF NOT EXISTS `index_epubChapters_bookUrl` ON `epubChapters` (`bookUrl`)")
-            database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_epubChapters_bookUrl_href` ON `epubChapters` (`bookUrl`, `href`)")
+            db.execSQL("CREATE INDEX IF NOT EXISTS `index_epubChapters_bookUrl` ON `epubChapters` (`bookUrl`)")
+            db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_epubChapters_bookUrl_href` ON `epubChapters` (`bookUrl`, `href`)")
         }
     }
 
     private val migration_30_31 = object : Migration(30, 31) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE readRecord RENAME TO readRecord1")
-            database.execSQL(
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE readRecord RENAME TO readRecord1")
+            db.execSQL(
                 """
                     CREATE TABLE IF NOT EXISTS `readRecord` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))
                 """
             )
-            database.execSQL("insert into readRecord (deviceId, bookName, readTime) select androidId, bookName, readTime from readRecord1")
+            db.execSQL("insert into readRecord (deviceId, bookName, readTime) select androidId, bookName, readTime from readRecord1")
         }
     }
 
     private val migration_31_32 = object : Migration(31, 32) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("DROP TABLE `epubChapters`")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("DROP TABLE `epubChapters`")
         }
     }
 
     private val migration_32_33 = object : Migration(32, 33) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE bookmarks RENAME TO bookmarks_old")
-            database.execSQL(
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE bookmarks RENAME TO bookmarks_old")
+            db.execSQL(
                 """
                     CREATE TABLE IF NOT EXISTS `bookmarks` (`time` INTEGER NOT NULL,
                     `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, 
@@ -242,12 +242,12 @@ object DatabaseMigrations {
                     `content` TEXT NOT NULL, PRIMARY KEY(`time`))
                 """
             )
-            database.execSQL(
+            db.execSQL(
                 """
                     CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `bookmarks` (`bookName`, `bookAuthor`)
                 """
             )
-            database.execSQL(
+            db.execSQL(
                 """
                     insert into bookmarks (time, bookName, bookAuthor, chapterIndex, chapterPos, chapterName, bookText, content)
                     select time, ifNull(b.name, bookName) bookName, ifNull(b.author, bookAuthor) bookAuthor, 
@@ -259,77 +259,77 @@ object DatabaseMigrations {
     }
 
     private val migration_33_34 = object : Migration(33, 34) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE `book_groups` ADD `cover` TEXT")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE `book_groups` ADD `cover` TEXT")
         }
     }
 
     private val migration_34_35 = object : Migration(34, 35) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE `book_sources` ADD `concurrentRate` TEXT")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE `book_sources` ADD `concurrentRate` TEXT")
         }
     }
 
     private val migration_35_36 = object : Migration(35, 36) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE `book_sources` ADD `loginUi` TEXT")
-            database.execSQL("ALTER TABLE `book_sources` ADD`loginCheckJs` TEXT")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE `book_sources` ADD `loginUi` TEXT")
+            db.execSQL("ALTER TABLE `book_sources` ADD`loginCheckJs` TEXT")
         }
     }
 
     private val migration_36_37 = object : Migration(36, 37) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE `rssSources` ADD `loginUrl` TEXT")
-            database.execSQL("ALTER TABLE `rssSources` ADD `loginUi` TEXT")
-            database.execSQL("ALTER TABLE `rssSources` ADD `loginCheckJs` TEXT")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE `rssSources` ADD `loginUrl` TEXT")
+            db.execSQL("ALTER TABLE `rssSources` ADD `loginUi` TEXT")
+            db.execSQL("ALTER TABLE `rssSources` ADD `loginCheckJs` TEXT")
         }
     }
 
     private val migration_37_38 = object : Migration(37, 38) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE `book_sources` ADD `respondTime` INTEGER NOT NULL DEFAULT 180000")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE `book_sources` ADD `respondTime` INTEGER NOT NULL DEFAULT 180000")
         }
     }
 
     private val migration_38_39 = object : Migration(38, 39) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE `rssSources` ADD `concurrentRate` TEXT")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE `rssSources` ADD `concurrentRate` TEXT")
         }
     }
 
     private val migration_39_40 = object : Migration(39, 40) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE `chapters` ADD `isVip` INTEGER NOT NULL DEFAULT 0")
-            database.execSQL("ALTER TABLE `chapters` ADD `isPay` INTEGER NOT NULL DEFAULT 0")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE `chapters` ADD `isVip` INTEGER NOT NULL DEFAULT 0")
+            db.execSQL("ALTER TABLE `chapters` ADD `isPay` INTEGER NOT NULL DEFAULT 0")
         }
     }
 
     private val migration_40_41 = object : Migration(40, 41) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE `httpTTS` ADD `loginUrl` TEXT")
-            database.execSQL("ALTER TABLE `httpTTS` ADD `loginUi` TEXT")
-            database.execSQL("ALTER TABLE `httpTTS` ADD `loginCheckJs` TEXT")
-            database.execSQL("ALTER TABLE `httpTTS` ADD `header` TEXT")
-            database.execSQL("ALTER TABLE `httpTTS` ADD `concurrentRate` TEXT")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE `httpTTS` ADD `loginUrl` TEXT")
+            db.execSQL("ALTER TABLE `httpTTS` ADD `loginUi` TEXT")
+            db.execSQL("ALTER TABLE `httpTTS` ADD `loginCheckJs` TEXT")
+            db.execSQL("ALTER TABLE `httpTTS` ADD `header` TEXT")
+            db.execSQL("ALTER TABLE `httpTTS` ADD `concurrentRate` TEXT")
         }
     }
 
     private val migration_41_42 = object : Migration(41, 42) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE 'httpTTS' ADD `contentType` TEXT")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE 'httpTTS' ADD `contentType` TEXT")
         }
     }
 
     private val migration_42_43 = object : Migration(42, 43) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE `chapters` ADD `isVolume` INTEGER NOT NULL DEFAULT 0")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE `chapters` ADD `isVolume` INTEGER NOT NULL DEFAULT 0")
         }
     }
 
     private val migration_82_83 = object : Migration(82, 83) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE readRecord RENAME TO readRecord_old")
-            database.execSQL(
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE readRecord RENAME TO readRecord_old")
+            db.execSQL(
                 """
                 CREATE TABLE IF NOT EXISTS `readRecord` (
                     `deviceId` TEXT NOT NULL,
@@ -341,7 +341,7 @@ object DatabaseMigrations {
                 )
                 """
             )
-            database.execSQL(
+            db.execSQL(
                 """
                 INSERT INTO readRecord(deviceId, bookName, bookAuthor, readTime, lastRead)
                 SELECT
@@ -363,10 +363,10 @@ object DatabaseMigrations {
                 FROM readRecord_old rr
                 """
             )
-            database.execSQL("DROP TABLE readRecord_old")
+            db.execSQL("DROP TABLE readRecord_old")
 
-            database.execSQL("ALTER TABLE readRecordDetail RENAME TO readRecordDetail_old")
-            database.execSQL(
+            db.execSQL("ALTER TABLE readRecordDetail RENAME TO readRecordDetail_old")
+            db.execSQL(
                 """
                 CREATE TABLE IF NOT EXISTS `readRecordDetail` (
                     `deviceId` TEXT NOT NULL,
@@ -381,7 +381,7 @@ object DatabaseMigrations {
                 )
                 """
             )
-            database.execSQL(
+            db.execSQL(
                 """
                 INSERT INTO readRecordDetail(
                     deviceId, bookName, bookAuthor, date, readTime, readWords, firstReadTime, lastReadTime
@@ -408,10 +408,10 @@ object DatabaseMigrations {
                 FROM readRecordDetail_old rd
                 """
             )
-            database.execSQL("DROP TABLE readRecordDetail_old")
+            db.execSQL("DROP TABLE readRecordDetail_old")
 
-            database.execSQL("ALTER TABLE readRecordSession RENAME TO readRecordSession_old")
-            database.execSQL(
+            db.execSQL("ALTER TABLE readRecordSession RENAME TO readRecordSession_old")
+            db.execSQL(
                 """
                 CREATE TABLE IF NOT EXISTS `readRecordSession` (
                     `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
@@ -424,7 +424,7 @@ object DatabaseMigrations {
                 )
                 """
             )
-            database.execSQL(
+            db.execSQL(
                 """
                 INSERT INTO readRecordSession(id, deviceId, bookName, bookAuthor, startTime, endTime, words)
                 SELECT
@@ -448,7 +448,7 @@ object DatabaseMigrations {
                 FROM readRecordSession_old rs
                 """
             )
-            database.execSQL("DROP TABLE readRecordSession_old")
+            db.execSQL("DROP TABLE readRecordSession_old")
         }
     }
 
@@ -515,8 +515,8 @@ object DatabaseMigrations {
     }
 
     private val migration_98_99 = object : Migration(98, 99) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL(
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL(
                 """
                 CREATE TABLE IF NOT EXISTS `txtTocRules_new` (
                     `id` INTEGER NOT NULL,
@@ -530,26 +530,26 @@ object DatabaseMigrations {
                 )
                 """.trimIndent()
             )
-            database.execSQL(
+            db.execSQL(
                 """
                 INSERT INTO txtTocRules_new (id, name, chapterRule, volumeRule, example, serialNumber, enable)
                 SELECT id, name, rule, '', example, serialNumber, enable FROM txtTocRules
                 """.trimIndent()
             )
-            database.execSQL("DROP TABLE txtTocRules")
-            database.execSQL("ALTER TABLE txtTocRules_new RENAME TO txtTocRules")
-            database.execSQL("ALTER TABLE highlightRules ADD COLUMN fontWeight INTEGER NOT NULL DEFAULT 400")
-            database.execSQL("ALTER TABLE highlightRules ADD COLUMN isItalic INTEGER NOT NULL DEFAULT 0")
-            database.execSQL("ALTER TABLE highlightRules ADD COLUMN npLeft REAL NOT NULL DEFAULT 0.1")
-            database.execSQL("ALTER TABLE highlightRules ADD COLUMN npRight REAL NOT NULL DEFAULT 0.1")
-            database.execSQL("ALTER TABLE highlightRules ADD COLUMN npTop REAL NOT NULL DEFAULT 0.1")
-            database.execSQL("ALTER TABLE highlightRules ADD COLUMN npBottom REAL NOT NULL DEFAULT 0.1")
+            db.execSQL("DROP TABLE txtTocRules")
+            db.execSQL("ALTER TABLE txtTocRules_new RENAME TO txtTocRules")
+            db.execSQL("ALTER TABLE highlightRules ADD COLUMN fontWeight INTEGER NOT NULL DEFAULT 400")
+            db.execSQL("ALTER TABLE highlightRules ADD COLUMN isItalic INTEGER NOT NULL DEFAULT 0")
+            db.execSQL("ALTER TABLE highlightRules ADD COLUMN npLeft REAL NOT NULL DEFAULT 0.1")
+            db.execSQL("ALTER TABLE highlightRules ADD COLUMN npRight REAL NOT NULL DEFAULT 0.1")
+            db.execSQL("ALTER TABLE highlightRules ADD COLUMN npTop REAL NOT NULL DEFAULT 0.1")
+            db.execSQL("ALTER TABLE highlightRules ADD COLUMN npBottom REAL NOT NULL DEFAULT 0.1")
         }
     }
 
     private val migration_99_100 = object : Migration(99, 100) {
-        override fun migrate(database: SupportSQLiteDatabase) {
-            database.execSQL("ALTER TABLE highlightRules ADD COLUMN fontSizeOffset INTEGER NOT NULL DEFAULT 0")
+        override fun migrate(db: SupportSQLiteDatabase) {
+            db.execSQL("ALTER TABLE highlightRules ADD COLUMN fontSizeOffset INTEGER NOT NULL DEFAULT 0")
         }
     }
 
@@ -560,10 +560,10 @@ object DatabaseMigrations {
      * 中的聚合规则合并,以保证主键唯一。
      */
     private val migration_102_103 = object : Migration(102, 103) {
-        override fun migrate(database: SupportSQLiteDatabase) {
+        override fun migrate(db: SupportSQLiteDatabase) {
             // 将旧设备分区合并到本地分区;同一本书的旧空作者仅在书架存在唯一作者时归并。
             // 阅读时段记录按书名、作者和完整时间区间聚合,字段重复的记录只保留一条并取较大的字数。
-            database.execSQL(
+            db.execSQL(
                 """
                 CREATE TABLE readRecord_migrated (
                     deviceId TEXT NOT NULL,
@@ -575,7 +575,7 @@ object DatabaseMigrations {
                 )
                 """.trimIndent()
             )
-            database.execSQL(
+            db.execSQL(
                 """
                 INSERT INTO readRecord_migrated(deviceId, bookName, bookAuthor, readTime, lastRead)
                 SELECT '', bookName, canonicalAuthor, SUM(readTime), MAX(lastRead)
@@ -590,10 +590,10 @@ object DatabaseMigrations {
                 GROUP BY bookName, canonicalAuthor
                 """.trimIndent()
             )
-            database.execSQL("DROP TABLE readRecord")
-            database.execSQL("ALTER TABLE readRecord_migrated RENAME TO readRecord")
+            db.execSQL("DROP TABLE readRecord")
+            db.execSQL("ALTER TABLE readRecord_migrated RENAME TO readRecord")
 
-            database.execSQL(
+            db.execSQL(
                 """
                 CREATE TABLE readRecordDetail_migrated (
                     deviceId TEXT NOT NULL,
@@ -608,7 +608,7 @@ object DatabaseMigrations {
                 )
                 """.trimIndent()
             )
-            database.execSQL(
+            db.execSQL(
                 """
                 INSERT INTO readRecordDetail_migrated(
                     deviceId, bookName, bookAuthor, date, readTime, readWords,
@@ -629,10 +629,10 @@ object DatabaseMigrations {
                 GROUP BY bookName, canonicalAuthor, date
                 """.trimIndent()
             )
-            database.execSQL("DROP TABLE readRecordDetail")
-            database.execSQL("ALTER TABLE readRecordDetail_migrated RENAME TO readRecordDetail")
+            db.execSQL("DROP TABLE readRecordDetail")
+            db.execSQL("ALTER TABLE readRecordDetail_migrated RENAME TO readRecordDetail")
 
-            database.execSQL(
+            db.execSQL(
                 """
                 CREATE TABLE readRecordSession_migrated (
                     id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
@@ -645,7 +645,7 @@ object DatabaseMigrations {
                 )
                 """.trimIndent()
             )
-            database.execSQL(
+            db.execSQL(
                 """
                 INSERT INTO readRecordSession_migrated(
                     deviceId, bookName, bookAuthor, startTime, endTime, words
@@ -662,8 +662,8 @@ object DatabaseMigrations {
                 GROUP BY bookName, canonicalAuthor, startTime, endTime
                 """.trimIndent()
             )
-            database.execSQL("DROP TABLE readRecordSession")
-            database.execSQL("ALTER TABLE readRecordSession_migrated RENAME TO readRecordSession")
+            db.execSQL("DROP TABLE readRecordSession")
+            db.execSQL("ALTER TABLE readRecordSession_migrated RENAME TO readRecordSession")
         }
     }
 }

+ 2 - 1
app/src/main/java/io/legado/app/di/appModule.kt

@@ -776,7 +776,8 @@ val appModule = module {
             changeBookSourceUseCase = get(),
             clearBookCacheUseCase = get(),
             deleteBooksUseCase = get(),
-            updateBooksGroupUseCase = get()
+            updateBooksGroupUseCase = get(),
+            downloadCacheSettingsGateway = get()
         )
     }
 

+ 1 - 0
app/src/main/java/io/legado/app/domain/model/AiMessageParts.kt

@@ -121,6 +121,7 @@ object AiMessagePartJson {
      * Migrate legacy ToolCall/ToolResult pairs into unified Tool parts.
      * Handles old messages stored before the unified Tool part was introduced.
      */
+    @Suppress("DEPRECATION")
     private fun AiMessagePart.migrateLegacy(): AiMessagePart {
         return when (this) {
             is AiMessagePart.ToolCall -> AiMessagePart.Tool(

+ 6 - 3
app/src/main/java/io/legado/app/help/AppFreezeMonitor.kt

@@ -8,13 +8,16 @@ import android.content.IntentFilter
 import android.os.Handler
 import android.os.HandlerThread
 import android.os.SystemClock
-import io.legado.app.help.config.AppConfig
+import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.utils.LogUtils
+import org.koin.core.context.GlobalContext
 
 object AppFreezeMonitor {
 
     private const val TAG = "AppFreezeMonitor"
 
+    private val otherGateway by lazy { GlobalContext.get().get<OtherSettingsGateway>() }
+
     val handler by lazy {
         Handler(HandlerThread("AppFreezeMonitor").apply { start() }.looper)
     }
@@ -28,7 +31,7 @@ object AppFreezeMonitor {
 
     @SuppressLint("UnspecifiedRegisterReceiverFlag")
     fun init(context: Context) {
-        if (!AppConfig.recordLog) {
+        if (!otherGateway.currentSettings.recordLog) {
             if (registeredReceiver) {
                 registeredReceiver = false
                 context.unregisterReceiver(screenStatusReceiver)
@@ -60,7 +63,7 @@ object AppFreezeMonitor {
 
                 previous = current
 
-                if (AppConfig.recordLog) {
+                if (otherGateway.currentSettings.recordLog) {
                     handler.postDelayed(this, 3000)
                 } else {
                     monitorRunnable = null

+ 14 - 12
app/src/main/java/io/legado/app/help/AppWebDav.kt

@@ -7,7 +7,7 @@ import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookProgress
 import io.legado.app.exception.NoStackTraceException
-import io.legado.app.help.config.AppConfig
+import io.legado.app.domain.gateway.BackupSettingsGateway
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.help.storage.Backup
 import io.legado.app.help.storage.BackupRestoreLock
@@ -17,7 +17,6 @@ import io.legado.app.lib.webdav.WebDav
 import io.legado.app.lib.webdav.WebDavException
 import io.legado.app.lib.webdav.WebDavFile
 import io.legado.app.model.remote.RemoteBookWebDav
-import io.legado.app.ui.config.backupConfig.BackupConfig
 import io.legado.app.utils.AlphanumComparator
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.GSON
@@ -33,12 +32,15 @@ import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
 import splitties.init.appCtx
+import org.koin.core.context.GlobalContext
 import java.io.File
 
 /**
  * webDav初始化会访问网络,不要放到主线程
  */
 object AppWebDav {
+    private val backupGateway by lazy { GlobalContext.get().get<BackupSettingsGateway>() }
+
     private const val defaultWebDavUrl = "https://dav.jianguoyun.com/dav/"
     private val bookProgressUrl get() = "${rootWebDavUrl}bookProgress/"
     private val exportsWebDavUrl get() = "${rootWebDavUrl}books/"
@@ -60,10 +62,10 @@ object AppWebDav {
 
     private val rootWebDavUrl: String
         get() {
-            val configUrl = BackupConfig.webDavUrl.trim()
+            val configUrl = backupGateway.currentSettings.webDavUrl.trim()
             var url = if (configUrl.isEmpty()) defaultWebDavUrl else configUrl
             if (!url.endsWith("/")) url = "${url}/"
-            AppConfig.webDavDir.trim().let {
+            backupGateway.currentSettings.webDavDir.trim().let {
                 if (it.isNotEmpty()) {
                     url = "${url}${it}/"
                 }
@@ -74,10 +76,10 @@ object AppWebDav {
     suspend fun upConfig() {
         configMutex.withLock {
             val config = AppliedWebDavConfig(
-                url = BackupConfig.webDavUrl,
-                account = BackupConfig.webDavAccount,
-                password = BackupConfig.webDavPassword,
-                dir = BackupConfig.webDavDir,
+                url = backupGateway.currentSettings.webDavUrl,
+                account = backupGateway.currentSettings.webDavAccount,
+                password = backupGateway.currentSettings.webDavPassword,
+                dir = backupGateway.currentSettings.webDavDir,
             )
             if (appliedConfig == config) return
 
@@ -176,8 +178,8 @@ object AppWebDav {
 
     suspend fun testWebDav(): Boolean {
         return kotlin.runCatching {
-            val account = BackupConfig.webDavAccount
-            val password = BackupConfig.webDavPassword
+            val account = backupGateway.currentSettings.webDavAccount
+            val password = backupGateway.currentSettings.webDavPassword
             if (account.isNullOrEmpty() || password.isNullOrEmpty()) {
                 appCtx.toastOnUi("账号或密码为空")
                 return false
@@ -289,7 +291,7 @@ object AppWebDav {
         onSuccess: (() -> Unit)? = null
     ) {
         val authorization = authorization ?: return
-        if (!AppConfig.syncBookProgress) return
+        if (!backupGateway.currentSettings.syncBookProgress) return
         if (!NetworkUtils.isAvailable()) return
         try {
             val bookProgress = BookProgress(book)
@@ -310,7 +312,7 @@ object AppWebDav {
     ): Boolean {
         try {
             val authorization = authorization ?: return false
-            if (!AppConfig.syncBookProgress) return false
+            if (!backupGateway.currentSettings.syncBookProgress) return false
             if (!NetworkUtils.isAvailable()) return false
             val json = GSON.toJson(bookProgress)
             val url = getProgressUrl(bookProgress.name, bookProgress.author)

+ 10 - 3
app/src/main/java/io/legado/app/help/CrashHandler.kt

@@ -11,8 +11,9 @@ import android.os.Process
 import android.webkit.WebSettings
 import io.legado.app.constant.AppConst
 import io.legado.app.constant.AppLog
+import io.legado.app.domain.gateway.BackupSettingsGateway
+import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.exception.NoStackTraceException
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.model.ReadAloud
 import io.legado.app.utils.FileDoc
@@ -25,6 +26,7 @@ import io.legado.app.utils.longToastOnUiLegacy
 import io.legado.app.utils.stackTraceStr
 import io.legado.app.utils.writeText
 import splitties.init.appCtx
+import org.koin.core.context.GlobalContext
 import java.io.PrintWriter
 import java.io.StringWriter
 import java.text.SimpleDateFormat
@@ -85,7 +87,9 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler {
         LocalConfig.appCrash = true
         //保存日志文件
         val crashFileName = saveCrashInfo2File(ex)
-        if ((ex is OutOfMemoryError || ex.cause is OutOfMemoryError) && AppConfig.recordHeapDump) {
+        if ((ex is OutOfMemoryError || ex.cause is OutOfMemoryError) &&
+            otherGateway.currentSettings.recordHeapDump
+        ) {
             doHeapDump()
         }
         return if (startCrashReport(crashFileName)) {
@@ -111,6 +115,9 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler {
     companion object {
         const val EXTRA_CRASH_FILE_NAME = "crashFileName"
 
+        private val otherGateway by lazy { GlobalContext.get().get<OtherSettingsGateway>() }
+        private val backupGateway by lazy { GlobalContext.get().get<BackupSettingsGateway>() }
+
         private const val CRASH_REPORT_ACTIVITY = "io.legado.app.ui.about.CrashReportActivity"
 
         /**
@@ -172,7 +179,7 @@ class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler {
             val time = format.format(Date())
             val fileName = "crash-$time-$timestamp.log"
             try {
-                val backupPath = AppConfig.backupPath
+                val backupPath = backupGateway.currentSettings.backupPath
                     ?: throw NoStackTraceException("备份路径未配置")
                 val uri = Uri.parse(backupPath)
                 val fileDoc = FileDoc.fromUri(uri, true)

+ 5 - 2
app/src/main/java/io/legado/app/help/DispatchersMonitor.kt

@@ -1,7 +1,8 @@
 package io.legado.app.help
 
-import io.legado.app.help.config.AppConfig
+import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.utils.LogUtils
+import org.koin.core.context.GlobalContext
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers.Default
@@ -22,6 +23,8 @@ object DispatchersMonitor {
 
     private const val TAG = "DispatchersMonitor"
 
+    private val otherGateway by lazy { GlobalContext.get().get<OtherSettingsGateway>() }
+
     private val dispatcher by lazy {
         Executors.newSingleThreadExecutor {
             Thread(it, TAG)
@@ -32,7 +35,7 @@ object DispatchersMonitor {
 
     fun init() {
         scope.coroutineContext.cancelChildren()
-        if (!AppConfig.recordLog) {
+        if (!otherGateway.currentSettings.recordLog) {
             return
         }
         monitor(IO)

+ 14 - 9
app/src/main/java/io/legado/app/help/JsExtensions.kt

@@ -12,9 +12,9 @@ import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppPattern
 import io.legado.app.data.entities.BaseSource
 import io.legado.app.exception.NoStackTraceException
-import io.legado.app.help.config.AppConfig
+import io.legado.app.domain.gateway.AppShellSettingsGateway
+import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
 import io.legado.app.help.config.ThemeConfigStore
-import io.legado.app.ui.config.themeConfig.ThemeConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.crypto.base64ToByteArray
 import io.legado.app.help.crypto.digest
@@ -32,7 +32,6 @@ import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.analyzeRule.QueryTTF
 import io.legado.app.ui.association.OnLineImportActivity
 import io.legado.app.ui.association.OpenUrlConfirmActivity
-import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.utils.ArchiveUtils
 import io.legado.app.utils.ChineseUtils
 import io.legado.app.utils.EncoderUtils
@@ -65,6 +64,7 @@ import okio.use
 import org.jsoup.Connection
 import org.jsoup.Jsoup
 import splitties.init.appCtx
+import org.koin.core.context.GlobalContext
 import java.io.ByteArrayInputStream
 import java.io.ByteArrayOutputStream
 import java.io.File
@@ -93,6 +93,11 @@ interface JsExtensions : JsEncodeUtils {
     fun getSource(): BaseSource?
     fun getTag(): String?
 
+    private val cacheGateway
+        get() = GlobalContext.get().get<DownloadCacheSettingsGateway>()
+    private val shellGateway
+        get() = GlobalContext.get().get<AppShellSettingsGateway>()
+
     private val context: CoroutineContext
         get() = rhinoContextOrNull?.coroutineContext ?: EmptyCoroutineContext
 
@@ -133,7 +138,7 @@ interface JsExtensions : JsEncodeUtils {
     }
     fun ajaxAll(urlList: Array<String>, skipRateLimit: Boolean): Array<StrResponse> {
         return runBlocking(context) {
-            urlList.asFlow().mapAsync(OtherConfig.threadCount) { url ->
+            urlList.asFlow().mapAsync(cacheGateway.currentSettings.threadCount) { url ->
                 val analyzeUrl = AnalyzeUrl(
                     url,
                     source = getSource(),
@@ -156,7 +161,7 @@ interface JsExtensions : JsEncodeUtils {
         skipRateLimit: Boolean
     ): Array<StrResponse> {
         return runBlocking(context) {
-            urlList.asFlow().mapAsync(OtherConfig.threadCount) { url ->
+            urlList.asFlow().mapAsync(cacheGateway.currentSettings.threadCount) { url ->
                 val analyzeUrl = AnalyzeUrl(
                     url,
                     source = getSource(),
@@ -232,7 +237,7 @@ interface JsExtensions : JsEncodeUtils {
                 url = url,
                 html = html,
                 javaScript = js,
-                headerMap = getSource()?.getHeaderMap(AppConfig.userAgent, true),
+                headerMap = getSource()?.getHeaderMap(cacheGateway.currentSettings.userAgent, true),
                 tag = getSource()?.getKey()
             ).getStrResponse().body
         }
@@ -271,7 +276,7 @@ interface JsExtensions : JsEncodeUtils {
                 url = url,
                 html = html,
                 javaScript = js,
-                headerMap = getSource()?.getHeaderMap(AppConfig.userAgent, true),
+                headerMap = getSource()?.getHeaderMap(cacheGateway.currentSettings.userAgent, true),
                 tag = getSource()?.getKey(),
                 sourceRegex = sourceRegex,
                 delayTime = delayTime
@@ -317,7 +322,7 @@ interface JsExtensions : JsEncodeUtils {
                 url = url,
                 html = html,
                 javaScript = js,
-                headerMap = getSource()?.getHeaderMap(AppConfig.userAgent, true),
+                headerMap = getSource()?.getHeaderMap(cacheGateway.currentSettings.userAgent, true),
                 tag = getSource()?.getKey(),
                 overrideUrlRegex = overrideUrlRegex,
                 delayTime = delayTime
@@ -1196,7 +1201,7 @@ interface JsExtensions : JsEncodeUtils {
      */
     @JavascriptInterface
     fun getThemeMode(): String {
-        return ThemeConfig.themeMode
+        return shellGateway.currentSettings.themeMode
     }
 
     /**

+ 12 - 9
app/src/main/java/io/legado/app/help/WebDavManager.kt

@@ -6,7 +6,7 @@ import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookProgress
 import io.legado.app.exception.NoStackTraceException
-import io.legado.app.help.config.AppConfig
+import io.legado.app.domain.gateway.BackupSettingsGateway
 import io.legado.app.help.config.AppConfigStore
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.help.storage.Backup
@@ -17,7 +17,6 @@ import io.legado.app.lib.webdav.WebDav
 import io.legado.app.lib.webdav.WebDavException
 import io.legado.app.lib.webdav.WebDavFile
 import io.legado.app.model.remote.RemoteBookWebDav
-import io.legado.app.ui.config.backupConfig.BackupConfig
 import io.legado.app.utils.AlphanumComparator
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.GSON
@@ -41,6 +40,7 @@ import kotlinx.coroutines.flow.debounce
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
+import org.koin.core.context.GlobalContext
 import java.io.File
 import kotlin.coroutines.cancellation.CancellationException
 
@@ -72,6 +72,8 @@ class WebDavManager(
     private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
 ) {
 
+    private val backupGateway by lazy { GlobalContext.get().get<BackupSettingsGateway>() }
+
     private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
     private val configFlow = MutableStateFlow(readConfig())
     private val _state = MutableStateFlow<WebDavState>(WebDavState.Unconfigured)
@@ -213,7 +215,7 @@ class WebDavManager(
         onSuccess: (() -> Unit)? = null
     ) {
         val auth = requireAuthorization()
-        if (!AppConfig.syncBookProgress) return
+        if (!backupGateway.currentSettings.syncBookProgress) return
         if (!NetworkUtils.isAvailable()) return
         try {
             val bookProgress = BookProgress(book)
@@ -231,7 +233,7 @@ class WebDavManager(
 
     suspend fun uploadBookProgress(bookProgress: BookProgress, onSuccess: (() -> Unit)? = null) {
         val auth = requireAuthorization()
-        if (!AppConfig.syncBookProgress) return
+        if (!backupGateway.currentSettings.syncBookProgress) return
         if (!NetworkUtils.isAvailable()) return
         val json = GSON.toJson(bookProgress)
         val url = getProgressUrl(bookProgress.name, bookProgress.author)
@@ -285,12 +287,13 @@ class WebDavManager(
     }
 
     private fun readConfig(): WebDavConfig {
+        val settings = backupGateway.currentSettings
         return WebDavConfig(
-            url = BackupConfig.webDavUrl,
-            account = BackupConfig.webDavAccount,
-            password = BackupConfig.webDavPassword,
-            dir = BackupConfig.webDavDir,
-            deviceName = BackupConfig.webDavDeviceName
+            url = settings.webDavUrl,
+            account = settings.webDavAccount,
+            password = settings.webDavPassword,
+            dir = settings.webDavDir,
+            deviceName = settings.webDavDeviceName
         )
     }
 

+ 16 - 9
app/src/main/java/io/legado/app/help/book/BookExtensions.kt

@@ -21,10 +21,12 @@ import io.legado.app.data.entities.HighlightTagRule
 import io.legado.app.data.entities.TagGroupRule
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.RuleBigDataHelp
-import io.legado.app.help.config.AppConfig
+import io.legado.app.domain.gateway.BookExportSettingsGateway
+import io.legado.app.domain.gateway.ImportBookSettingsGateway
+import io.legado.app.domain.gateway.OtherSettingsGateway
+import io.legado.app.domain.gateway.ReadSettingsGateway
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.ui.book.info.HighlightedTag
-import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.utils.FileDoc
 import io.legado.app.utils.GSON
 import io.legado.app.utils.MD5Utils
@@ -45,7 +47,12 @@ import kotlinx.datetime.daysUntil
 import kotlinx.datetime.todayIn
 import kotlin.math.max
 import kotlin.math.min
+import org.koin.core.context.GlobalContext
 
+private val otherGateway by lazy { GlobalContext.get().get<OtherSettingsGateway>() }
+private val importBookGateway by lazy { GlobalContext.get().get<ImportBookSettingsGateway>() }
+private val readGateway by lazy { GlobalContext.get().get<ReadSettingsGateway>() }
+private val exportGateway by lazy { GlobalContext.get().get<BookExportSettingsGateway>() }
 
 val Book.isAudio: Boolean
     get() = isType(BookType.audio)
@@ -189,8 +196,8 @@ fun Book.getLocalUri(): Uri {
         return uri
     }
     //不同的设备书籍保存路径可能不一样, uri无效时尝试寻找当前保存路径下的文件
-    val defaultBookDir = OtherConfig.defaultBookTreeUri
-    val importBookDir = AppConfig.importBookPath
+    val defaultBookDir = otherGateway.currentSettings.defaultBookTreeUri
+    val importBookDir = importBookGateway.currentSettings.importBookPath
 
     // 查找书籍保存目录
     if (!defaultBookDir.isNullOrBlank()) {
@@ -261,7 +268,7 @@ fun Book.getLocalUri(): Uri {
 
 
 fun Book.getArchiveUri(): Uri? {
-    val defaultBookDir = OtherConfig.defaultBookTreeUri
+    val defaultBookDir = otherGateway.currentSettings.defaultBookTreeUri
     return if (isArchive && !defaultBookDir.isNullOrBlank()) {
         FileDoc.fromUri(defaultBookDir.toUri(), true)
             .find(archiveName)?.uri
@@ -542,8 +549,8 @@ fun Book.sync(oldBook: Book) {
         appDb.bookChapterDao.getChapter(bookUrl, durChapterIndex)?.let {
             durChapterTitle = it.getDisplayTitle(
                 replaceRules,
-                getUseReplaceRule(AppConfig.replaceEnableDefault),
-                chineseConverterType = AppConfig.chineseConverterType,
+                getUseReplaceRule(otherGateway.currentSettings.replaceEnableDefault),
+                chineseConverterType = readGateway.currentSettings.chineseConverterType,
             )
         }
     }
@@ -619,7 +626,7 @@ fun Book.isSameNameAuthor(other: Any?): Boolean {
 
 fun Book.getExportFileName(
     suffix: String,
-    template: String? = AppConfig.bookExportFileName,
+    template: String? = exportGateway.currentSettings.bookExportFileName,
 ): String {
     if (template.isNullOrBlank()) {
         return "$name 作者:${getRealAuthor()}.$suffix"
@@ -672,7 +679,7 @@ fun Book.getExportFileName(
 fun Book.getExportFileName(
     suffix: String,
     epubIndex: Int,
-    jsStr: String? = AppConfig.episodeExportFileName
+    jsStr: String? = exportGateway.currentSettings.episodeExportFileName
 ): String {
     // 默认规则
     val default = "$name 作者:${getRealAuthor()} [${epubIndex}].$suffix"

+ 11 - 7
app/src/main/java/io/legado/app/help/book/BookHelp.kt

@@ -15,8 +15,8 @@ import io.legado.app.data.entities.BookSource
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.model.localBook.TextFile
-import io.legado.app.ui.config.otherConfig.OtherConfig
-import io.legado.app.ui.config.readConfig.ReadConfig
+import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
+import io.legado.app.domain.gateway.ReadSettingsGateway
 import io.legado.app.utils.ArchiveUtils
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.ImageUtils
@@ -46,6 +46,7 @@ import kotlinx.coroutines.sync.Semaphore
 import kotlinx.coroutines.sync.withLock
 import kotlinx.coroutines.withContext
 import org.apache.commons.text.similarity.JaccardSimilarity
+import org.koin.core.context.GlobalContext
 import splitties.init.appCtx
 import java.io.ByteArrayInputStream
 import java.io.File
@@ -61,6 +62,9 @@ import kotlin.math.min
 
 @Suppress("unused", "ConstPropertyName")
 object BookHelp {
+    private val readGateway by lazy { GlobalContext.get().get<ReadSettingsGateway>() }
+    private val cacheGateway by lazy { GlobalContext.get().get<DownloadCacheSettingsGateway>() }
+
     private const val CACHED_CONTENT_PREFIX_LENGTH = 1_024
     private val downloadDir: File = appCtx.externalFiles
     private const val cacheFolderName = "book_cache"
@@ -136,12 +140,12 @@ object BookHelp {
     private fun clearComicCache(book: Book) {
         //只处理漫画
         //为0的时候,不清除已缓存数据
-        if (!book.isImage || ReadConfig.imageRetainNum == 0) {
+        if (!book.isImage || cacheGateway.currentSettings.imageRetainNum == 0) {
             return
         }
         //向前保留设定数量,向后保留预下载数量
-        val startIndex = book.durChapterIndex - ReadConfig.imageRetainNum
-        val endIndex = book.durChapterIndex + ReadConfig.preDownloadNum
+        val startIndex = book.durChapterIndex - cacheGateway.currentSettings.imageRetainNum
+        val endIndex = book.durChapterIndex + readGateway.currentSettings.preDownloadNum
         val chapterList = appDb.bookChapterDao.getChapterList(book.bookUrl, startIndex, endIndex)
         val imgNames = hashSetOf<String>()
         //获取需要保留章节的图片信息
@@ -204,7 +208,7 @@ object BookHelp {
             book.getFolderName(),
             bookChapter.getFileName(),
         ).writeText(content)
-        if (book.isOnLineTxt && ReadConfig.tocCountWords) {
+        if (book.isOnLineTxt && readGateway.currentSettings.tocCountWords) {
             val wordCount = StringUtils.wordCountFormat(content.length)
             bookChapter.wordCount = wordCount
             appDb.bookChapterDao.update(bookChapter)
@@ -302,7 +306,7 @@ object BookHelp {
         book: Book,
         bookChapter: BookChapter,
         content: String,
-        concurrency: Int = OtherConfig.threadCount,
+        concurrency: Int = cacheGateway.currentSettings.threadCount,
         onProgress: (suspend (completed: Int, total: Int) -> Unit)? = null,
     ): Int = coroutineScope {
         val imageUrls = flowImages(bookChapter, content).toList()

+ 13 - 8
app/src/main/java/io/legado/app/help/book/ContentProcessor.kt

@@ -14,7 +14,8 @@ import io.legado.app.domain.model.BookContentProcessEngine
 import io.legado.app.domain.model.TextProcessAction
 import io.legado.app.domain.model.TextProcessStyle
 import io.legado.app.exception.RegexTimeoutException
-import io.legado.app.help.config.AppConfig
+import io.legado.app.domain.gateway.OtherSettingsGateway
+import io.legado.app.domain.gateway.ReadSettingsGateway
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.utils.ChineseUtils
 import io.legado.app.utils.GSON
@@ -24,6 +25,7 @@ import io.legado.app.utils.replace
 import io.legado.app.utils.stackTraceStr
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.CancellationException
+import org.koin.core.context.GlobalContext
 import splitties.init.appCtx
 import java.lang.ref.WeakReference
 import java.util.concurrent.CopyOnWriteArrayList
@@ -61,6 +63,9 @@ class ContentProcessor private constructor(
     private val contentReplaceRules = CopyOnWriteArrayList<ReplaceRule>()
     val removeSameTitleCache = hashSetOf<String>()
 
+    private val otherGateway by lazy { GlobalContext.get().get<OtherSettingsGateway>() }
+    private val readGateway by lazy { GlobalContext.get().get<ReadSettingsGateway>() }
+
     init {
         upReplaceRules()
         upRemoveSameTitle()
@@ -119,12 +124,12 @@ class ContentProcessor private constructor(
                 if (match != null) {
                     mContent = mContent.substring(match.range.last + 1)
                     sameTitleRemoved = true
-                } else if (useReplace && book.getUseReplaceRule(AppConfig.replaceEnableDefault)) {
+                } else if (useReplace && book.getUseReplaceRule(otherGateway.currentSettings.replaceEnableDefault)) {
                     title = Regex.escape(
                         chapter.getDisplayTitle(
                             contentReplaceRules,
                             chineseConvert = false,
-                            chineseConverterType = AppConfig.chineseConverterType,
+                            chineseConverterType = readGateway.currentSettings.chineseConverterType,
                         )
                     )
                     match = Regex("^(\\s|\\p{P}|${name})*${title}(\\s)*")
@@ -144,7 +149,7 @@ class ContentProcessor private constructor(
             if (chineseConvert) {
                 //简繁转换
                 try {
-                    when (AppConfig.chineseConverterType) {
+                    when (readGateway.currentSettings.chineseConverterType) {
                         1 -> mContent = ChineseUtils.t2s(mContent)
                         2 -> mContent = ChineseUtils.s2t(mContent)
                     }
@@ -153,14 +158,14 @@ class ContentProcessor private constructor(
                 }
             }
             val useHtmlMap = mutableMapOf<String, String>()
-            if (AppConfig.adaptSpecialStyle) { //html处理
+            if (readGateway.currentSettings.adaptSpecialStyle) { //html处理
                 mContent = AppPattern.useHtmlRegex.replace(mContent) { matchResult ->
                     val placeholder = "特殊格式的占位不应该被看见${useHtmlMap.size}。"
                     useHtmlMap[placeholder] = "\n${matchResult.value.replace("\n", "")}\n"
                     placeholder
                 }
             }
-            if (useReplace && book.getUseReplaceRule(AppConfig.replaceEnableDefault)) {
+            if (useReplace && book.getUseReplaceRule(otherGateway.currentSettings.replaceEnableDefault)) {
                 val replaceBook = book.toSearchBook()
                 //替换
                 effectiveReplaceRules = arrayListOf()
@@ -220,8 +225,8 @@ class ContentProcessor private constructor(
             //重新添加标题
             mContent = chapter.getDisplayTitle(
                 getTitleReplaceRules(),
-                useReplace = useReplace && book.getUseReplaceRule(AppConfig.replaceEnableDefault),
-                chineseConverterType = AppConfig.chineseConverterType,
+                useReplace = useReplace && book.getUseReplaceRule(otherGateway.currentSettings.replaceEnableDefault),
+                chineseConverterType = readGateway.currentSettings.chineseConverterType,
             ) + "\n" + mContent
         }
         if (isAndroid8) {

+ 1 - 2
app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt

@@ -11,7 +11,6 @@ import io.legado.app.data.entities.HighlightRule
 import io.legado.app.data.repository.ReadStyleConfigStore
 import io.legado.app.domain.gateway.ReadSettingsGateway
 import io.legado.app.model.ReadSessionState
-import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.GSON
 import io.legado.app.utils.fromJsonObject
 import io.legado.app.utils.hexString
@@ -154,7 +153,7 @@ object ReadBookConfig {
     val resolvedMenuBgColor: Int
         get() {
             val isNight = ReadStyleResolver.isNightTheme()
-            return when (ReadConfig.readBarStyle) {
+            return when (readSettings.readBarStyle) {
                 1 -> { // 跟随阅读背景
                     val background = ReadStyleResolver.currentBackground(durConfig)
                     if (background.type == 0) {

+ 22 - 5
app/src/main/java/io/legado/app/help/config/ReadStyleResolver.kt

@@ -5,18 +5,35 @@ import android.graphics.drawable.Drawable
 import androidx.core.graphics.drawable.toDrawable
 import androidx.core.graphics.toColorInt
 import com.google.android.material.color.MaterialColors
+import io.legado.app.domain.gateway.AppShellSettingsGateway
+import io.legado.app.domain.gateway.ThemeSettingsGateway
 import io.legado.app.model.ReadSessionState
-import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.BitmapUtils
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.externalFiles
+import io.legado.app.utils.isNightMode
 import io.legado.app.utils.printOnDebug
 import io.legado.app.utils.resizeAndRecycle
+import io.legado.app.utils.sysConfiguration
 import splitties.init.appCtx
+import org.koin.core.context.GlobalContext
 import java.io.File
 
 object ReadStyleResolver {
 
+    private val shellGateway by lazy { GlobalContext.get().get<AppShellSettingsGateway>() }
+    private val themeGateway by lazy { GlobalContext.get().get<ThemeSettingsGateway>() }
+
+    private val isNightThemeCompat: Boolean
+        get() = when (shellGateway.currentSettings.themeMode) {
+            "1" -> false
+            "2" -> true
+            else -> sysConfiguration.isNightMode
+        }
+
+    private val isEInkModeCompat: Boolean
+        get() = themeGateway.currentSettings.appTheme == "4"
+
     enum class ReadStyleMode {
         Day,
         Night,
@@ -29,16 +46,16 @@ object ReadStyleResolver {
     )
 
     fun currentMode(): ReadStyleMode {
-        val isNightTheme = ReadSessionState.isDarkThemeOverride ?: ReadConfig.isNightTheme
-        return resolveReadStyleMode(ReadConfig.isEInkMode, isNightTheme)
+        val isNightTheme = ReadSessionState.isDarkThemeOverride ?: isNightThemeCompat
+        return resolveReadStyleMode(isEInkModeCompat, isNightTheme)
     }
 
     fun currentMode(isNightTheme: Boolean): ReadStyleMode {
-        return resolveReadStyleMode(ReadConfig.isEInkMode, isNightTheme)
+        return resolveReadStyleMode(isEInkModeCompat, isNightTheme)
     }
 
     fun isNightTheme(): Boolean {
-        return ReadSessionState.isDarkThemeOverride ?: ReadConfig.isNightTheme
+        return ReadSessionState.isDarkThemeOverride ?: isNightThemeCompat
     }
 
     fun withCurrentBackground(

+ 41 - 18
app/src/main/java/io/legado/app/help/config/ThemeConfigStore.kt

@@ -4,10 +4,12 @@ import android.content.Context
 import android.graphics.Bitmap
 import android.util.DisplayMetrics
 import androidx.annotation.Keep
+import androidx.appcompat.app.AppCompatDelegate
 import androidx.core.graphics.toColorInt
+import io.legado.app.domain.gateway.AppShellSettingsGateway
+import io.legado.app.domain.gateway.ThemeSettingsGateway
 import io.legado.app.ui.book.read.ConfigUpdateAction
 import io.legado.app.ui.book.read.ReadConfigUpdateBus
-import io.legado.app.ui.config.themeConfig.ThemeConfig
 import io.legado.app.R
 import io.legado.app.constant.PreferKey
 import io.legado.app.constant.Theme
@@ -23,8 +25,11 @@ import io.legado.app.utils.getFile
 import io.legado.app.utils.getPrefInt
 import io.legado.app.utils.getPrefString
 import io.legado.app.utils.hexString
+import io.legado.app.utils.isNightMode
 import io.legado.app.utils.printOnDebug
 import io.legado.app.utils.stackBlur
+import io.legado.app.utils.sysConfiguration
+import org.koin.core.context.GlobalContext
 import splitties.init.appCtx
 import java.io.File
 
@@ -33,13 +38,26 @@ object ThemeConfigStore {
     const val configFileName = "themeConfig.json"
     val configFilePath = FileUtils.getPath(appCtx.filesDir, configFileName)
 
+    private val themeSettingsGateway: ThemeSettingsGateway
+        get() = GlobalContext.get().get()
+
+    private val shellSettingsGateway: AppShellSettingsGateway
+        get() = GlobalContext.get().get()
+
+    val isNightTheme: Boolean
+        get() = when (shellSettingsGateway.currentSettings.themeMode) {
+            "1" -> false
+            "2" -> true
+            else -> sysConfiguration.isNightMode
+        }
+
     val configList: ArrayList<Config> by lazy {
         val cList = getConfigs() ?: DefaultData.themeConfigs
         ArrayList(cList)
     }
 
     fun getTheme() = when {
-        AppConfig.isNightTheme -> Theme.Dark
+        isNightTheme -> Theme.Dark
         else -> Theme.Light
     }
 
@@ -62,19 +80,25 @@ object ThemeConfigStore {
     }
 
     fun initNightMode() {
-        ThemeConfig.initNightMode()
+        val mode = when (shellSettingsGateway.currentSettings.themeMode) {
+            "1" -> AppCompatDelegate.MODE_NIGHT_NO
+            "2" -> AppCompatDelegate.MODE_NIGHT_YES
+            else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
+        }
+        AppCompatDelegate.setDefaultNightMode(mode)
     }
 
     fun getBgImage(context: Context, metrics: DisplayMetrics): Bitmap? {
+        val theme = themeSettingsGateway.currentSettings
         val bgCfg = when (getTheme()) {
             Theme.Light -> Pair(
-                ThemeConfig.bgImageLight,
-                ThemeConfig.bgImageBlurring
+                theme.backgroundImageLight,
+                theme.backgroundImageBlurring
             )
 
             Theme.Dark -> Pair(
-                ThemeConfig.bgImageDark,
-                ThemeConfig.bgImageNBlurring
+                theme.backgroundImageDark,
+                theme.backgroundImageDarkBlurring
             )
 
             else -> null
@@ -153,13 +177,14 @@ object ThemeConfigStore {
      * 清理无用背景图片
      */
     fun clearBg() {
-        val bgImagePath = ThemeConfig.bgImageLight
+        val theme = themeSettingsGateway.currentSettings
+        val bgImagePath = theme.backgroundImageLight
         appCtx.externalFiles.getFile(PreferKey.bgImage).listFiles()?.forEach {
             if (it.absolutePath != bgImagePath) {
                 it.delete()
             }
         }
-        val bgImageNPath = ThemeConfig.bgImageDark
+        val bgImageNPath = theme.backgroundImageDark
         appCtx.externalFiles.getFile(PreferKey.bgImageN).listFiles()?.forEach {
             if (it.absolutePath != bgImageNPath) {
                 it.delete()
@@ -168,7 +193,7 @@ object ThemeConfigStore {
     }
 
     fun getDurConfig(context: Context): Config {
-        val isNight = AppConfig.isNightTheme
+        val isNight = isNightTheme
         val name = if (isNight) {
             "MD3-Night"
         } else {
@@ -190,10 +215,9 @@ object ThemeConfigStore {
             context.getPrefInt(PreferKey.cBackground, context.getCompatColor(R.color.md_grey_100))
         val bBackground =
             context.getPrefInt(PreferKey.cBBackground, context.getCompatColor(R.color.md_grey_200))
-        val bgImgPath =
-            ThemeConfig.bgImageLight
-        val bgImgBlur =
-            ThemeConfig.bgImageBlurring
+        val theme = themeSettingsGateway.currentSettings
+        val bgImgPath = theme.backgroundImageLight
+        val bgImgBlur = theme.backgroundImageBlurring
 
         return Config(
             themeName = name,
@@ -222,10 +246,9 @@ object ThemeConfigStore {
             context.getPrefInt(PreferKey.cNBackground, context.getCompatColor(R.color.md_grey_900))
         val bBackground =
             context.getPrefInt(PreferKey.cNBBackground, context.getCompatColor(R.color.md_grey_850))
-        val bgImgPath =
-            ThemeConfig.bgImageDark
-        val bgImgBlur =
-            ThemeConfig.bgImageNBlurring
+        val theme = themeSettingsGateway.currentSettings
+        val bgImgPath = theme.backgroundImageDark
+        val bgImgBlur = theme.backgroundImageDarkBlurring
         return Config(
             themeName = name,
             isNightTheme = true,

+ 3 - 2
app/src/main/java/io/legado/app/help/glide/LegadoGlideModule.kt

@@ -11,7 +11,6 @@ import com.bumptech.glide.load.engine.cache.MemorySizeCalculator
 import com.bumptech.glide.load.model.GlideUrl
 import com.bumptech.glide.module.AppGlideModule
 import io.legado.app.BuildConfig
-import io.legado.app.help.config.AppConfig
 import java.io.File
 import java.io.InputStream
 
@@ -20,6 +19,8 @@ import java.io.InputStream
 @GlideModule
 class LegadoGlideModule : AppGlideModule() {
 
+    private val otherSettingsGateway get() = org.koin.core.context.GlobalContext.get().get<io.legado.app.domain.gateway.OtherSettingsGateway>()
+
     override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
         registry.replace(
             GlideUrl::class.java,
@@ -45,7 +46,7 @@ class LegadoGlideModule : AppGlideModule() {
         builder.setMemorySizeCalculator(calculator)
         builder.setBitmapPool(bitmapPool)
         builder.setDiskCache(InternalCacheDiskCacheFactory(context, 1024 * 1024 * 1000))
-        if (!BuildConfig.DEBUG && !AppConfig.recordLog) {
+        if (!BuildConfig.DEBUG && !otherSettingsGateway.currentSettings.recordLog) {
             builder.setLogLevel(Log.ERROR)
         }
     }

+ 3 - 2
app/src/main/java/io/legado/app/help/http/BackstageWebView.kt

@@ -18,7 +18,6 @@ import io.legado.app.data.entities.BaseSource
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.CacheManager
 import io.legado.app.help.WebCacheManager
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.webView.WebJsExtensions
 import io.legado.app.help.webView.WebJsExtensions.Companion.getInjectionString
@@ -58,6 +57,8 @@ class BackstageWebView(
     private val isRule: Boolean = false,
 ) {
 
+    private val cacheSettingsGateway get() = org.koin.core.context.GlobalContext.get().get<io.legado.app.domain.gateway.DownloadCacheSettingsGateway>()
+
     private val mHandler = Handler(Looper.getMainLooper())
     private var callback: Callback? = null
     private var mWebView: WebView? = null
@@ -138,7 +139,7 @@ class BackstageWebView(
         settings.javaScriptEnabled = true
         settings.domStorageEnabled = true
         settings.blockNetworkImage = true
-        settings.userAgentString = headerMap?.get(AppConst.UA_NAME) ?: AppConfig.userAgent
+        settings.userAgentString = headerMap?.get(AppConst.UA_NAME) ?: cacheSettingsGateway.currentSettings.userAgent
         settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
         settings.cacheMode = if (cacheFirst) {
             WebSettings.LOAD_CACHE_ELSE_NETWORK

+ 4 - 3
app/src/main/java/io/legado/app/help/http/HttpHelper.kt

@@ -2,7 +2,6 @@ package io.legado.app.help.http
 
 import io.legado.app.constant.AppConst
 import io.legado.app.help.CacheManager
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.glide.progress.ProgressManager.LISTENER
 import io.legado.app.help.glide.progress.ProgressResponseBody
 import io.legado.app.help.http.CookieManager.cookieJarHeader
@@ -30,6 +29,8 @@ private val proxyClientCache: ConcurrentHashMap<String, OkHttpClient> by lazy {
     ConcurrentHashMap()
 }
 
+private val cacheSettingsGateway get() = org.koin.core.context.GlobalContext.get().get<io.legado.app.domain.gateway.DownloadCacheSettingsGateway>()
+
 val cookieJar by lazy {
     object : CookieJar {
 
@@ -96,7 +97,7 @@ val okHttpClient: OkHttpClient by lazy {
             val request = chain.request()
             val builder = request.newBuilder()
             if (request.header(AppConst.UA_NAME) == null) {
-                builder.addHeader(AppConst.UA_NAME, AppConfig.userAgent)
+                builder.addHeader(AppConst.UA_NAME, cacheSettingsGateway.currentSettings.userAgent)
             } else if (request.header(AppConst.UA_NAME) == "null") {
                 builder.removeHeader(AppConst.UA_NAME)
             }
@@ -122,7 +123,7 @@ val okHttpClient: OkHttpClient by lazy {
             }
             networkResponse
         }
-    if (AppConfig.isCronet) {
+    if (cacheSettingsGateway.currentSettings.cronetEnabled) {
         if (Cronet.loader?.install() == true) {
             Cronet.interceptor?.let {
                 builder.addInterceptor(it)

+ 2 - 2
app/src/main/java/io/legado/app/help/http/ObsoleteUrlFactory.kt

@@ -1048,7 +1048,7 @@ class ObsoleteUrlFactory(private var client: OkHttpClient) : URLStreamHandlerFac
             "NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS"
         )
         fun format(value: Date?): String {
-            return STANDARD_DATE_FORMAT.get().format(value)
+            return STANDARD_DATE_FORMAT.get().format(value!!)
         }
 
         fun permitsRequestBody(method: String): Boolean {
@@ -1152,7 +1152,7 @@ class ObsoleteUrlFactory(private var client: OkHttpClient) : URLStreamHandlerFac
         fun getSystemProperty(key: String?, defaultValue: String?): String? {
             val value: String?
             try {
-                value = System.getProperty(key)
+                value = System.getProperty(key!!)
             } catch (ex: AccessControlException) {
                 return defaultValue
             }

+ 2 - 0
app/src/main/java/io/legado/app/help/readaloud/playback/SystemTtsFileSynthesizer.kt

@@ -48,10 +48,12 @@ class SystemTtsFileSynthesizer(
                     }
                 }
 
+                @Deprecated("Deprecated in Java")
                 override fun onError(failedId: String?) {
                     if (failedId == utteranceId && continuation.isActive) continuation.resume(false)
                 }
 
+                @Suppress("DEPRECATION")
                 override fun onError(failedId: String?, errorCode: Int) = onError(failedId)
             })
             if (instance.setSpeechRate(speechRate) == TextToSpeech.ERROR ||

+ 1 - 1
app/src/main/java/io/legado/app/help/readaloud/playback/SystemTtsVoiceCatalog.kt

@@ -79,7 +79,7 @@ class SystemTtsVoiceCatalog(context: Context) {
             } else {
                 TextToSpeech(appContext, listener, enginePackage)
             }
-            continuation.invokeOnCancellation { instance?.shutdown() }
+            continuation.invokeOnCancellation { instance.shutdown() }
         }
 
     companion object {

+ 8 - 5
app/src/main/java/io/legado/app/help/storage/Backup.kt

@@ -7,12 +7,12 @@ import androidx.documentfile.provider.DocumentFile
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.PreferKey
 import io.legado.app.data.appDb
+import io.legado.app.domain.gateway.BackupSettingsGateway
 import io.legado.app.domain.gateway.ReadStyleGateway
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.AppWebDav
 import io.legado.app.help.DirectLinkUpload
 import io.legado.app.help.book.isLocal
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfigStore
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.help.config.ReadBookConfig
@@ -53,6 +53,9 @@ object Backup {
     private val readStyleGateway: ReadStyleGateway
         get() = GlobalContext.get().get()
 
+    private val backupSettingsGateway: BackupSettingsGateway
+        get() = GlobalContext.get().get()
+
     val backupPath: String by lazy {
         appCtx.filesDir.getFile("backup").createFolderIfNotExist().absolutePath
     }
@@ -96,8 +99,8 @@ object Backup {
     private fun getNowZipFileName(): String {
         val backupDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
             .format(Date(System.currentTimeMillis()))
-        val deviceName = AppConfig.webDavDeviceName
-        return if (deviceName?.isNotBlank() == true) {
+        val deviceName = backupSettingsGateway.currentSettings.webDavDeviceName
+        return if (deviceName.isNotBlank()) {
             "backup${backupDate}-${deviceName}.zip"
         } else {
             "backup${backupDate}.zip"
@@ -116,7 +119,7 @@ object Backup {
                     if (shouldBackup()) {
                         val backupZipFileName = getNowZipFileName()
                         if (!AppWebDav.hasBackUp(backupZipFileName)) {
-                            backup(context, AppConfig.backupPath)
+                            backup(context, backupSettingsGateway.currentSettings.backupPath)
                         } else {
                             LocalConfig.lastBackup = System.currentTimeMillis()
                         }
@@ -276,7 +279,7 @@ object Backup {
             .map(File::getAbsolutePath)
         FileUtils.delete(zipFilePath)
         FileUtils.delete(zipFilePath.replace("tmp_", ""))
-        val backupFileName = if (AppConfig.onlyLatestBackup) {
+        val backupFileName = if (backupSettingsGateway.currentSettings.onlyLatestBackup) {
             "backup.zip"
         } else {
             zipFileName

+ 1 - 2
app/src/main/java/io/legado/app/help/storage/Restore.kt

@@ -51,7 +51,6 @@ import io.legado.app.help.config.SettingsWriter
 import io.legado.app.help.config.ThemeConfigStore
 import io.legado.app.model.BookCover
 import io.legado.app.model.localBook.LocalBook
-import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.utils.ACache
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.GSON
@@ -382,7 +381,7 @@ object Restore : KoinComponent {
         appCtx.toastOnUi(R.string.restore_success)
         withContext(Main) {
             delay(100)
-            get<AppLocaleGateway>().setLanguage(OtherConfig.language)
+            get<AppLocaleGateway>().apply { setLanguage(currentLanguage) }
             if (!BuildConfig.DEBUG) {
                 LauncherIconHelp.changeIcon(appCtx.getPrefString(PreferKey.launcherIcon))
             }

+ 3 - 2
app/src/main/java/io/legado/app/help/update/AppUpdateGitHub.kt

@@ -2,7 +2,6 @@ package io.legado.app.help.update
 
 import io.legado.app.constant.AppConst
 import io.legado.app.exception.NoStackTraceException
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.http.newCallResponse
 import io.legado.app.help.http.okHttpClient
@@ -21,6 +20,8 @@ import java.time.format.DateTimeFormatter
 
 object AppUpdateGitHub : AppUpdate.AppUpdateInterface {
 
+    private val otherSettingsGateway get() = org.koin.core.context.GlobalContext.get().get<io.legado.app.domain.gateway.OtherSettingsGateway>()
+
     private const val repoPath = "HapeLee/legado-with-MD3"
     private const val githubApiBaseUrl = "https://api.github.com/repos/$repoPath/releases"
     private const val updateManifestBaseUrl =
@@ -28,7 +29,7 @@ object AppUpdateGitHub : AppUpdate.AppUpdateInterface {
     private const val manifestTimeoutMillis = 2500L
 
     private val checkVariant: AppVariant
-        get() = when (AppConfig.updateToVariant) {
+        get() = when (otherSettingsGateway.currentSettings.updateToVariant) {
             "official_version" -> AppVariant.OFFICIAL
             "beta_release_version" -> AppVariant.BETA_RELEASE
             "all_version" -> AppVariant.ALL

+ 3 - 3
app/src/main/java/io/legado/app/lib/cronet/AbsCallBack.kt

@@ -432,7 +432,7 @@ abstract class AbsCallBack(
 
     inner class CronetBodySource : Source {
 
-        private var buffer = ByteBuffer.allocateDirect(32 * 1024)
+        private var buffer: ByteBuffer? = ByteBuffer.allocateDirect(32 * 1024)
         private var closed = false
         private val timeout = readTimeoutMillis.toLong()
 
@@ -460,8 +460,8 @@ abstract class AbsCallBack(
                 return -1
             }
 
-            if (byteCount < buffer.limit()) {
-                buffer.limit(byteCount.toInt())
+            if (byteCount < buffer!!.limit()) {
+                buffer!!.limit(byteCount.toInt())
             }
 
             request?.read(buffer)

+ 1 - 1
app/src/main/java/io/legado/app/lib/cronet/CronetLoader.kt

@@ -132,7 +132,7 @@ object CronetLoader : CronetEngine.Builder.LibraryLoader(), Cronet.LoaderInterfa
         } catch (e: Throwable) {
             //如果找不到,则从远程下载
             //删除历史文件
-            deleteHistoryFile(Objects.requireNonNull(soFile.parentFile), soFile)
+            deleteHistoryFile(soFile.parentFile!!, soFile)
             //md5 = getUrlMd5(md5Url)
             DebugLog.d(javaClass.simpleName, "soMD5:$md5")
             if (md5.length != 32 || soUrl.isEmpty()) {

+ 1 - 2
app/src/main/java/io/legado/app/lib/permission/RequestManager.kt

@@ -34,8 +34,7 @@ internal object RequestManager : OnPermissionsResultCallback {
             if (index >= 0) {
                 val to = it.size - 1
                 if (index != to) {
-                    @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
-                    Collections.swap(requests, index, to)
+                    Collections.swap(it, index, to)
                 }
             } else {
                 it.push(request)

+ 3 - 2
app/src/main/java/io/legado/app/lib/prefs/EditTextPreferenceDialog.kt

@@ -7,13 +7,14 @@ import android.view.WindowManager
 import androidx.preference.EditTextPreferenceDialogFragmentCompat
 import androidx.preference.PreferenceDialogFragmentCompat
 import io.legado.app.R
-import io.legado.app.help.config.AppConfig
 //import io.legado.app.lib.theme.accentColor
 //import io.legado.app.lib.theme.filletBackground
 
 
 class EditTextPreferenceDialog : EditTextPreferenceDialogFragmentCompat() {
 
+    private val themeSettingsGateway get() = org.koin.core.context.GlobalContext.get().get<io.legado.app.domain.gateway.ThemeSettingsGateway>()
+
     companion object {
 
         fun newInstance(key: String): EditTextPreferenceDialog {
@@ -41,7 +42,7 @@ class EditTextPreferenceDialog : EditTextPreferenceDialogFragmentCompat() {
 
     override fun onStart() {
         super.onStart()
-        if (AppConfig.isEInkMode) {
+        if (themeSettingsGateway.currentSettings.appTheme == "4") {
             dialog?.window?.let {
                 it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
                 val attr = it.attributes

+ 3 - 2
app/src/main/java/io/legado/app/lib/prefs/ListPreferenceDialog.kt

@@ -7,13 +7,14 @@ import android.view.WindowManager
 import androidx.preference.ListPreferenceDialogFragmentCompat
 import androidx.preference.PreferenceDialogFragmentCompat
 import io.legado.app.R
-import io.legado.app.help.config.AppConfig
 //import io.legado.app.lib.theme.accentColor
 //import io.legado.app.lib.theme.filletBackground
 import io.legado.app.utils.dpToPx
 
 class ListPreferenceDialog : ListPreferenceDialogFragmentCompat() {
 
+    private val themeSettingsGateway get() = org.koin.core.context.GlobalContext.get().get<io.legado.app.domain.gateway.ThemeSettingsGateway>()
+
     companion object {
 
         fun newInstance(key: String?): ListPreferenceDialog {
@@ -44,7 +45,7 @@ class ListPreferenceDialog : ListPreferenceDialogFragmentCompat() {
 
     override fun onStart() {
         super.onStart()
-        if (AppConfig.isEInkMode) {
+        if (themeSettingsGateway.currentSettings.appTheme == "4") {
             dialog?.window?.let {
                 it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
                 val attr = it.attributes

+ 3 - 2
app/src/main/java/io/legado/app/lib/prefs/MultiSelectListPreferenceDialog.kt

@@ -7,13 +7,14 @@ import android.view.WindowManager
 import androidx.preference.MultiSelectListPreferenceDialogFragmentCompat
 import androidx.preference.PreferenceDialogFragmentCompat
 import io.legado.app.R
-import io.legado.app.help.config.AppConfig
 //import io.legado.app.lib.theme.accentColor
 //import io.legado.app.lib.theme.filletBackground
 import io.legado.app.utils.dpToPx
 
 class MultiSelectListPreferenceDialog : MultiSelectListPreferenceDialogFragmentCompat() {
 
+    private val themeSettingsGateway get() = org.koin.core.context.GlobalContext.get().get<io.legado.app.domain.gateway.ThemeSettingsGateway>()
+
     companion object {
 
         fun newInstance(key: String?): MultiSelectListPreferenceDialog {
@@ -46,7 +47,7 @@ class MultiSelectListPreferenceDialog : MultiSelectListPreferenceDialogFragmentC
 
     override fun onStart() {
         super.onStart()
-        if (AppConfig.isEInkMode) {
+        if (themeSettingsGateway.currentSettings.appTheme == "4") {
             dialog?.window?.let {
                 it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
                 val attr = it.attributes

+ 2 - 3
app/src/main/java/io/legado/app/lib/prefs/ThemeCardPreference.kt

@@ -19,7 +19,6 @@ import com.google.android.material.card.MaterialCardView
 import io.legado.app.R
 import io.legado.app.constant.PreferKey
 import io.legado.app.domain.gateway.ThemeSettingsGateway
-import io.legado.app.ui.config.themeConfig.ThemeConfig
 import io.legado.app.utils.getPrefString
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.activity
@@ -92,8 +91,8 @@ class ThemeCardPreference(context: Context, attrs: AttributeSet) : Preference(co
             holder.card.setOnClickListener {
                 if (value != currentValue) {
                     if (value == "13") {
-                        val hasLightBg = !ThemeConfig.bgImageLight.isNullOrEmpty()
-                        val hasDarkBg = !ThemeConfig.bgImageDark.isNullOrEmpty()
+                        val hasLightBg = !themeSettingsGateway.currentSettings.backgroundImageLight.isNullOrEmpty()
+                        val hasDarkBg = !themeSettingsGateway.currentSettings.backgroundImageDark.isNullOrEmpty()
                         if (!hasLightBg || !hasDarkBg) {
                             context.toastOnUi(R.string.transparent_theme_alarm)
                             return@setOnClickListener

+ 8 - 3
app/src/main/java/io/legado/app/model/AudioPlay.kt

@@ -18,7 +18,8 @@ import io.legado.app.help.book.getBookSource
 import io.legado.app.help.book.readSimulating
 import io.legado.app.help.book.simulatedTotalChapterNum
 import io.legado.app.help.book.update
-import io.legado.app.help.config.AppConfig
+import io.legado.app.domain.gateway.OtherSettingsGateway
+import io.legado.app.domain.gateway.ReadSettingsGateway
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.model.webBook.WebBook
 import io.legado.app.service.AudioPlayService
@@ -28,12 +29,16 @@ import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.MainScope
 import kotlinx.coroutines.cancelChildren
+import org.koin.core.context.GlobalContext
 import splitties.init.appCtx
 
 @SuppressLint("StaticFieldLeak")
 @Suppress("unused")
 object AudioPlay : CoroutineScope by MainScope() {
 
+    private val otherSettingsGateway get() = GlobalContext.get().get<OtherSettingsGateway>()
+    private val readSettingsGateway get() = GlobalContext.get().get<ReadSettingsGateway>()
+
     enum class PlayMode {
         LIST_END_STOP,
         SINGLE_LOOP,
@@ -406,8 +411,8 @@ object AudioPlay : CoroutineScope by MainScope() {
                 appDb.bookChapterDao.getChapter(book.bookUrl, book.durChapterIndex)?.let {
                     book.durChapterTitle = it.getDisplayTitle(
                         ContentProcessor.get(book.name, book.origin).getTitleReplaceRules(),
-                        book.getUseReplaceRule(AppConfig.replaceEnableDefault),
-                        chineseConverterType = AppConfig.chineseConverterType,
+                        book.getUseReplaceRule(otherSettingsGateway.currentSettings.replaceEnableDefault),
+                        chineseConverterType = readSettingsGateway.currentSettings.chineseConverterType,
                     )
                     SourceCallBack.callBackBook(SourceCallBack.SAVE_READ, bookSource, book, it)
                 }

+ 20 - 9
app/src/main/java/io/legado/app/model/BookCover.kt

@@ -19,13 +19,13 @@ import com.bumptech.glide.request.RequestOptions
 import com.bumptech.glide.request.target.Target
 import com.bumptech.glide.request.target.Target.SIZE_ORIGINAL
 import io.legado.app.R
-import io.legado.app.ui.config.coverConfig.CoverConfig
 import io.legado.app.data.entities.BaseSource
 import io.legado.app.data.entities.Book
+import io.legado.app.domain.gateway.AppShellSettingsGateway
+import io.legado.app.domain.gateway.CoverSettingsGateway
+import io.legado.app.domain.gateway.MangaSettingsGateway
 import io.legado.app.help.CacheManager
 import io.legado.app.help.DefaultData
-import io.legado.app.help.config.AppConfig
-import io.legado.app.ui.config.readMangaConfig.ReadMangaConfig
 import io.legado.app.help.glide.BlurTransformation
 import io.legado.app.help.glide.ImageLoader
 import io.legado.app.help.glide.OkHttpModelLoader
@@ -36,6 +36,8 @@ import io.legado.app.domain.usecase.CoverAlbumUseCase
 import io.legado.app.utils.BitmapUtils
 import io.legado.app.utils.GSON
 import io.legado.app.utils.fromJsonObject
+import io.legado.app.utils.isNightMode
+import io.legado.app.utils.sysConfiguration
 import kotlinx.coroutines.currentCoroutineContext
 import org.koin.core.component.KoinComponent
 import org.koin.core.component.inject
@@ -49,11 +51,20 @@ object BookCover : KoinComponent {
     private const val coverRuleConfigKey = "legadoCoverRuleConfig"
     const val configFileName = "coverRule.json"
     private val coverAlbumUseCase: CoverAlbumUseCase by inject()
+    private val shellSettingsGateway: AppShellSettingsGateway by inject()
+    private val coverSettingsGateway: CoverSettingsGateway by inject()
+    private val mangaSettingsGateway: MangaSettingsGateway by inject()
+
+    private val isNightTheme: Boolean
+        get() = when (shellSettingsGateway.currentSettings.themeMode) {
+            "1" -> false
+            "2" -> true
+            else -> sysConfiguration.isNightMode
+        }
 
     val defaultDrawable: Drawable
         @SuppressLint("UseCompatLoadingForDrawables")
         get() {
-            val isNightTheme = AppConfig.isNightTheme
             val paths = coverAlbumUseCase.selectedImagePaths(isNightTheme)
 
             if (paths.isEmpty()) {
@@ -68,7 +79,7 @@ object BookCover : KoinComponent {
 
     fun getRandomDefaultPath(
         seed: Any? = null,
-        isNight: Boolean = AppConfig.isNightTheme
+        isNight: Boolean = isNightTheme
     ): String? {
         val paths = coverAlbumUseCase.selectedImagePaths(isNight)
         if (paths.isEmpty()) return null
@@ -81,7 +92,7 @@ object BookCover : KoinComponent {
 
     fun getRandomDefaultDrawable(
         seed: Any? = null,
-        isNight: Boolean = AppConfig.isNightTheme
+        isNight: Boolean = isNightTheme
     ): Drawable {
         val randomPath = getRandomDefaultPath(seed, isNight)
             ?: return appCtx.resources.getDrawable(R.drawable.image_cover_default, null)
@@ -111,7 +122,7 @@ object BookCover : KoinComponent {
         onLoadFinish: (() -> Unit)? = null,
     ): RequestBuilder<Drawable> {
         val currentDefault = getRandomDefaultDrawable()
-        if (AppConfig.useDefaultCover) {
+        if (coverSettingsGateway.currentSettings.useDefaultCover) {
             return ImageLoader.load(context, currentDefault)
                 .centerCrop()
         }
@@ -175,7 +186,7 @@ object BookCover : KoinComponent {
         if (transformation != null) {
             builder = builder.transform(transformation)
         }
-        builder = if (ReadMangaConfig.disableMangaCrossFade) {
+        builder = if (mangaSettingsGateway.currentSettings.disableMangaCrossFade) {
             builder
         } else {
             builder.transition(DrawableTransitionOptions.withCrossFade())
@@ -214,7 +225,7 @@ object BookCover : KoinComponent {
         val currentDefault = getRandomDefaultDrawable()
         val loadBlur = ImageLoader.load(context, currentDefault)
             .transform(BlurTransformation(25), CenterCrop())
-        if (AppConfig.useDefaultCover) {
+        if (coverSettingsGateway.currentSettings.useDefaultCover) {
             return loadBlur
         }
         var options = RequestOptions().set(OkHttpModelLoader.loadOnlyWifiOption, loadOnlyWifi)

+ 5 - 2
app/src/main/java/io/legado/app/model/CacheBook.kt

@@ -14,8 +14,8 @@ import io.legado.app.model.cache.CacheDownloadRequest
 import io.legado.app.model.cache.CacheDownloadStateStore
 import io.legado.app.model.cache.ChapterSelection
 import io.legado.app.model.cache.ExplicitCacheBookFifo
+import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
 import io.legado.app.service.CacheBookService
-import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.utils.LogUtils
 import io.legado.app.utils.onEachParallel
 import kotlinx.coroutines.CompletableDeferred
@@ -39,6 +39,7 @@ import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withTimeout
+import org.koin.core.context.GlobalContext
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.atomic.AtomicInteger
 import java.util.concurrent.atomic.AtomicLong
@@ -67,13 +68,15 @@ object CacheBook {
         private val processMutex = Mutex()
         private val workingState = MutableStateFlow(true)
 
+        private val cacheSettingsGateway get() = GlobalContext.get().get<DownloadCacheSettingsGateway>()
+
         fun setWorkingState(value: Boolean) {
             workingState.value = value
         }
 
         suspend fun startProcessJob(context: CoroutineContext) = processMutex.withLock {
             setWorkingState(true)
-            val concurrency = OtherConfig.cacheBookThreadCount.coerceIn(1, maxDownloadConcurrency)
+            val concurrency = cacheSettingsGateway.currentSettings.cacheBookThreadCount.coerceIn(1, maxDownloadConcurrency)
             flow {
                 while (currentCoroutineContext().isActive && taskMap.isNotEmpty() && !isPaused) {
                     if (!workingState.value) {

+ 8 - 3
app/src/main/java/io/legado/app/model/ImageProvider.kt

@@ -12,8 +12,9 @@ import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.isEpub
 import io.legado.app.help.book.isMobi
+import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
+import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.help.book.isPdf
-import io.legado.app.help.config.AppConfig
 import io.legado.app.model.localBook.EpubFile
 import io.legado.app.model.localBook.MobiFile
 import io.legado.app.model.localBook.PdfFile
@@ -23,6 +24,7 @@ import io.legado.app.utils.SvgUtils
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.withContext
+import org.koin.core.context.GlobalContext
 import splitties.init.appCtx
 import java.io.File
 import java.io.FileOutputStream
@@ -30,6 +32,9 @@ import kotlin.math.min
 
 object ImageProvider {
 
+    private val cacheSettingsGateway get() = GlobalContext.get().get<DownloadCacheSettingsGateway>()
+    private val otherSettingsGateway get() = GlobalContext.get().get<OtherSettingsGateway>()
+
     private val errorBitmap: Bitmap by lazy {
         BitmapFactory.decodeResource(appCtx.resources, R.drawable.image_loading_error)
     }
@@ -41,7 +46,7 @@ object ImageProvider {
     private const val M = 1024 * 1024
     val cacheSize: Int
         get() {
-            return AppConfig.bitmapCacheSize.takeIf { it in 1..<2048 }
+            return cacheSettingsGateway.currentSettings.bitmapCacheSize.takeIf { it in 1..<2048 }
                 ?.times(M)
                 ?: (50 * M)
         }
@@ -184,7 +189,7 @@ object ImageProvider {
         height: Int? = null
     ): Bitmap {
         //src为空白时 可能被净化替换掉了 或者规则失效
-        if (book.getUseReplaceRule(AppConfig.replaceEnableDefault) && src.isBlank()) {
+        if (book.getUseReplaceRule(otherSettingsGateway.currentSettings.replaceEnableDefault) && src.isBlank()) {
             book.setUseReplaceRule(false)
             appCtx.toastOnUi(R.string.error_image_url_empty)
         }

+ 3 - 3
app/src/main/java/io/legado/app/model/ReadAloud.kt

@@ -15,7 +15,6 @@ import io.legado.app.lib.dialogs.SelectItem
 import io.legado.app.service.BaseReadAloudService
 import io.legado.app.service.HttpReadAloudService
 import io.legado.app.service.TTSReadAloudService
-import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.GSON
 import io.legado.app.utils.LogUtils
 import io.legado.app.utils.StringUtils
@@ -27,8 +26,9 @@ import kotlinx.coroutines.runBlocking
 import splitties.init.appCtx
 
 object ReadAloud {
+    private val aloudSettingsGateway get() = org.koin.core.context.GlobalContext.get().get<io.legado.app.domain.gateway.ReadAloudSettingsGateway>()
     private var aloudClass: Class<*> = getReadAloudClass()
-    val ttsEngine get() = ReadBook.book?.getTtsEngine() ?: ReadConfig.ttsEngine
+    val ttsEngine get() = ReadBook.book?.getTtsEngine() ?: aloudSettingsGateway.currentSettings.ttsEngine
     var httpTTS: HttpTTS? = null
     var coordinatorDefaultEngineType: String = ReadAloudVoice.ENGINE_SYSTEM
         private set
@@ -83,7 +83,7 @@ object ReadAloud {
     }
 
     private fun findCoordinatorHttpSeed(): HttpTTS? {
-        if (!ReadConfig.useMultiSpeaker) return null
+        if (!aloudSettingsGateway.currentSettings.useMultiSpeaker) return null
         return runCatching {
             val bookUrl = ReadBook.book?.bookUrl ?: return@runCatching null
             val boundVoices = runBlocking {

+ 21 - 17
app/src/main/java/io/legado/app/model/ReadBook.kt

@@ -11,6 +11,9 @@ import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.readRecord.ReadRecord
 import io.legado.app.data.entities.readRecord.ReadRecordSession
 import io.legado.app.data.repository.ReadRecordRepository
+import io.legado.app.domain.gateway.BackupSettingsGateway
+import io.legado.app.domain.gateway.OtherSettingsGateway
+import io.legado.app.domain.gateway.ReadSettingsGateway
 import io.legado.app.help.AppWebDav
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.ContentProcessor
@@ -23,7 +26,6 @@ import io.legado.app.help.book.isSameNameAuthor
 import io.legado.app.help.book.readSimulating
 import io.legado.app.help.book.simulatedTotalChapterNum
 import io.legado.app.help.book.update
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.data.repository.HighlightRuleRepository
 import io.legado.app.help.coroutine.Coroutine
@@ -60,7 +62,6 @@ import io.legado.app.ui.book.read.pageestimate.PageEstimateMetrics
 import io.legado.app.ui.book.read.pageestimate.RoomExactChapterPageCountStore
 import io.legado.app.ui.book.read.pageestimate.WholeBookPageCoordinator
 import io.legado.app.ui.book.read.pageestimate.WholeBookPageState
-import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.buildMainHandler
 import io.legado.app.utils.dpToPx
 import io.legado.app.utils.postEvent
@@ -160,6 +161,9 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
     var msg: String? = null
     private val readRecordRepository: ReadRecordRepository by inject()
     private val readAloudSessionStore: ReadAloudSessionStore by inject()
+    private val readSettingsGateway: ReadSettingsGateway by inject()
+    private val otherSettingsGateway: OtherSettingsGateway by inject()
+    private val backupSettingsGateway: BackupSettingsGateway by inject()
     private var lastReadLength: Long = 0
     private val loadingChapters = arrayListOf<Int>()
     private val readRecord = ReadRecord()
@@ -397,8 +401,8 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
                 append(book.config.useReplaceRule)
                 append('|').append(book.config.delTag)
                 append('|').append(book.config.translationMode)
-                append('|').append(AppConfig.chineseConverterType)
-                append('|').append(AppConfig.replaceEnableDefault)
+                append('|').append(readSettingsGateway.currentSettings.chineseConverterType)
+                append('|').append(otherSettingsGateway.currentSettings.replaceEnableDefault)
                 append('|').append(processor.getTitleReplaceRules())
                 append('|').append(processor.getContentReplaceRules())
             },
@@ -467,7 +471,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
         if (!wholeBookPageCoordinator.awaitInitialized(layoutGeneration)) return
         val chapters = withContext(IO) { appDb.bookChapterDao.getChapterList(book.bookUrl) }
         val processor = ContentProcessor.get(book)
-        val useReplaceRule = book.getUseReplaceRule(AppConfig.replaceEnableDefault)
+        val useReplaceRule = book.getUseReplaceRule(otherSettingsGateway.currentSettings.replaceEnableDefault)
         val highlightRules = HighlightRuleRepository().loadEnabled(ReadBookConfig.durConfig.name)
         for (chapter in chapters) {
             ensureActive()
@@ -476,7 +480,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
             val displayTitle = chapter.getDisplayTitle(
                 processor.getTitleReplaceRules(),
                 useReplaceRule,
-                chineseConverterType = AppConfig.chineseConverterType,
+                chineseConverterType = readSettingsGateway.currentSettings.chineseConverterType,
             )
             val processed = processor.getContent(book, chapter, content, includeTitle = false)
             wholeBookPageCoordinator.updateChapterContent(
@@ -497,7 +501,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
                 title = displayTitle,
                 paragraphs = processed.textList,
                 includeTitle = ReadBookConfig.titleMode != 2 || chapter.isVolume || processed.textList.isEmpty(),
-                adaptSpecialStyle = ReadConfig.adaptSpecialStyle,
+                adaptSpecialStyle = readSettingsGateway.currentSettings.adaptSpecialStyle,
                 htmlSemanticTextResolver = AndroidReaderHtmlSemanticTextResolver,
             )
             val environment = readerPaginationEnvironment ?: return
@@ -660,7 +664,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
                     ConfigUpdateAction.RebuildWholeBookPageIndex,
                 )
             )
-            if (ReadConfig.readBarStyleFollowPage) {
+            if (readSettingsGateway.currentSettings.readBarStyleFollowPage) {
                 postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
             }
         }
@@ -774,7 +778,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
         uploadSuccessAction: (() -> Unit)? = null,
         syncSuccessAction: (() -> Unit)? = null
     ) {
-        if (!ReadConfig.syncBookProgress) return
+        if (!backupSettingsGateway.currentSettings.syncBookProgress) return
         val book = book ?: return
         Coroutine.async {
             AppWebDav.getBookProgress(book)
@@ -1541,8 +1545,8 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
         val contentProcessor = ContentProcessor.get(book.name, book.origin)
         val displayTitle = chapter.getDisplayTitle(
             contentProcessor.getTitleReplaceRules(),
-            book.getUseReplaceRule(AppConfig.replaceEnableDefault),
-            chineseConverterType = AppConfig.chineseConverterType,
+            book.getUseReplaceRule(otherSettingsGateway.currentSettings.replaceEnableDefault),
+            chineseConverterType = readSettingsGateway.currentSettings.chineseConverterType,
         )
         val contents = contentProcessor
             .getContent(book, chapter, content, includeTitle = false)
@@ -1565,7 +1569,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
             title = displayTitle,
             paragraphs = contents.textList,
             includeTitle = ReadBookConfig.titleMode != 2 || chapter.isVolume || contents.textList.isEmpty(),
-            adaptSpecialStyle = ReadConfig.adaptSpecialStyle,
+            adaptSpecialStyle = readSettingsGateway.currentSettings.adaptSpecialStyle,
             htmlSemanticTextResolver = AndroidReaderHtmlSemanticTextResolver,
         )
         val readerChapterInput = ReaderChapterInput(
@@ -1706,8 +1710,8 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
                     appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex)?.let {
                         book.durChapterTitle = it.getDisplayTitle(
                             ContentProcessor.get(book.name, book.origin).getTitleReplaceRules(),
-                            book.getUseReplaceRule(AppConfig.replaceEnableDefault),
-                            chineseConverterType = AppConfig.chineseConverterType,
+                            book.getUseReplaceRule(otherSettingsGateway.currentSettings.replaceEnableDefault),
+                            chineseConverterType = readSettingsGateway.currentSettings.chineseConverterType,
                         )
                         SourceCallBack.callBackBook(SourceCallBack.SAVE_READ, bookSource, book, it)
                     }
@@ -1742,7 +1746,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
     private fun preDownload() {
         if (book?.isLocal == true) return
         executor.execute {
-            if (ReadConfig.preDownloadNum < 2) {
+            if (readSettingsGateway.currentSettings.preDownloadNum < 2) {
                 return@execute
             }
             preDownloadTask?.cancel()
@@ -1750,7 +1754,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
                 //预下载
                 launch {
                     val maxChapterIndex =
-                        min(durChapterIndex + ReadConfig.preDownloadNum, chapterSize)
+                        min(durChapterIndex + readSettingsGateway.currentSettings.preDownloadNum, chapterSize)
                     for (i in durChapterIndex.plus(2)..maxChapterIndex) {
                         if (downloadedChapters.contains(i)) continue
                         if ((downloadFailChapters[i] ?: 0) >= 3) continue
@@ -1758,7 +1762,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
                     }
                 }
                 launch {
-                    val minChapterIndex = durChapterIndex - min(5, ReadConfig.preDownloadNum)
+                    val minChapterIndex = durChapterIndex - min(5, readSettingsGateway.currentSettings.preDownloadNum)
                     for (i in durChapterIndex.minus(2) downTo minChapterIndex) {
                         if (downloadedChapters.contains(i)) continue
                         if ((downloadFailChapters[i] ?: 0) >= 3) continue

+ 4 - 2
app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt

@@ -14,10 +14,10 @@ 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.domain.gateway.DownloadCacheSettingsGateway
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.CacheManager
 import io.legado.app.help.JsExtensions
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.http.BackstageWebView
 import io.legado.app.help.http.CookieStore
 import io.legado.app.help.source.getShareScope
@@ -42,6 +42,7 @@ import org.apache.commons.text.StringEscapeUtils
 import org.jsoup.nodes.Node
 import org.mozilla.javascript.NativeObject
 import org.mozilla.javascript.Scriptable
+import org.koin.core.context.GlobalContext
 import java.lang.ref.WeakReference
 import java.net.URL
 import java.util.Locale
@@ -78,6 +79,7 @@ class AnalyzeRule(
 
     private val stringRuleCache = hashMapOf<String, List<SourceRule>>()
     private val regexCache = hashMapOf<String, Regex?>()
+    private val cacheSettingsGateway get() = GlobalContext.get().get<DownloadCacheSettingsGateway>()
     private val scriptCache = hashMapOf<String, CompiledScript>()
     private var topScopeRef: WeakReference<Scriptable>? = null
     private var evalJSCallCount = 0
@@ -175,7 +177,7 @@ class AnalyzeRule(
                 url = baseUrl,
                 html = content.toString(),
                 javaScript = jsStr,
-                headerMap = getSource()?.getHeaderMap(AppConfig.userAgent, true),
+                headerMap = getSource()?.getHeaderMap(cacheSettingsGateway.currentSettings.userAgent, true),
                 tag = getSource()?.getKey(),
                 cacheFirst = true,
                 timeout = 10000,

+ 6 - 4
app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt

@@ -13,11 +13,11 @@ import io.legado.app.constant.AppPattern
 import io.legado.app.data.entities.BaseSource
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
+import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.CacheManager
 import io.legado.app.help.ConcurrentRateLimiter
 import io.legado.app.help.JsExtensions
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.crypto.toHexString
 import io.legado.app.help.exoplayer.ExoPlayerHelper
 import io.legado.app.help.glide.GlideHeaders
@@ -57,6 +57,7 @@ import okhttp3.Request
 import okhttp3.RequestBody.Companion.toRequestBody
 import okhttp3.Response
 import okhttp3.ResponseBody.Companion.toResponseBody
+import org.koin.core.context.GlobalContext
 import java.io.ByteArrayInputStream
 import java.io.InputStream
 import java.net.URLEncoder
@@ -118,6 +119,7 @@ class AnalyzeUrl(
     private val domain: String
     private var webViewDelayTime: Long = 0
     private val concurrentRateLimiter = ConcurrentRateLimiter(source)
+    private val cacheSettingsGateway get() = GlobalContext.get().get<DownloadCacheSettingsGateway>()
 
     // 服务器ID
     var serverID: Long? = null
@@ -128,7 +130,7 @@ class AnalyzeUrl(
         val urlMatch = paramPattern.find(baseUrl)
         if (urlMatch != null) baseUrl = baseUrl.substring(0, urlMatch.range.first)
         (headerMapF ?: runScriptWithContext(coroutineContext) {
-            source?.getHeaderMap(AppConfig.userAgent, hasLoginHeader)
+            source?.getHeaderMap(cacheSettingsGateway.currentSettings.userAgent, hasLoginHeader)
         })?.let {
             headerMap.putAll(it)
             if (it.containsKey("proxy")) {
@@ -599,7 +601,7 @@ class AnalyzeUrl(
         if (readTimeout == null && callTimeout == null && dnsIp == null) {
             return client
         }
-        if (AppConfig.isCronet && dnsIp != null) {
+        if (cacheSettingsGateway.currentSettings.cronetEnabled && dnsIp != null) {
             customIp[urlNoQuery] = dnsIp!!
         }
         return client.newBuilder().run {
@@ -755,7 +757,7 @@ class AnalyzeUrl(
     }
 
     fun getUserAgent(): String {
-        return headerMap.get(UA_NAME, true) ?: AppConfig.userAgent
+        return headerMap.get(UA_NAME, true) ?: cacheSettingsGateway.currentSettings.userAgent
     }
 
     fun isPost(): Boolean {

+ 17 - 11
app/src/main/java/io/legado/app/model/localBook/LocalBook.kt

@@ -18,6 +18,9 @@ import io.legado.app.exception.EmptyFileException
 import io.legado.app.exception.NoBooksDirException
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.exception.TocEmptyException
+import io.legado.app.domain.gateway.ImportBookSettingsGateway
+import io.legado.app.domain.gateway.OtherSettingsGateway
+import io.legado.app.domain.gateway.ReadSettingsGateway
 import io.legado.app.help.AppWebDav
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.ContentProcessor
@@ -36,12 +39,10 @@ import io.legado.app.help.book.isUmd
 import io.legado.app.help.book.removeLocalUriCache
 import io.legado.app.help.book.simulatedTotalChapterNum
 import io.legado.app.help.book.upKind
-import io.legado.app.help.config.AppConfig
 import io.legado.app.lib.webdav.WebDav
 import io.legado.app.lib.webdav.WebDavException
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.analyzeRule.CustomUrl
-import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.utils.ArchiveUtils
 import io.legado.app.utils.FileDoc
 import io.legado.app.utils.FileUtils
@@ -58,6 +59,7 @@ import io.legado.app.utils.printOnDebug
 import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.runBlocking
 import org.apache.commons.text.StringEscapeUtils
+import org.koin.core.context.GlobalContext
 import splitties.init.appCtx
 import java.io.ByteArrayInputStream
 import java.io.File
@@ -72,6 +74,10 @@ import java.io.InputStream
  */
 object LocalBook {
 
+    private val otherSettingsGateway get() = GlobalContext.get().get<OtherSettingsGateway>()
+    private val readSettingsGateway get() = GlobalContext.get().get<ReadSettingsGateway>()
+    private val importBookSettingsGateway get() = GlobalContext.get().get<ImportBookSettingsGateway>()
+
     private val nameAuthorPatterns = arrayOf(
         Regex("(.*?)《([^《》]+)》.*?作者:(.*)"),
         Regex("(.*?)《([^《》]+)》(.*)"),
@@ -157,15 +163,15 @@ object LocalBook {
         book.durChapterTitle = list.getOrElse(book.durChapterIndex) { list.last() }
             .getDisplayTitle(
                 replaceRules,
-                book.getUseReplaceRule(AppConfig.replaceEnableDefault),
-                chineseConverterType = AppConfig.chineseConverterType,
+                book.getUseReplaceRule(otherSettingsGateway.currentSettings.replaceEnableDefault),
+                chineseConverterType = readSettingsGateway.currentSettings.chineseConverterType,
             )
         book.latestChapterTitle =
             list.getOrElse(book.simulatedTotalChapterNum() - 1) { list.last() }
                 .getDisplayTitle(
                     replaceRules,
-                    book.getUseReplaceRule(AppConfig.replaceEnableDefault),
-                    chineseConverterType = AppConfig.chineseConverterType,
+                    book.getUseReplaceRule(otherSettingsGateway.currentSettings.replaceEnableDefault),
+                    chineseConverterType = readSettingsGateway.currentSettings.chineseConverterType,
                 )
         book.totalChapterNum = list.size
         book.latestChapterTime = System.currentTimeMillis()
@@ -385,11 +391,11 @@ object LocalBook {
         val tempFileName = fileName.substringBeforeLast(".")
         var name = ""
         var author = ""
-        if (!AppConfig.bookImportFileName.isNullOrBlank()) {
+        if (!importBookSettingsGateway.currentSettings.bookImportFileName.isNullOrBlank()) {
             try {
                 //在用户脚本后添加捕获author、name的代码,只要脚本中author、name有值就会被捕获
                 val js =
-                    AppConfig.bookImportFileName + "\nJSON.stringify({author:author,name:name})"
+                    importBookSettingsGateway.currentSettings.bookImportFileName + "\nJSON.stringify({author:author,name:name})"
                 //在脚本中定义如何分解文件名成书名、作者名
                 val jsonStr = RhinoScriptEngine.run {
                     val bindings = ScriptBindings()
@@ -446,7 +452,7 @@ object LocalBook {
         fileName: String,
         source: BaseSource? = null,
     ): Uri {
-        OtherConfig.defaultBookTreeUri
+        otherSettingsGateway.currentSettings.defaultBookTreeUri
             ?: throw NoBooksDirException()
         val inputStream = when {
             str.isAbsUrl() -> AnalyzeUrl(
@@ -472,7 +478,7 @@ object LocalBook {
         fileName: String
     ): Uri {
         inputStream.use {
-            val defaultBookTreeUri = OtherConfig.defaultBookTreeUri
+            val defaultBookTreeUri = otherSettingsGateway.currentSettings.defaultBookTreeUri
             if (defaultBookTreeUri.isNullOrBlank()) throw NoBooksDirException()
             val treeUri = defaultBookTreeUri.toUri()
             return if (treeUri.isContentScheme()) {
@@ -526,7 +532,7 @@ object LocalBook {
         val webDavUrl = localBook.getRemoteUrl()
         if (webDavUrl.isNullOrBlank()) throw NoStackTraceException("Book file is not webDav File")
         try {
-            OtherConfig.defaultBookTreeUri
+            otherSettingsGateway.currentSettings.defaultBookTreeUri
                 ?: throw NoBooksDirException()
             // 兼容旧版链接
             val webdav: WebDav = kotlin.runCatching {

+ 6 - 3
app/src/main/java/io/legado/app/model/localBook/TextFile.kt

@@ -5,11 +5,11 @@ import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.TxtTocRule
+import io.legado.app.domain.gateway.ReadSettingsGateway
 import io.legado.app.exception.EmptyFileException
 import io.legado.app.help.DefaultData
 import io.legado.app.help.book.isLocalModified
 import io.legado.app.help.book.upKind
-import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.EncodingDetect
 import io.legado.app.utils.MD5Utils
 import io.legado.app.utils.StringUtils
@@ -18,9 +18,12 @@ import java.io.FileNotFoundException
 import java.nio.charset.Charset
 import java.util.regex.PatternSyntaxException
 import kotlin.math.min
+import org.koin.core.context.GlobalContext
 
 class TextFile(private var book: Book) {
 
+    private val readSettingsGateway get() = GlobalContext.get().get<ReadSettingsGateway>()
+
     @Suppress("ConstPropertyName")
     companion object {
         private val padRegex = "^[\\n\\s]+".toRegex()
@@ -359,7 +362,7 @@ class TextFile(private var book: Book) {
         val toc = arrayListOf<BookChapter>()
         var bookWordCount = 0
         val maxByteLengthWithNoToc = maxLengthWithNoTocBytes(
-            charset, ReadConfig.maxLengthWithNoToc
+            charset, readSettingsGateway.currentSettings.maxLengthWithNoToc
         )
         LocalBook.getBookInputStream(book).use { bis ->
             //block的个数
@@ -511,7 +514,7 @@ class TextFile(private var book: Book) {
         return try {
             Regex(volumeRule, RegexOption.MULTILINE)
         } catch (e: PatternSyntaxException) {
-            AppLog.put("TXT分卷规则正则语法错误:${rule?.name}\n$e", e)
+            AppLog.put("TXT分卷规则正则语法错误:${rule.name}\n$e", e)
             null
         }
     }

+ 5 - 2
app/src/main/java/io/legado/app/model/remote/RemoteBookWebDav.kt

@@ -1,11 +1,11 @@
 package io.legado.app.model.remote
 
-import io.legado.app.ui.config.otherConfig.OtherConfig
 import android.net.Uri
 import io.legado.app.constant.AppPattern.archiveFileRegex
 import io.legado.app.constant.AppPattern.bookFileRegex
 import io.legado.app.constant.BookType
 import io.legado.app.data.entities.Book
+import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.book.update
 import io.legado.app.lib.webdav.Authorization
@@ -16,6 +16,7 @@ import io.legado.app.model.localBook.LocalBook
 import io.legado.app.utils.NetworkUtils
 import io.legado.app.utils.isContentScheme
 import kotlinx.coroutines.runBlocking
+import org.koin.core.context.GlobalContext
 
 class RemoteBookWebDav(
     val rootBookUrl: String,
@@ -23,6 +24,8 @@ class RemoteBookWebDav(
     val serverID: Long? = null
 ) : RemoteBookManager() {
 
+    private val otherSettingsGateway get() = GlobalContext.get().get<OtherSettingsGateway>()
+
     init {
         runBlocking {
             WebDav(rootBookUrl, authorization).makeAsDir()
@@ -57,7 +60,7 @@ class RemoteBookWebDav(
     }
 
     override suspend fun downloadRemoteBook(remoteBook: RemoteBook): Uri {
-        OtherConfig.defaultBookTreeUri
+        otherSettingsGateway.currentSettings.defaultBookTreeUri
             ?: throw NoStackTraceException("没有设置书籍保存位置!")
         if (!NetworkUtils.isAvailable()) throw NoStackTraceException("网络不可用")
         val webdav = WebDav(remoteBook.path, authorization)

+ 13 - 7
app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt

@@ -9,21 +9,23 @@ 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.rule.TocRule
+import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
+import io.legado.app.domain.gateway.OtherSettingsGateway
+import io.legado.app.domain.gateway.ReadSettingsGateway
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.exception.TocEmptyException
 import io.legado.app.help.book.ContentProcessor
 import io.legado.app.help.book.simulatedTotalChapterNum
-import io.legado.app.help.config.AppConfig
 import io.legado.app.model.Debug
 import io.legado.app.model.analyzeRule.AnalyzeRule
 import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter
 import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
 import io.legado.app.model.analyzeRule.AnalyzeUrl
-import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.utils.isTrue
 import io.legado.app.utils.mapAsync
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.flow.flow
+import org.koin.core.context.GlobalContext
 import org.mozilla.javascript.Context
 import splitties.init.appCtx
 import kotlin.coroutines.coroutineContext
@@ -33,6 +35,10 @@ import kotlin.coroutines.coroutineContext
  */
 object BookChapterList {
 
+    private val cacheSettingsGateway get() = GlobalContext.get().get<DownloadCacheSettingsGateway>()
+    private val otherSettingsGateway get() = GlobalContext.get().get<OtherSettingsGateway>()
+    private val readSettingsGateway get() = GlobalContext.get().get<ReadSettingsGateway>()
+
     suspend fun analyzeChapterList(
         bookSource: BookSource,
         book: Book,
@@ -97,7 +103,7 @@ object BookChapterList {
                     for (urlStr in chapterData.second) {
                         emit(urlStr)
                     }
-                }.mapAsync(OtherConfig.threadCount) { urlStr ->
+                }.mapAsync(cacheSettingsGateway.currentSettings.threadCount) { urlStr ->
                     val analyzeUrl = AnalyzeUrl(
                         mUrl = urlStr,
                         source = bookSource,
@@ -155,8 +161,8 @@ object BookChapterList {
         book.durChapterTitle = list.getOrElse(book.durChapterIndex) { list.last() }
             .getDisplayTitle(
                 replaceRules,
-                book.getUseReplaceRule(AppConfig.replaceEnableDefault),
-                chineseConverterType = AppConfig.chineseConverterType,
+                book.getUseReplaceRule(otherSettingsGateway.currentSettings.replaceEnableDefault),
+                chineseConverterType = readSettingsGateway.currentSettings.chineseConverterType,
             )
         if (book.totalChapterNum < list.size) {
             book.lastCheckCount = list.size - book.totalChapterNum
@@ -168,8 +174,8 @@ object BookChapterList {
             list.getOrElse(book.simulatedTotalChapterNum() - 1) { list.last() }
                 .getDisplayTitle(
                     replaceRules,
-                    book.getUseReplaceRule(AppConfig.replaceEnableDefault),
-                    chineseConverterType = AppConfig.chineseConverterType,
+                    book.getUseReplaceRule(otherSettingsGateway.currentSettings.replaceEnableDefault),
+                    chineseConverterType = readSettingsGateway.currentSettings.chineseConverterType,
                 )
         coroutineContext.ensureActive()
         preserveChapterMetadata(list, book)

+ 7 - 4
app/src/main/java/io/legado/app/model/webBook/BookContent.kt

@@ -7,20 +7,20 @@ 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.rule.ContentRule
+import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
+import io.legado.app.domain.gateway.ReadSettingsGateway
 import io.legado.app.exception.ContentEmptyException
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.isAudio
 import io.legado.app.help.book.isOnLineTxt
 import io.legado.app.help.book.isVideo
-import io.legado.app.help.config.AppConfig
 import io.legado.app.model.Debug
 import io.legado.app.model.analyzeRule.AnalyzeRule
 import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter
 import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
 import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setNextChapterUrl
 import io.legado.app.model.analyzeRule.AnalyzeUrl
-import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.utils.HtmlFormatter
 import io.legado.app.utils.NetworkUtils
 import io.legado.app.utils.mapAsync
@@ -28,6 +28,7 @@ import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.flow.flow
 import org.apache.commons.text.StringEscapeUtils
+import org.koin.core.context.GlobalContext
 import splitties.init.appCtx
 import kotlin.coroutines.coroutineContext
 
@@ -37,6 +38,8 @@ import kotlin.coroutines.coroutineContext
 object BookContent {
 
     private const val maxNextPageConcurrency = 4
+    private val cacheSettingsGateway get() = GlobalContext.get().get<DownloadCacheSettingsGateway>()
+    private val readSettingsGateway get() = GlobalContext.get().get<ReadSettingsGateway>()
 
     @Throws(Exception::class)
     suspend fun analyzeContent(
@@ -119,7 +122,7 @@ object BookContent {
                 for (urlStr in contentData.second) {
                     emit(urlStr)
                 }
-            }.mapAsync(OtherConfig.threadCount.coerceIn(1, maxNextPageConcurrency)) { urlStr ->
+            }.mapAsync(cacheSettingsGateway.currentSettings.threadCount.coerceIn(1, maxNextPageConcurrency)) { urlStr ->
                 val analyzeUrl = AnalyzeUrl(
                     mUrl = urlStr,
                     source = bookSource,
@@ -231,7 +234,7 @@ object BookContent {
         var content = analyzeRule.getString(contentRule.content, unescape = false)
         if (!book.isAudio && !book.isVideo) { //音频和视频获取的是链接,不需要html格式化 && !book.isVideo
             val useHtmlMap = mutableMapOf<String, String>()
-            if (AppConfig.adaptSpecialStyle) {
+            if (readSettingsGateway.currentSettings.adaptSpecialStyle) {
                 content = AppPattern.useHtmlRegex.replace(content) {
                     val placeholder = "{usehtml_${useHtmlMap.size}}"
                     useHtmlMap[placeholder] = it.value

+ 6 - 3
app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt

@@ -7,8 +7,8 @@ import android.view.KeyEvent
 import io.legado.app.constant.EventBus
 import io.legado.app.constant.PreferKey
 import io.legado.app.data.appDb
+import io.legado.app.domain.gateway.ReadAloudSettingsGateway
 import io.legado.app.help.LifecycleHelp
-import io.legado.app.help.config.AppConfig
 import io.legado.app.model.AudioPlay
 import io.legado.app.model.ReadAloud
 import io.legado.app.model.ReadBook
@@ -18,6 +18,7 @@ import io.legado.app.ui.main.MainActivity
 import io.legado.app.utils.LogUtils
 import io.legado.app.utils.getPrefBoolean
 import io.legado.app.utils.postEvent
+import org.koin.core.context.GlobalContext
 
 
 /**
@@ -36,6 +37,8 @@ class MediaButtonReceiver : BroadcastReceiver() {
 
         private const val TAG = "MediaButtonReceiver"
 
+        private val aloudSettingsGateway get() = GlobalContext.get().get<ReadAloudSettingsGateway>()
+
         fun handleIntent(context: Context, intent: Intent): Boolean {
             val intentAction = intent.action
             if (Intent.ACTION_MEDIA_BUTTON == intentAction) {
@@ -90,14 +93,14 @@ class MediaButtonReceiver : BroadcastReceiver() {
                     }
                 }
 
-                isMediaKey && !AppConfig.readAloudByMediaButton -> {
+                isMediaKey && !aloudSettingsGateway.currentSettings.readAloudByMediaButton -> {
                     // break
                 }
 
                 MainActivity.hasActiveReadBookRoute || MainActivity.hasActiveAudioPlayRoute ->
                     postEvent(EventBus.MEDIA_BUTTON, true)
 
-                else -> if (AppConfig.mediaButtonOnExit || LifecycleHelp.activitySize() > 0 || !isMediaKey) {
+                else -> if (aloudSettingsGateway.currentSettings.mediaButtonOnExit || LifecycleHelp.activitySize() > 0 || !isMediaKey) {
                     ReadAloud.upReadAloudClass()
                     if (ReadBook.book != null) {
                         ReadBook.readAloud()

+ 10 - 7
app/src/main/java/io/legado/app/service/AudioPlayService.kt

@@ -31,9 +31,10 @@ import io.legado.app.constant.EventBus
 import io.legado.app.constant.IntentAction
 import io.legado.app.constant.NotificationId
 import io.legado.app.constant.Status
+import io.legado.app.domain.gateway.OtherSettingsGateway
+import io.legado.app.domain.gateway.ReadAloudSettingsGateway
 import io.legado.app.domain.model.PlaybackTimer
 import io.legado.app.help.MediaHelp
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.exoplayer.ExoPlayerHelper
 import io.legado.app.help.glide.ImageLoader
@@ -41,7 +42,6 @@ import io.legado.app.model.AudioPlay
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.analyzeRule.AnalyzeUrl.Companion.getMediaItem
 import io.legado.app.receiver.MediaButtonReceiver
-import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.ui.main.MainActivity
 import io.legado.app.utils.activityPendingIntent
 import io.legado.app.utils.postEvent
@@ -53,6 +53,7 @@ import kotlinx.coroutines.Job
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
+import org.koin.core.context.GlobalContext
 import splitties.init.appCtx
 import splitties.systemservices.audioManager
 import splitties.systemservices.notificationManager
@@ -96,7 +97,9 @@ class AudioPlayService : BaseService(),
         private const val APP_ACTION_TIMER = "Timer"
     }
 
-    private val useWakeLock = AppConfig.audioPlayUseWakeLock
+    private val otherSettingsGateway get() = GlobalContext.get().get<OtherSettingsGateway>()
+    private val aloudSettingsGateway get() = GlobalContext.get().get<ReadAloudSettingsGateway>()
+    private val useWakeLock = otherSettingsGateway.currentSettings.audioPlayUseWakeLock
     private val wakeLock by lazy {
         powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "legado:AudioPlayService")
             .apply {
@@ -515,7 +518,7 @@ class AudioPlayService : BaseService(),
     @SuppressLint("UnspecifiedImmutableFlag")
     private fun initMediaSession() {
         mediaSessionCompat = MediaSessionCompat(this, "readAloud")
-        if (ReadConfig.systemMediaControlCompatibilityChange) {
+        if (aloudSettingsGateway.currentSettings.systemMediaControlCompatibilityChange) {
             mediaSessionCompat?.setCallback(object : MediaSessionCompat.Callback() {
                 override fun onSeekTo(pos: Long) {
                     position = pos.toInt()
@@ -580,7 +583,7 @@ class AudioPlayService : BaseService(),
      * 音频焦点变化
      */
     override fun onAudioFocusChange(focusChange: Int) {
-        if (ReadConfig.ignoreAudioFocus) {
+        if (aloudSettingsGateway.currentSettings.ignoreAudioFocus) {
             AppLog.put("忽略音频焦点处理(有声)")
             return
         }
@@ -680,7 +683,7 @@ class AudioPlayService : BaseService(),
     private fun choiceMediaStyle(): androidx.media.app.NotificationCompat.MediaStyle {
         val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle()
         mediaStyle.setShowActionsInCompactView(1,2,4)
-        if (ReadConfig.systemMediaControlCompatibilityChange) {
+        if (aloudSettingsGateway.currentSettings.systemMediaControlCompatibilityChange) {
             //fix #4090 android 14 can not show play control in lock screen
             mediaStyle.setMediaSession(mediaSessionCompat?.sessionToken)
         }
@@ -717,7 +720,7 @@ class AudioPlayService : BaseService(),
      * @return 音频焦点
      */
     private fun requestFocus(): Boolean {
-        if (ReadConfig.ignoreAudioFocus) {
+        if (aloudSettingsGateway.currentSettings.ignoreAudioFocus) {
             return true
         }
         return MediaHelp.requestFocus(mFocusRequest)

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

@@ -10,6 +10,7 @@ import io.legado.app.constant.AppLog
 import io.legado.app.constant.IntentAction
 import io.legado.app.constant.NotificationId
 import io.legado.app.data.appDb
+import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
 import io.legado.app.help.book.update
 import io.legado.app.model.CacheBook
 import io.legado.app.model.CacheBookModel
@@ -18,7 +19,6 @@ import io.legado.app.model.cache.CacheDownloadRequest
 import io.legado.app.model.cache.CacheDownloadSource
 import io.legado.app.model.cache.ChapterSelection
 import io.legado.app.model.webBook.WebBook
-import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.ui.main.MainActivity
 import io.legado.app.utils.LogUtils
 import io.legado.app.utils.activityPendingIntent
@@ -37,6 +37,7 @@ import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
+import org.koin.core.context.GlobalContext
 import splitties.init.appCtx
 import splitties.systemservices.notificationManager
 import java.util.concurrent.ConcurrentHashMap
@@ -58,7 +59,8 @@ class CacheBookService : BaseService() {
             private set
     }
 
-    private val threadCount = OtherConfig.cacheBookThreadCount.coerceIn(1, CacheBook.maxDownloadConcurrency)
+    private val cacheSettingsGateway get() = GlobalContext.get().get<DownloadCacheSettingsGateway>()
+    private val threadCount = cacheSettingsGateway.currentSettings.cacheBookThreadCount.coerceIn(1, CacheBook.maxDownloadConcurrency)
     // 显式离线缓存:书籍级 FIFO,同时只准入一本;单书内仍用 threadCount 并发章节
     private val maxActiveBookCount = 1
     private val admissionQueue = CacheDownloadAdmissionQueue(maxActiveBookCount)

+ 9 - 8
app/src/main/java/io/legado/app/ui/about/AboutViewModel.kt

@@ -7,9 +7,9 @@ import androidx.lifecycle.viewModelScope
 import io.legado.app.R
 import io.legado.app.base.BaseViewModel
 import io.legado.app.constant.AppLog
+import io.legado.app.domain.gateway.BackupSettingsGateway
 import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.help.CrashHandler
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.update.AppUpdate
 import io.legado.app.utils.FileDoc
 import io.legado.app.utils.FileUtils
@@ -38,7 +38,8 @@ import java.io.FileFilter
 
 class AboutViewModel(
     application: Application,
-    otherSettingsGateway: OtherSettingsGateway,
+    private val otherSettingsGateway: OtherSettingsGateway,
+    private val backupSettingsGateway: BackupSettingsGateway,
 ) : BaseViewModel(application) {
 
     private val _uiState = MutableStateFlow(
@@ -134,7 +135,7 @@ class AboutViewModel(
             context.externalCacheDir
                 ?.getFile("crash")
                 ?.let { FileUtils.delete(it, false) }
-            val backupPath = AppConfig.backupPath
+            val backupPath = backupSettingsGateway.currentSettings.backupPath
             if (!backupPath.isNullOrEmpty()) {
                 val uri = Uri.parse(backupPath)
                 FileDoc.fromUri(uri, true)
@@ -158,7 +159,7 @@ class AboutViewModel(
             ?.getFile("crash")
             ?.listFiles(FileFilter { it.isFile })
             ?.forEach { list.add(FileDoc.fromFile(it)) }
-        val backupPath = AppConfig.backupPath
+        val backupPath = backupSettingsGateway.currentSettings.backupPath
         if (!backupPath.isNullOrEmpty()) {
             val uri = Uri.parse(backupPath)
             FileDoc.fromUri(uri, true)
@@ -171,11 +172,11 @@ class AboutViewModel(
 
     private fun saveLog() {
         execute {
-            val backupPath = AppConfig.backupPath ?: run {
+            val backupPath = backupSettingsGateway.currentSettings.backupPath ?: run {
                 _effects.tryEmit(AboutEffect.ShowToast(context.getString(R.string.about_backup_dir_not_set)))
                 return@execute
             }
-            if (!AppConfig.recordLog) {
+            if (!otherSettingsGateway.currentSettings.recordLog) {
                 _effects.tryEmit(AboutEffect.ShowToast(context.getString(R.string.about_log_recording_disabled)))
                 delay(3000)
             }
@@ -190,11 +191,11 @@ class AboutViewModel(
 
     private fun createHeapDump() {
         execute {
-            val backupPath = AppConfig.backupPath ?: run {
+            val backupPath = backupSettingsGateway.currentSettings.backupPath ?: run {
                 _effects.tryEmit(AboutEffect.ShowToast(context.getString(R.string.about_backup_dir_not_set)))
                 return@execute
             }
-            if (!AppConfig.recordHeapDump) {
+            if (!otherSettingsGateway.currentSettings.recordHeapDump) {
                 _effects.tryEmit(AboutEffect.ShowToast(context.getString(R.string.about_heap_dump_recording_disabled)))
                 delay(3000)
             }

+ 2 - 3
app/src/main/java/io/legado/app/ui/about/CrashLogsDialog.kt

@@ -18,7 +18,6 @@ import io.legado.app.base.adapter.ItemViewHolder
 import io.legado.app.base.adapter.RecyclerAdapter
 import io.legado.app.databinding.DialogRecyclerViewBinding
 import io.legado.app.databinding.Item1lineTextBinding
-import io.legado.app.help.config.AppConfig
 //import io.legado.app.lib.theme.primaryColor
 import io.legado.app.ui.widget.dialog.TextDialog
 import io.legado.app.utils.FileDoc
@@ -112,7 +111,7 @@ class CrashLogsDialog : BaseBottomSheetDialogFragment(R.layout.dialog_recycler_v
                     ?.forEach {
                         list.add(FileDoc.fromFile(it))
                     }
-                val backupPath = AppConfig.backupPath
+                val backupPath = org.koin.core.context.GlobalContext.get().get<io.legado.app.domain.gateway.BackupSettingsGateway>().currentSettings.backupPath
                 if (!backupPath.isNullOrEmpty()) {
                     val uri = Uri.parse(backupPath)
                     FileDoc.fromUri(uri, true)
@@ -146,7 +145,7 @@ class CrashLogsDialog : BaseBottomSheetDialogFragment(R.layout.dialog_recycler_v
                     ?.let {
                         FileUtils.delete(it, false)
                     }
-                val backupPath = AppConfig.backupPath
+                val backupPath = org.koin.core.context.GlobalContext.get().get<io.legado.app.domain.gateway.BackupSettingsGateway>().currentSettings.backupPath
                 if (!backupPath.isNullOrEmpty()) {
                     val uri = Uri.parse(backupPath)
                     FileDoc.fromUri(uri, true)

+ 3 - 2
app/src/main/java/io/legado/app/ui/about/UpdateDialog.kt

@@ -8,7 +8,6 @@ import io.legado.app.R
 import io.legado.app.base.BaseBottomSheetDialogFragment
 import io.legado.app.constant.AppConst
 import io.legado.app.databinding.DialogUpdateBinding
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.update.AppUpdate
 import io.legado.app.help.update.AppVariant
 import io.legado.app.model.Download
@@ -22,6 +21,8 @@ import io.noties.markwon.image.glide.GlideImagesPlugin
 
 class UpdateDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_update) {
 
+    private val otherSettingsGateway get() = org.koin.core.context.GlobalContext.get().get<io.legado.app.domain.gateway.OtherSettingsGateway>()
+
     enum class Mode { UPDATE, VIEW_LOG }
 
     constructor(updateInfo: AppUpdate.UpdateInfo, mode: Mode = Mode.UPDATE) : this() {
@@ -35,7 +36,7 @@ class UpdateDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_update) {
     }
 
     private val checkVariant: AppVariant
-        get() = when (AppConfig.updateToVariant) {
+        get() = when (otherSettingsGateway.currentSettings.updateToVariant) {
             "official_version" -> AppVariant.OFFICIAL
             "beta_release_version" -> AppVariant.BETA_RELEASE
             "all_version" -> AppVariant.ALL

+ 9 - 4
app/src/main/java/io/legado/app/ui/ai/chat/AiChatScreen.kt

@@ -69,10 +69,11 @@ import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.lerp
 import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalClipboardManager
+import android.content.ClipData
+import androidx.compose.ui.platform.ClipEntry
+import androidx.compose.ui.platform.LocalClipboard
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
@@ -127,7 +128,7 @@ fun AiChatScreen(
     val snackbarHostState = remember { SnackbarHostState() }
     val drawerState = rememberDrawerState(DrawerValue.Closed)
     val scope = rememberCoroutineScope()
-    val clipboardManager = LocalClipboardManager.current
+    val clipboard = LocalClipboard.current
     var draft by rememberSaveable { mutableStateOf("") }
     val listState = rememberLazyListState()
     val density = LocalDensity.current
@@ -329,7 +330,11 @@ fun AiChatScreen(
                             assistantLabel = assistantLabel,
                             onOpenBookInfo = onOpenBookInfo,
                             onCopy = {
-                                clipboardManager.setText(AnnotatedString(message.content))
+                                scope.launch {
+                                    clipboard.setClipEntry(
+                                        ClipEntry(ClipData.newPlainText("content", message.content))
+                                    )
+                                }
                             },
                             onRegenerate = if (message.role == AiMessageRole.ASSISTANT && message.parentMessageId != null) {
                                 { onIntent(AiChatIntent.RegenerateMessage(message.id)) }

+ 2 - 2
app/src/main/java/io/legado/app/ui/ai/chat/AiChatViewModel.kt

@@ -373,7 +373,7 @@ class AiChatViewModel(
                                 reasoning = fullReasoning.toString(),
                                 toolTrace = toolTrace
                             ),
-                            parentMessageId = message.parentMessageId!!,
+                            parentMessageId = message.parentMessageId,
                             thinkingDuration = duration
                         )
                     }
@@ -482,7 +482,7 @@ class AiChatViewModel(
                         )
                     }
                     // Generate AI title for first conversation
-                    if (conversationIdForMsg != null && historySnapshot.none { it.role == AiMessageRole.ASSISTANT }) {
+                    if (historySnapshot.none { it.role == AiMessageRole.ASSISTANT }) {
                         generateConversationTitle(conversationIdForMsg, content, assistantContent)
                     }
                 }

+ 3 - 4
app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt

@@ -19,7 +19,6 @@ import io.legado.app.data.entities.RssSource
 import io.legado.app.databinding.DialogCustomGroupBinding
 import io.legado.app.databinding.DialogRecyclerViewBinding
 import io.legado.app.databinding.ItemSourceImportBinding
-import io.legado.app.help.config.AppConfig
 import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.domain.model.settings.OtherSettings
 import io.legado.app.lib.dialogs.alert
@@ -143,11 +142,11 @@ class ImportRssSourceDialog() : BaseDialogFragment(R.layout.dialog_recycler_view
         binding.toolBar.setOnMenuItemClickListener(this)
         binding.toolBar.inflateMenu(R.menu.import_source)
         binding.toolBar.menu.findItem(R.id.menu_keep_original_name)?.isChecked =
-            AppConfig.importKeepName
+            otherSettingsGateway.currentSettings.importKeepName
         binding.toolBar.menu.findItem(R.id.menu_keep_group)?.isChecked =
-            AppConfig.importKeepGroup
+            otherSettingsGateway.currentSettings.importKeepGroup
         binding.toolBar.menu.findItem(R.id.menu_keep_enable)?.isChecked =
-            AppConfig.importKeepEnable
+            otherSettingsGateway.currentSettings.importKeepEnable
         binding.toolBar.menu.findItem(R.id.menu_select_new_source)?.isVisible = false
         binding.toolBar.menu.findItem(R.id.menu_select_update_source)?.isVisible = false
     }

+ 5 - 4
app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt

@@ -6,13 +6,13 @@ import androidx.lifecycle.MutableLiveData
 import com.jayway.jsonpath.JsonPath
 import io.legado.app.R
 import io.legado.app.base.BaseViewModel
+import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.constant.AppConst
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppPattern
 import io.legado.app.data.entities.RssSource
 import io.legado.app.data.repository.RssRepository
 import io.legado.app.exception.NoStackTraceException
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.http.decompressed
 import io.legado.app.help.http.newCallResponseBody
 import io.legado.app.help.http.okHttpClient
@@ -32,6 +32,7 @@ import splitties.init.appCtx
 class ImportRssSourceViewModel(
     app: Application,
     private val repository: RssRepository,
+    private val otherSettingsGateway: OtherSettingsGateway,
 ) : BaseViewModel(app) {
     var isAddGroup = false
     var groupName: String? = null
@@ -66,9 +67,9 @@ class ImportRssSourceViewModel(
     fun importSelect(finally: () -> Unit) {
         execute {
             val group = groupName?.trim()
-            val keepName = AppConfig.importKeepName
-            val keepGroup = AppConfig.importKeepGroup
-            val keepEnable = AppConfig.importKeepEnable
+            val keepName = otherSettingsGateway.currentSettings.importKeepName
+            val keepGroup = otherSettingsGateway.currentSettings.importKeepGroup
+            val keepEnable = otherSettingsGateway.currentSettings.importKeepEnable
             val selectSource = arrayListOf<RssSource>()
             selectStatus.forEachIndexed { index, b ->
                 if (b) {

+ 7 - 8
app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt

@@ -428,13 +428,6 @@ private fun BookmarkGroupHeaderContent(
             supportingColor = LegadoTheme.colorScheme.onSurfaceVariant,
             trailingIconColor = LegadoTheme.colorScheme.onSurfaceVariant
         ),
-        headlineContent = {
-            AppText(
-                text = title,
-                style = LegadoTheme.typography.titleMedium,
-                color = contentColor
-            )
-        },
         supportingContent = {
             subtitle?.let {
                 AppText(
@@ -444,5 +437,11 @@ private fun BookmarkGroupHeaderContent(
                 )
             }
         }
-    )
+    ) {
+        AppText(
+            text = title,
+            style = LegadoTheme.typography.titleMedium,
+            color = contentColor
+        )
+    }
 }

+ 3 - 2
app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt

@@ -4,6 +4,7 @@ import android.app.Application
 import android.os.Bundle
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.viewModelScope
+import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
 import io.legado.app.base.BaseViewModel
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppPattern
@@ -13,7 +14,6 @@ import io.legado.app.data.entities.SearchBook
 import io.legado.app.data.repository.BookSourceRepository
 import io.legado.app.data.repository.SearchRepository
 import io.legado.app.model.webBook.WebBook
-import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.utils.mapParallelSafe
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.ExecutorCoroutineDispatcher
@@ -38,8 +38,9 @@ class ChangeCoverViewModel(
     application: Application,
     private val searchRepository: SearchRepository,
     private val bookSourceRepository: BookSourceRepository,
+    private val downloadCacheSettingsGateway: DownloadCacheSettingsGateway,
 ) : BaseViewModel(application) {
-    private val threadCount = OtherConfig.threadCount
+    private val threadCount = downloadCacheSettingsGateway.currentSettings.threadCount
     private var searchPool: ExecutorCoroutineDispatcher? = null
     private var searchSuccess: ((SearchBook) -> Unit)? = null
     private var upAdapter: (() -> Unit)? = null

+ 4 - 2
app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceAdapter.kt

@@ -25,6 +25,8 @@ class ChangeBookSourceAdapter(
     val callBack: CallBack
 ) : DiffRecyclerAdapter<SearchBook, ItemChangeSourceBinding>(context) {
 
+    private val changeSourceSettingsGateway get() = org.koin.core.context.GlobalContext.get().get<io.legado.app.domain.gateway.ChangeSourceSettingsGateway>()
+
     override val diffItemCallback = object : DiffUtil.ItemCallback<SearchBook>() {
         override fun areItemsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean {
             return oldItem.bookUrl == newItem.bookUrl
@@ -90,13 +92,13 @@ class ChangeBookSourceAdapter(
                 binding.ivGood.setImageResource(R.drawable.ic_praise)
             }
 
-            if (ChangeSourceConfig.loadWordCount && !item.chapterWordCountText.isNullOrBlank()) {
+            if (changeSourceSettingsGateway.currentSettings.loadWordCount && !item.chapterWordCountText.isNullOrBlank()) {
                 tvCurrentChapterWordCount.visible()
             } else {
                 tvCurrentChapterWordCount.gone()
             }
 
-            if (ChangeSourceConfig.loadWordCount && item.respondTime >= 0) {
+            if (changeSourceSettingsGateway.currentSettings.loadWordCount && item.respondTime >= 0) {
                 tvRespondTime.visible()
             } else {
                 tvRespondTime.gone()

+ 13 - 10
app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt

@@ -29,8 +29,9 @@ import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.databinding.DialogBookChangeSourceBinding
 import io.legado.app.domain.gateway.ChangeSourceSettingsGateway
+import io.legado.app.domain.gateway.OtherSettingsGateway
+import io.legado.app.domain.gateway.ReadSettingsGateway
 import io.legado.app.domain.model.settings.ChangeSourceSettings
-import io.legado.app.help.config.AppConfig
 import io.legado.app.lib.dialogs.alert
 import io.legado.app.model.ReadBook
 import io.legado.app.ui.book.search.SearchScope
@@ -73,6 +74,8 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
     private val callBack: CallBack? get() = activity as? CallBack
     private val viewModel: ChangeBookSourceViewModel by viewModel()
     private val changeSourceSettingsGateway by inject<ChangeSourceSettingsGateway>()
+    private val otherSettingsGateway by inject<OtherSettingsGateway>()
+    private val readSettingsGateway by inject<ReadSettingsGateway>()
     private val waitDialog by lazy { WaitDialog(requireContext()) }
     private val adapter by lazy { ChangeBookSourceAdapter(requireContext(), viewModel, this) }
     private val editSourceResult =
@@ -83,7 +86,7 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
     private var currentSelectedSearchBook: SearchBook? = null
     private val searchFinishCallback: (isEmpty: Boolean) -> Unit = {
         if (it) {
-            val searchScope = SearchScope(ChangeSourceConfig.searchScope)
+            val searchScope = SearchScope(changeSourceSettingsGateway.currentSettings.searchScope)
             val group = searchScope.display
             if (!searchScope.isAll()) {
                 lifecycleScope.launch {
@@ -134,13 +137,13 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
         binding.toolBar.menu.applyTint(requireContext())
         binding.toolBar.setOnMenuItemClickListener(this)
         binding.toolBar.menu.findItem(R.id.menu_check_author)
-            ?.isChecked = ChangeSourceConfig.checkAuthor
+            ?.isChecked = changeSourceSettingsGateway.currentSettings.checkAuthor
         binding.toolBar.menu.findItem(R.id.menu_load_info)
-            ?.isChecked = ChangeSourceConfig.loadInfo
+            ?.isChecked = changeSourceSettingsGateway.currentSettings.loadInfo
         binding.toolBar.menu.findItem(R.id.menu_load_toc)
-            ?.isChecked = ChangeSourceConfig.loadToc
+            ?.isChecked = changeSourceSettingsGateway.currentSettings.loadToc
         binding.toolBar.menu.findItem(R.id.menu_load_word_count)
-            ?.isChecked = ChangeSourceConfig.loadWordCount
+            ?.isChecked = changeSourceSettingsGateway.currentSettings.loadWordCount
     }
 
     private fun initRecyclerView() {
@@ -430,8 +433,8 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
                 ReadBook.book?.migrateTo(
                     book,
                     toc,
-                    AppConfig.replaceEnableDefault,
-                    AppConfig.chineseConverterType,
+                    otherSettingsGateway.currentSettings.replaceEnableDefault,
+                    readSettingsGateway.currentSettings.chineseConverterType,
                 )
                 callBack?.addToBookshelf(book, toc)
                 context?.toastOnUi(getString(R.string.book_added_to_shelf))
@@ -508,7 +511,7 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
     private fun upGroupMenu() {
         binding.toolBar.menu.findItem(R.id.menu_group)?.run {
             subMenu?.transaction { menu ->
-                val searchScope = SearchScope(ChangeSourceConfig.searchScope)
+                val searchScope = SearchScope(changeSourceSettingsGateway.currentSettings.searchScope)
                 val selectedGroup = searchScope.displayNames.firstOrNull() ?: ""
                 menu.removeGroup(R.id.source_group)
                 val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source)
@@ -537,7 +540,7 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
      */
     private fun upGroupMenuName() {
         val menuGroup = binding.toolBar.menu.findItem(R.id.menu_group)
-        val searchScope = SearchScope(ChangeSourceConfig.searchScope)
+        val searchScope = SearchScope(changeSourceSettingsGateway.currentSettings.searchScope)
         if (searchScope.isAll()) {
             menuGroup?.title = getString(R.string.group)
         } else {

+ 15 - 12
app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt

@@ -23,7 +23,8 @@ import io.legado.app.help.book.releaseHtmlData
 import io.legado.app.help.config.SourceConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.model.webBook.WebBook
-import io.legado.app.ui.config.otherConfig.OtherConfig
+import io.legado.app.domain.gateway.ChangeSourceSettingsGateway
+import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
 import io.legado.app.utils.internString
 import io.legado.app.utils.mapParallel
 import io.legado.app.utils.mapParallelSafe
@@ -60,8 +61,10 @@ open class ChangeBookSourceViewModel(
     application: Application,
     private val searchRepository: SearchRepository,
     private val bookSourceRepository: BookSourceRepository,
+    private val downloadCacheSettingsGateway: DownloadCacheSettingsGateway,
+    private val changeSourceSettingsGateway: ChangeSourceSettingsGateway,
 ) : BaseViewModel(application) {
-    private val threadCount = OtherConfig.threadCount
+    private val threadCount = downloadCacheSettingsGateway.currentSettings.threadCount
     private var searchPool: ExecutorCoroutineDispatcher? = null
     val searchStateData = MutableLiveData<Boolean>()
     private val _isSearching = MutableStateFlow(false)
@@ -140,7 +143,7 @@ open class ChangeBookSourceViewModel(
         }
     }.map {
         kotlin.runCatching {
-            val comparator = if (ChangeSourceConfig.loadWordCount) {
+            val comparator = if (changeSourceSettingsGateway.currentSettings.loadWordCount) {
                 wordCountComparator
             } else {
                 defaultComparator
@@ -213,7 +216,7 @@ open class ChangeBookSourceViewModel(
             bookMap.clear()
             tocMapChapterCount = 0
             _changeSourceProgress.value = 0 to ""
-            bookSourceParts.addAll(io.legado.app.ui.book.search.SearchScope(ChangeSourceConfig.searchScope).getBookSourceParts())
+            bookSourceParts.addAll(io.legado.app.ui.book.search.SearchScope(changeSourceSettingsGateway.currentSettings.searchScope).getBookSourceParts())
             initSearchPool()
             search()
         }
@@ -272,10 +275,10 @@ open class ChangeBookSourceViewModel(
     }
 
     private suspend fun search(source: BookSource) {
-        val checkAuthor = ChangeSourceConfig.checkAuthor
-        val loadInfo = ChangeSourceConfig.loadInfo
-        val loadToc = ChangeSourceConfig.loadToc
-        val loadWordCount = ChangeSourceConfig.loadWordCount
+        val checkAuthor = changeSourceSettingsGateway.currentSettings.checkAuthor
+        val loadInfo = changeSourceSettingsGateway.currentSettings.loadInfo
+        val loadToc = changeSourceSettingsGateway.currentSettings.loadToc
+        val loadWordCount = changeSourceSettingsGateway.currentSettings.loadWordCount
         val resultBooks = WebBook.searchBookAwait(
             source, name,
             filter = { fName, fAuthor, _ ->
@@ -298,7 +301,7 @@ open class ChangeBookSourceViewModel(
         if (book.tocUrl.isEmpty()) {
             WebBook.getBookInfoAwait(source, book)
         }
-        if (ChangeSourceConfig.loadToc || ChangeSourceConfig.loadWordCount) {
+        if (changeSourceSettingsGateway.currentSettings.loadToc || changeSourceSettingsGateway.currentSettings.loadWordCount) {
             loadBookToc(source, book)
         } else {
             //从详情页里获取最新章节
@@ -318,7 +321,7 @@ open class ChangeBookSourceViewModel(
         }
         bookMap[book.primaryStr()] = book
         book.releaseHtmlData()
-        if (ChangeSourceConfig.loadWordCount) {
+        if (changeSourceSettingsGateway.currentSettings.loadWordCount) {
             loadBookWordCount(source, book, chapters)
         } else {
             val searchBook = book.toSearchBook()
@@ -414,13 +417,13 @@ open class ChangeBookSourceViewModel(
 
     private suspend fun getDbSearchBooks(): List<SearchBook> {
         val searchScope = io.legado.app.ui.book.search.SearchScope(
-            ChangeSourceConfig.searchScope
+            changeSourceSettingsGateway.currentSettings.searchScope
         )
         val group = when {
             searchScope.isAll() || searchScope.isSource() -> ""
             else -> searchScope.displayNames.firstOrNull() ?: ""
         }
-        val bookAuthor = if (ChangeSourceConfig.checkAuthor) author else ""
+        val bookAuthor = if (changeSourceSettingsGateway.currentSettings.checkAuthor) author else ""
         return searchRepository.findChangeSourceBooks(name, bookAuthor, screenKey, group)
     }
 

+ 3 - 4
app/src/main/java/io/legado/app/ui/book/info/BookInfoBackdropStyle.kt

@@ -1,7 +1,5 @@
 package io.legado.app.ui.book.info
 
-import io.legado.app.ui.config.themeConfig.ThemeConfig
-
 internal data class BookInfoBackdropStyle(
     val showCover: Boolean,
     val blurCover: Boolean,
@@ -9,14 +7,15 @@ internal data class BookInfoBackdropStyle(
 )
 
 internal fun resolveBookInfoBackdropStyle(backgroundMode: String): BookInfoBackdropStyle {
+    // 取值与主题设置持久化的 bookInfoBackground 一致:off/ off_for_default / on
     return when (backgroundMode) {
-        ThemeConfig.BOOK_INFO_BACKGROUND_BLUR_OFF -> BookInfoBackdropStyle(
+        "off" -> BookInfoBackdropStyle(
             showCover = true,
             blurCover = false,
             applySeedOverlay = true,
         )
 
-        ThemeConfig.BOOK_INFO_BACKGROUND_COVER_HIDDEN -> BookInfoBackdropStyle(
+        "off_for_default" -> BookInfoBackdropStyle(
             showCover = false,
             blurCover = false,
             applySeedOverlay = false,

+ 1 - 2
app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt

@@ -61,7 +61,6 @@ import io.legado.app.model.SourceCallBack
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.model.webBook.WebBook
-import io.legado.app.ui.config.coverConfig.CoverConfig
 import io.legado.app.ui.main.MainIntent
 import io.legado.app.ui.widget.components.image.cover.buildCoverImageRequest
 import io.legado.app.utils.ArchiveUtils
@@ -640,7 +639,7 @@ class BookInfoViewModel(
                 context = context,
                 data = path,
                 sourceOrigin = sourceOrigin,
-                loadOnlyWifi = CoverConfig.loadCoverOnlyWifi,
+                loadOnlyWifi = coverSettingsGateway.currentSettings.loadOnlyOnWifi,
                 crossfade = false
             )
             val result = imageLoader.execute(request)

+ 10 - 6
app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageScreenViewModel.kt

@@ -31,9 +31,10 @@ import io.legado.app.help.book.removeType
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.model.CacheBook
 import io.legado.app.service.ExportBookService
-import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
+import io.legado.app.domain.gateway.BookshelfSettingsGateway
+import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
+import org.koin.core.context.GlobalContext
 import io.legado.app.ui.config.bookshelfConfig.BookshelfManageScreenConfig
-import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.ui.main.bookshelf.toLightBook
 import io.legado.app.utils.cnCompare
 import io.legado.app.utils.move
@@ -56,6 +57,8 @@ import java.util.concurrent.ConcurrentHashMap
 import kotlin.math.max
 import kotlin.math.min
 
+private val bookshelfSettingsGateway get() = GlobalContext.get().get<BookshelfSettingsGateway>()
+
 data class BookshelfManageScreenExportConfig(
     val exportUseReplace: Boolean = true,
     val enableCustomExport: Boolean = false,
@@ -77,8 +80,8 @@ data class BookshelfManageScreenUiState(
     val groupName: String? = null,
     val groupList: List<BookGroup> = emptyList(),
     val books: List<Book> = emptyList(),
-    val bookSort: Int = BookshelfConfig.bookshelfSort,
-    val bookSortOrder: Int = BookshelfConfig.bookshelfSortOrder,
+    val bookSort: Int = bookshelfSettingsGateway.currentSettings.bookshelfSort,
+    val bookSortOrder: Int = bookshelfSettingsGateway.currentSettings.bookshelfSortOrder,
     val isDownloadRunning: Boolean = false,
     val isChangingSource: Boolean = false,
     val changeSourceProgress: String? = null,
@@ -166,7 +169,8 @@ class BookshelfManageScreenViewModel(
     private val changeBookSourceUseCase: ChangeBookSourceUseCase,
     private val clearBookCacheUseCase: ClearBookCacheUseCase,
     private val deleteBooksUseCase: DeleteBooksUseCase,
-    private val updateBooksGroupUseCase: UpdateBooksGroupUseCase
+    private val updateBooksGroupUseCase: UpdateBooksGroupUseCase,
+    private val downloadCacheSettingsGateway: DownloadCacheSettingsGateway,
 ) : BaseViewModel(application) {
 
     private companion object {
@@ -742,7 +746,7 @@ class BookshelfManageScreenViewModel(
             return
         }
         execute {
-            val concurrency = OtherConfig.threadCount.coerceAtLeast(1)
+            val concurrency = downloadCacheSettingsGateway.currentSettings.threadCount.coerceAtLeast(1)
             _uiState.update {
                 it.copy(
                     isChangingSource = true,

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/manga/MangaReaderViewModel.kt

@@ -447,7 +447,7 @@ class MangaReaderViewModel(
                 MangaChapterTransitionDirection.NEXT
             }
             val targetExists = targetChapterIndex in 0 until session.chapterCount
-            val targetName = session.book?.chapterTitles?.getOrNull(targetChapterIndex)
+            val targetName = session.book.chapterTitles.getOrNull(targetChapterIndex)
                 ?: appCtx.getString(
                     R.string.manga_reader_transition_chapter_number,
                     targetChapterIndex + 1,

+ 12 - 10
app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt

@@ -66,7 +66,6 @@ import io.legado.app.receiver.NetworkChangedListener
 import io.legado.app.receiver.TimeBatteryReceiver
 import io.legado.app.service.BaseReadAloudService
 import io.legado.app.ui.book.read.page.entities.PageDirection
-import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.ui.login.SourceLoginJsExtensions
 import io.legado.app.ui.association.OpenUrlConfirmActivity
 import io.legado.app.ui.widget.PopupAction
@@ -116,6 +115,9 @@ class ReadBookController(
     ReadBookInputHandler,
     ReadBook.ReaderRenderCallback {
 
+    private val readSettingsGateway get() = org.koin.core.context.GlobalContext.get().get<io.legado.app.domain.gateway.ReadSettingsGateway>()
+    private val aloudSettingsGateway get() = org.koin.core.context.GlobalContext.get().get<io.legado.app.domain.gateway.ReadAloudSettingsGateway>()
+
     internal val layoutController = ReaderLayoutCoordinator(
         updateLayoutSize = { _, _ -> },
         relayoutContent = ReadBook::relayoutContent,
@@ -467,7 +469,7 @@ class ReadBookController(
         val debounce = now - composeImageClickAt < 300L
         composeImageClickAt = now
         composeImageDoubleClick = if (debounce) !composeImageDoubleClick else false
-        return when (ReadConfig.clickImgWay) {
+        return when (readSettingsGateway.currentSettings.clickImgWay) {
             "1" -> { viewModel.onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.Photo(image.source))); true }
             "2" -> if (!debounce && ReadBook.book?.isOnLineTxt == true) {
                 image.action?.takeIf(String::isNotBlank)?.let { clickImg(it, image.source); true }
@@ -1279,7 +1281,7 @@ class ReadBookController(
                         .setClassName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name)
                     
                     val title = resolveInfo.loadLabel(pm).toString()
-                    val icon = if (ReadConfig.showSelectMenuIcon) {
+                    val icon = if (readSettingsGateway.currentSettings.showSelectMenuIcon) {
                         runCatching { resolveInfo.loadIcon(pm) }.getOrNull()
                     } else null
 
@@ -1288,7 +1290,7 @@ class ReadBookController(
             }
 
             val allItems = items + thirdPartyItems
-            val configStr = ReadConfig.textSelectMenuConfig
+            val configStr = readSettingsGateway.currentSettings.textSelectMenuConfig
             val result = if (configStr.isEmpty()) {
                 allItems
             } else {
@@ -1829,17 +1831,17 @@ class ReadBookController(
     }
 
     override fun mouseWheelPage(direction: PageDirection) {
-        if (menuLayoutIsVisible || !ReadConfig.mouseWheelPage) {
+        if (menuLayoutIsVisible || !readSettingsGateway.currentSettings.mouseWheelPage) {
             return
         }
         keyPageDebounce(direction, mouseWheel = true, longPress = false)
     }
 
     private fun volumeKeyPage(direction: PageDirection, longPress: Boolean): Boolean {
-        if (!ReadConfig.volumeKeyPage) {
+        if (!readSettingsGateway.currentSettings.volumeKeyPage) {
             return false
         }
-        if (!ReadConfig.volumeKeyPageOnPlay && BaseReadAloudService.isPlay()) {
+        if (!readSettingsGateway.currentSettings.volumeKeyPageOnPlay && BaseReadAloudService.isPlay()) {
             return false
         }
         handleKeyPage(direction, longPress)
@@ -1847,7 +1849,7 @@ class ReadBookController(
     }
 
     override fun handleKeyPage(direction: PageDirection, longPress: Boolean) {
-        if (ReadConfig.keyPageOnLongPress || direction == PageDirection.NONE) {
+        if (readSettingsGateway.currentSettings.keyPageOnLongPress || direction == PageDirection.NONE) {
             keyPage(direction)
         } else {
             keyPageDebounce(direction, longPress = longPress)
@@ -1946,7 +1948,7 @@ class ReadBookController(
     }
 
     private fun upScreenTimeOut() {
-        val keepLightPrefer = ReadConfig.keepLight.toLongOrNull() ?: 0L
+        val keepLightPrefer = readSettingsGateway.currentSettings.keepLight.toLongOrNull() ?: 0L
         screenTimeOut = keepLightPrefer * 1000L
         screenOffTimerStartInternal()
     }
@@ -2030,7 +2032,7 @@ class ReadBookController(
     }
 
     fun setOrientation() {
-        when (ReadConfig.screenOrientation) {
+        when (readSettingsGateway.currentSettings.screenOrientation) {
             "0" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
             "1" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
             "2" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE

+ 2 - 2
app/src/main/java/io/legado/app/ui/book/read/ReadBookFloatingActionBar.kt

@@ -15,7 +15,7 @@ import androidx.compose.material.icons.filled.DarkMode
 import androidx.compose.material.icons.filled.History
 import androidx.compose.material.icons.filled.LightMode
 import androidx.compose.material.icons.filled.PlayArrow
-import androidx.compose.material.icons.filled.VolumeUp
+import androidx.compose.material.icons.automirrored.filled.VolumeUp
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.ui.Alignment
@@ -79,7 +79,7 @@ private fun ReadAloudDetachedCapsule(onIntent: (ReadBookIntent) -> Unit) {
     ) {
         MediumTonalButton(
             onClick = { onIntent(ReadBookIntent.BackToSpeakingPosition) },
-            icon = Icons.Default.VolumeUp,
+            icon = Icons.AutoMirrored.Filled.VolumeUp,
             text = stringResource(R.string.back_to_speaking_position),
             contentDescription = stringResource(R.string.back_to_speaking_position),
         )

+ 3 - 3
app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBarFloating.kt

@@ -29,7 +29,7 @@ import androidx.compose.material.icons.filled.Replay
 import androidx.compose.material.icons.filled.SwapHoriz
 import androidx.compose.material.icons.filled.SwapVert
 import androidx.compose.material.icons.filled.Sync
-import androidx.compose.material.icons.filled.Toc
+import androidx.compose.material.icons.automirrored.filled.Toc
 import androidx.compose.material.icons.filled.Translate
 import androidx.compose.material.icons.filled.Tune
 import androidx.compose.material3.Icon
@@ -191,7 +191,7 @@ private fun OverflowDropdownMenu(
                 if (state.isLocalTxt) {
                     RoundDropdownMenuItem(
                         text = stringResource(R.string.txt_toc_rule),
-                        leadingIcon = menuIcon(Icons.Default.Toc),
+                        leadingIcon = menuIcon(Icons.AutoMirrored.Filled.Toc),
                         onClick = { dismiss(); onIntent(ReadBookIntent.MenuTocRegex) },
                     )
                 }
@@ -241,7 +241,7 @@ private fun OverflowDropdownMenu(
         )
         RoundDropdownMenuItem(
             text = stringResource(R.string.re_segment),
-            leadingIcon = menuIcon(Icons.Default.Toc),
+            leadingIcon = menuIcon(Icons.AutoMirrored.Filled.Toc),
             isSelected = state.reSegment,
             onClick = { onIntent(ReadBookIntent.MenuReSegment) },
         )

+ 3 - 3
app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBarTitle.kt

@@ -39,7 +39,7 @@ import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.material.icons.filled.Payment
 import androidx.compose.material.icons.filled.Refresh
 import androidx.compose.material.icons.filled.SwapHoriz
-import androidx.compose.material.icons.filled.Toc
+import androidx.compose.material.icons.automirrored.filled.Toc
 import androidx.compose.material.icons.filled.Translate
 import androidx.compose.material3.Icon
 import androidx.compose.runtime.Composable
@@ -840,7 +840,7 @@ private fun MenuTitleBarMergedGlassButton(
                 // TXT directory rule
                 if (state.isLocalTxt) {
                     MergedGlassIconButton(
-                        icon = Icons.Default.Toc,
+                        icon = Icons.AutoMirrored.Filled.Toc,
                         tint = tint,
                         contentDescription = stringResource(R.string.txt_toc_rule),
                         onClick = { onIntent(ReadBookIntent.MenuTocRegex) },
@@ -1005,7 +1005,7 @@ private fun TxtTocRuleActionButton(
 ) {
     MenuTitleGlassButton(
         onClick = { onIntent(ReadBookIntent.MenuTocRegex) },
-        icon = Icons.Default.Toc,
+        icon = Icons.AutoMirrored.Filled.Toc,
         contentDescription = stringResource(R.string.txt_toc_rule),
         state = state,
         colors = colors,

+ 2 - 2
app/src/main/java/io/legado/app/ui/book/read/sheet/CharsetConfigSheet.kt

@@ -6,7 +6,7 @@ import androidx.compose.material3.DropdownMenuItem
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExposedDropdownMenuBox
 import androidx.compose.material3.ExposedDropdownMenuDefaults
-import androidx.compose.material3.MenuAnchorType
+import androidx.compose.material3.ExposedDropdownMenuAnchorType
 import androidx.compose.material3.OutlinedTextField
 import androidx.compose.material3.Text
 import androidx.compose.material3.TextButton
@@ -48,7 +48,7 @@ fun CharsetConfigSheet(
                             value = charset,
                             onValueChange = { charset = it },
                             modifier = Modifier
-                                .menuAnchor(MenuAnchorType.PrimaryEditable)
+                                .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable)
                                 .fillMaxWidth(),
                             readOnly = false,
                             label = { Text(stringResource(R.string.set_charset)) },

+ 3 - 3
app/src/main/java/io/legado/app/ui/book/read/sheet/ReaderMoreActionsSheet.kt

@@ -32,7 +32,7 @@ import androidx.compose.material.icons.filled.Settings
 import androidx.compose.material.icons.filled.SwapHoriz
 import androidx.compose.material.icons.filled.SwapVert
 import androidx.compose.material.icons.filled.Sync
-import androidx.compose.material.icons.filled.Toc
+import androidx.compose.material.icons.automirrored.filled.Toc
 import androidx.compose.material.icons.filled.Translate
 import androidx.compose.material.icons.filled.Tune
 import androidx.compose.material3.HorizontalDivider
@@ -301,7 +301,7 @@ private fun moreActionSpecs(
         applicable = !state.isLocalBook,
         onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.Download)) }),
     MoreActionSpec(
-        "toc_rule", stringResource(R.string.txt_toc_rule), Icons.Default.Toc,
+        "toc_rule", stringResource(R.string.txt_toc_rule), Icons.AutoMirrored.Filled.Toc,
         applicable = state.isLocalTxt, onClick = { dispatch(ReadBookIntent.MenuTocRegex) }),
     MoreActionSpec(
         "charset", stringResource(R.string.set_charset), Icons.Default.Translate,
@@ -323,7 +323,7 @@ private fun moreActionSpecs(
         "reverse_content", stringResource(R.string.reverse_content), Icons.Default.SwapVert,
         onClick = { dispatch(ReadBookIntent.MenuReverseContent) }),
     MoreActionSpec(
-        "re_segment", stringResource(R.string.re_segment), Icons.Default.Toc,
+        "re_segment", stringResource(R.string.re_segment), Icons.AutoMirrored.Filled.Toc,
         selected = state.reSegment, onClick = { onIntent(ReadBookIntent.MenuReSegment) }),
     MoreActionSpec(
         "del_ruby", stringResource(R.string.del_ruby_tag), Icons.Default.CleanHands,

+ 14 - 14
app/src/main/java/io/legado/app/ui/book/read/sheet/TypographyTabs.kt

@@ -13,16 +13,16 @@ import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.AlignHorizontalCenter
-import androidx.compose.material.icons.filled.AlignHorizontalLeft
-import androidx.compose.material.icons.filled.AlignHorizontalRight
+import androidx.compose.material.icons.automirrored.filled.AlignHorizontalLeft
+import androidx.compose.material.icons.automirrored.filled.AlignHorizontalRight
 import androidx.compose.material.icons.filled.AspectRatio
-import androidx.compose.material.icons.filled.CallSplit
+import androidx.compose.material.icons.automirrored.filled.CallSplit
 import androidx.compose.material.icons.filled.CleanHands
 import androidx.compose.material.icons.filled.Code
 import androidx.compose.material.icons.filled.CopyAll
 import androidx.compose.material.icons.filled.FormatAlignJustify
 import androidx.compose.material.icons.filled.FormatColorText
-import androidx.compose.material.icons.filled.FormatIndentIncrease
+import androidx.compose.material.icons.automirrored.filled.FormatIndentIncrease
 import androidx.compose.material.icons.filled.FormatItalic
 import androidx.compose.material.icons.filled.FormatLineSpacing
 import androidx.compose.material.icons.filled.FormatSize
@@ -32,7 +32,7 @@ import androidx.compose.material.icons.filled.Layers
 import androidx.compose.material.icons.filled.LineWeight
 import androidx.compose.material.icons.filled.Minimize
 import androidx.compose.material.icons.filled.Palette
-import androidx.compose.material.icons.filled.Segment
+import androidx.compose.material.icons.automirrored.filled.Segment
 import androidx.compose.material.icons.filled.SpaceBar
 import androidx.compose.material.icons.filled.TextFields
 import androidx.compose.material.icons.filled.Title
@@ -238,14 +238,14 @@ private fun PaddingSliders(
         title = stringResource(R.string.padding_left),
         value = left,
         valueRange = 0f..maxValue,
-        imageVector = Icons.Default.AlignHorizontalLeft,
+        imageVector = Icons.AutoMirrored.Filled.AlignHorizontalLeft,
         onValueChange = onLeftChange,
     )
     TinySliderSettingItem(
         title = stringResource(R.string.padding_right),
         value = right,
         valueRange = 0f..maxValue,
-        imageVector = Icons.Default.AlignHorizontalRight,
+        imageVector = Icons.AutoMirrored.Filled.AlignHorizontalRight,
         onValueChange = onRightChange,
     )
 }
@@ -356,7 +356,7 @@ internal fun TypographyBodyTab(
             value = indentCount.toFloat(),
             valueRange = 0f..4f,
             steps = 3,
-            imageVector = Icons.Default.FormatIndentIncrease,
+            imageVector = Icons.AutoMirrored.Filled.FormatIndentIncrease,
             onValueChange = { value ->
                 indentCount = value.toInt()
                 onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ParagraphIndent(" ".repeat(indentCount))))
@@ -391,7 +391,7 @@ internal fun TypographyBodyTab(
             value = paragraphSpacing,
             valueRange = 0f..20f,
             steps = 19,
-            imageVector = Icons.Default.Segment,
+            imageVector = Icons.AutoMirrored.Filled.Segment,
             valueFormat = { (it / 10f).toString() },
             onValueChange = { value ->
                 paragraphSpacing = value
@@ -520,7 +520,7 @@ internal fun TypographyTitleTab(
                 stringResource(R.string.split_title_by_regex),
             ),
             entryValues = arrayOf("0", "1", "2", "3"),
-            imageVector = Icons.Default.CallSplit,
+            imageVector = Icons.AutoMirrored.Filled.CallSplit,
             onValueChange = {
                 titleSegType = it.toInt()
                 onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleSegType(titleSegType)))
@@ -692,7 +692,7 @@ internal fun TypographyHeaderTab(
             value = headerLeft,
             tipNames = tipNames,
             tipValues = tipValues,
-            imageVector = Icons.Default.AlignHorizontalLeft,
+            imageVector = Icons.AutoMirrored.Filled.AlignHorizontalLeft,
             onValueChange = { onTipChange(CustomTipTarget.HEADER_LEFT, it) },
         )
         TipPositionDropdown(
@@ -708,7 +708,7 @@ internal fun TypographyHeaderTab(
             value = headerRight,
             tipNames = tipNames,
             tipValues = tipValues,
-            imageVector = Icons.Default.AlignHorizontalRight,
+            imageVector = Icons.AutoMirrored.Filled.AlignHorizontalRight,
             onValueChange = { onTipChange(CustomTipTarget.HEADER_RIGHT, it) },
         )
 
@@ -808,7 +808,7 @@ internal fun TypographyFooterTab(
             value = footerLeft,
             tipNames = tipNames,
             tipValues = tipValues,
-            imageVector = Icons.Default.AlignHorizontalLeft,
+            imageVector = Icons.AutoMirrored.Filled.AlignHorizontalLeft,
             onValueChange = { onTipChange(CustomTipTarget.FOOTER_LEFT, it) },
         )
         TipPositionDropdown(
@@ -824,7 +824,7 @@ internal fun TypographyFooterTab(
             value = footerRight,
             tipNames = tipNames,
             tipValues = tipValues,
-            imageVector = Icons.Default.AlignHorizontalRight,
+            imageVector = Icons.AutoMirrored.Filled.AlignHorizontalRight,
             onValueChange = { onTipChange(CustomTipTarget.FOOTER_RIGHT, it) },
         )
 

+ 2 - 3
app/src/main/java/io/legado/app/ui/book/readaloud/player/ReadAloudPlayerCoordinator.kt

@@ -14,7 +14,6 @@ import io.legado.app.model.ReadAloudSessionStore
 import io.legado.app.model.ReadBook
 import io.legado.app.service.BaseReadAloudService
 import io.legado.app.ui.book.read.ReadConfigUpdateBus
-import io.legado.app.ui.config.readConfig.ReadConfig
 import kotlinx.collections.immutable.ImmutableList
 import kotlinx.collections.immutable.persistentListOf
 import kotlinx.collections.immutable.toImmutableList
@@ -92,7 +91,7 @@ class ReadAloudPlayerCoordinator(
             engineName = playback.engineName,
             speakerName = playback.characterName.ifBlank { playback.roleType.storageValue },
             isPaused = session.status != ReadAloudSessionStatus.Playing,
-            speed = ReadConfig.ttsSpeechRate,
+            speed = readAloudSettingsGateway.currentSettings.ttsSpeechRate,
             timerMinutes = session.timerMinutes,
             finishCurrentChapterAfterTimer = settings.finishCurrentChapterAfterTimer,
         )
@@ -119,7 +118,7 @@ class ReadAloudPlayerCoordinator(
             engineName = playback.engineName,
             speakerName = playback.characterName.ifBlank { playback.roleType.storageValue },
             isPaused = session.status != ReadAloudSessionStatus.Playing,
-            speed = ReadConfig.ttsSpeechRate,
+            speed = readAloudSettingsGateway.currentSettings.ttsSpeechRate,
             timerMinutes = session.timerMinutes,
             finishCurrentChapterAfterTimer =
                 readAloudSettingsGateway.currentSettings.finishCurrentChapterAfterTimer,

+ 3 - 2
app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt

@@ -2,6 +2,7 @@ package io.legado.app.ui.book.search
 
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
+import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
 import io.legado.app.data.entities.BookSourcePart
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.data.local.preferences.LocalPreferencesKeys
@@ -20,7 +21,6 @@ import io.legado.app.domain.usecase.ExploreBooksUseCase
 import io.legado.app.domain.usecase.ResolveBookShelfStateUseCase
 import io.legado.app.domain.usecase.SearchBooksUseCase
 import io.legado.app.domain.usecase.SearchRunEvent
-import io.legado.app.ui.config.otherConfig.OtherConfig
 import kotlinx.collections.immutable.persistentListOf
 import kotlinx.collections.immutable.persistentSetOf
 import kotlinx.collections.immutable.toImmutableList
@@ -55,6 +55,7 @@ class SearchViewModel(
     private val addToBookshelfUseCase: AddToBookshelfUseCase,
     private val checkBookContentQualityUseCase: CheckBookContentQualityUseCase,
     private val localPreferencesRepository: SettingsRepository,
+    private val downloadCacheSettingsGateway: DownloadCacheSettingsGateway,
 ) : ViewModel() {
 
     private val searchLayoutMode = localPreferencesRepository
@@ -588,7 +589,7 @@ class SearchViewModel(
                             page = page,
                             scope = BookSearchScope(searchScope.toString()),
                             matchMode = _uiState.value.matchMode,
-                            concurrency = OtherConfig.threadCount,
+                            concurrency = downloadCacheSettingsGateway.currentSettings.threadCount,
                             types = _uiState.value.selectedSourceTypes.takeIf { it.isNotEmpty() },
                         ),
                         searchControl

+ 3 - 3
app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentAdapter.kt

@@ -6,7 +6,6 @@ import androidx.core.view.isVisible
 import io.legado.app.base.adapter.ItemViewHolder
 import io.legado.app.base.adapter.RecyclerAdapter
 import io.legado.app.databinding.ItemSearchListBinding
-import io.legado.app.help.config.AppConfig
 import io.legado.app.utils.themeColor
 
 
@@ -16,6 +15,7 @@ class SearchContentAdapter(context: Context, val callback: Callback) :
     private val textColorInt = context.themeColor(com.google.android.material.R.attr.colorOnSurface)
     private val accentColorInt = context.themeColor(androidx.appcompat.R.attr.colorPrimary)
     private val bgColorInt = context.themeColor(com.google.android.material.R.attr.colorSecondaryContainer)
+    private val isEInkMode get() = org.koin.core.context.GlobalContext.get().get<io.legado.app.domain.gateway.ThemeSettingsGateway>().currentSettings.appTheme == "4"
 
     override fun getViewBinding(parent: ViewGroup): ItemSearchListBinding {
         return ItemSearchListBinding.inflate(inflater, parent, false)
@@ -30,12 +30,12 @@ class SearchContentAdapter(context: Context, val callback: Callback) :
         binding.run {
             val isDur = callback.durChapterIndex() == item.chapterIndex
             if (payloads.isEmpty()) {
-                tvTitle.text = item.getTitleSpannable(textColorInt, AppConfig.isEInkMode)
+                tvTitle.text = item.getTitleSpannable(textColorInt, isEInkMode)
                 tvContent.text = item.getContentSpannable(
                     textColorInt,
                     accentColorInt,
                     bgColorInt,
-                    AppConfig.isEInkMode,
+                    isEInkMode,
                 )
 
                 if (item.progressPercent > 0) {

+ 7 - 8
app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt

@@ -372,13 +372,6 @@ fun SearchHistoryList(
                         modifier = Modifier
                             .clickable { onHistoryClick(item) }
                             .animateItem(),
-                        headlineContent = {
-                            AppText(
-                                text = item.query,
-                                maxLines = 1,
-                                overflow = TextOverflow.Ellipsis
-                            )
-                        },
                         leadingContent = {
                             Icon(Icons.Default.History, contentDescription = null)
                         },
@@ -393,7 +386,13 @@ fun SearchHistoryList(
                             containerColor = LegadoTheme.colorScheme.surface,
                             contentColor = LegadoTheme.colorScheme.onSurface
                         )
-                    )
+                    ) {
+                        AppText(
+                            text = item.query,
+                            maxLines = 1,
+                            overflow = TextOverflow.Ellipsis
+                        )
+                    }
                 }
                 item {
                     Box(

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

@@ -513,7 +513,7 @@ fun BookSourceScreen(
                 showExportSheet = true
             },
         ),
-        onDeleteSelected = { deleteIds = it as Set<String> },
+        onDeleteSelected = { deleteIds = @Suppress("UNCHECKED_CAST") (it as Set<String>) },
         dropDownMenuContent = { dismiss ->
             RoundDropdownMenuItem(
                 text = stringResource(R.string.group_manage),

+ 3 - 3
app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt

@@ -357,7 +357,7 @@ fun TocScreen(
 
     LaunchedEffect(state.items, book) {
         if (!hasAutoScrolled && state.items.isNotEmpty() && book != null) {
-            val durIndex = book?.durChapterIndex ?: -1
+            val durIndex = book.durChapterIndex
             val targetIndex = state.items.indexOfFirst { it.id == durIndex || it.isDur }
             if (targetIndex != -1) {
                 delay(100) 
@@ -482,7 +482,7 @@ fun TocScreen(
                                 RoundDropdownMenuItem(
                                     text = stringResource(R.string.local_book_toc_rule),
                                     onClick = {
-                                        onEditLocalTocRule(book?.tocUrl)
+                                        onEditLocalTocRule(book.tocUrl)
                                         dismiss()
                                     }
                                 )
@@ -995,7 +995,7 @@ fun BookmarkListContent(
 
     LaunchedEffect(bookmarks, book?.durChapterIndex) {
         if (bookmarks.isNotEmpty() && book != null) {
-            val durIndex = book!!.durChapterIndex
+            val durIndex = book.durChapterIndex
             var scrollPos = 0
             for ((index, bookmark) in bookmarks.withIndex()) {
                 if (bookmark.chapterIndex >= durIndex) break

+ 1 - 0
app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt

@@ -519,6 +519,7 @@ class TocViewModel(
     val useReplace get() = tocPreferences.value.useReplace
     val showWordCount get() = tocPreferences.value.showWordCount
 
+    @Suppress("OVERRIDE_DEPRECATION")
     override fun filterData(data: List<TocDomainItem>, key: String): List<TocDomainItem> {
         val collapsed = _collapsedVolumes.value
         val isSearch = key.isNotBlank()

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/toc/rule/TxtTocRuleViewModel.kt

@@ -85,7 +85,7 @@ class TxtTocRuleViewModel(
     override fun TxtTocRule.toUiItem() =
         TxtTocRuleItemUi(id, name, enable, this, example = example ?: "")
 
-    @Suppress("DEPRECATION")
+    @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
     override fun filterData(data: List<TxtTocRule>, key: String): List<TxtTocRule> {
         val filtered = if (key.isEmpty()) data
         else data.filter { it.name.contains(key, ignoreCase = true) }

+ 6 - 2
app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfManageScreenConfig.kt

@@ -1,16 +1,20 @@
 package io.legado.app.ui.config.bookshelfConfig
 
 import io.legado.app.data.dao.BookGroupDao
+import io.legado.app.domain.gateway.BookshelfSettingsGateway
+import org.koin.core.context.GlobalContext
 
 class BookshelfManageScreenConfig(
     private val bookGroupDao: BookGroupDao
 ) {
+    private val bookshelfSettingsGateway
+        get() = GlobalContext.get().get<BookshelfSettingsGateway>()
 
     fun getBookSortByGroupId(groupId: Long): Int {
-        val defaultSort = BookshelfConfig.bookshelfSort
+        val defaultSort = bookshelfSettingsGateway.currentSettings.bookshelfSort
         return bookGroupDao.getByID(groupId)?.getRealBookSort(defaultSort) ?: defaultSort
     }
 
     val bookshelfSortOrder: Int
-        get() = BookshelfConfig.bookshelfSortOrder
+        get() = bookshelfSettingsGateway.currentSettings.bookshelfSortOrder
 }

+ 13 - 8
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt

@@ -24,9 +24,9 @@ import io.legado.app.domain.usecase.RefreshTocUseCase
 import io.legado.app.domain.usecase.UpdateBooksGroupUseCase
 import io.legado.app.domain.gateway.BookshelfSettingsGateway
 import io.legado.app.domain.gateway.AppShellSettingsGateway
+import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
 import io.legado.app.domain.gateway.ThemeSettingsGateway
 import io.legado.app.exception.NoStackTraceException
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.model.CacheBook
 import io.legado.app.model.SourceCallBack
@@ -92,6 +92,7 @@ class BookshelfViewModel(
     private val bookshelfSettingsGateway: BookshelfSettingsGateway,
     private val appShellSettingsGateway: AppShellSettingsGateway,
     private val themeSettingsGateway: ThemeSettingsGateway,
+    private val downloadCacheSettingsGateway: DownloadCacheSettingsGateway,
 ) : BaseViewModel(application) {
     private var addBookJob: Coroutine<*>? = null
 
@@ -144,7 +145,8 @@ class BookshelfViewModel(
     val scrollTrigger = _scrollTrigger.asSharedFlow()
 
     private val updateConcurrency: Int
-        get() = AppConfig.threadCount.coerceIn(1, AppConst.MAX_THREAD)
+        get() = downloadCacheSettingsGateway.currentSettings.threadCount
+            .coerceIn(1, AppConst.MAX_THREAD)
 
     @OptIn(ExperimentalCoroutinesApi::class)
     private val updateDispatcher: CoroutineDispatcher
@@ -313,7 +315,7 @@ class BookshelfViewModel(
                 }
                 combine(flows) { results ->
                     results.fold(persistentMapOf<Long, ImmutableList<BookUiItem>>()) { acc, (id, list) ->
-                        acc.put(id, list)
+                        acc.putting(id, list)
                     }
                 }
             }
@@ -398,8 +400,8 @@ class BookshelfViewModel(
                 var previews = persistentMapOf<Long, ImmutableList<BookUiItem>>()
                 var counts = persistentMapOf<Long, Int>()
                 results.forEach { (groupId, count, preview) ->
-                    counts = counts.put(groupId, count)
-                    previews = previews.put(groupId, preview.toImmutableList())
+                    counts = counts.putting(groupId, count)
+                    previews = previews.putting(groupId, preview.toImmutableList())
                 }
                 GroupPreviewState(previews, counts, allBookCount)
             }
@@ -1080,9 +1082,12 @@ class BookshelfViewModel(
     }
 
     private fun addDownload(source: BookSource, book: Book) {
-        if (AppConfig.preDownloadNum == 0) return
+        if (downloadCacheSettingsGateway.currentSettings.preDownloadNum == 0) return
         val endIndex =
-            min(book.totalChapterNum - 1, book.durChapterIndex + AppConfig.preDownloadNum)
+            min(
+                book.totalChapterNum - 1,
+                book.durChapterIndex + downloadCacheSettingsGateway.currentSettings.preDownloadNum
+            )
         val cacheBook = CacheBook.getOrCreate(source, book)
         cacheBook.addDownload(book.durChapterIndex, endIndex)
     }
@@ -1096,7 +1101,7 @@ class BookshelfViewModel(
             )
         }
         eventListenerSource.clear()
-        if (AppConfig.preDownloadNum == 0) return
+        if (downloadCacheSettingsGateway.currentSettings.preDownloadNum == 0) return
         cacheBookJob?.cancel()
         cacheBookJob = viewModelScope.launch(updateDispatcher) {
             launch {

+ 8 - 9
app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt

@@ -398,13 +398,6 @@ fun ExploreSourceHeader(
             colors = ListItemDefaults.colors(
                 containerColor = Color.Transparent
             ),
-            headlineContent = {
-                AppText(
-                    text = item.bookSourceName,
-                    style = LegadoTheme.typography.titleMedium,
-                    color = contentColor
-                )
-            },
             trailingContent = {
                 AnimatedContent(
                     targetState = loadingKinds,
@@ -466,7 +459,13 @@ fun ExploreSourceHeader(
                         onClick = { onDelete(); showMenu = false }
                     )
                 }
-            }
-        )
+            },
+        ) {
+            AppText(
+                text = item.bookSourceName,
+                style = LegadoTheme.typography.titleMedium,
+                color = contentColor
+            )
+        }
     }
 }

+ 1 - 1
app/src/main/java/io/legado/app/ui/main/homepage/modules/ButtonGroupModule.kt

@@ -115,7 +115,7 @@ fun ButtonGroupModule(
                                 ) {
                                     if (hasIcon) {
                                         SourceIcon(
-                                            path = buttonIcon!!,
+                                            path = buttonIcon,
                                             modifier = Modifier.size(20.dp),
                                             placeholderIcon = {
 

+ 7 - 3
app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt

@@ -12,10 +12,10 @@ import io.legado.app.data.repository.ReadSettingsRepository
 import io.legado.app.data.repository.ReplaceRuleRepository
 import io.legado.app.data.repository.UploadRepository
 import io.legado.app.domain.gateway.BookContentProcessGateway
+import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.domain.model.TextProcessAction
 import io.legado.app.domain.model.TextProcessAnchor
 import io.legado.app.help.ReplaceAnalyzer
-import io.legado.app.help.config.AppConfig
 import io.legado.app.model.ReadBook
 import io.legado.app.ui.book.read.ContentProcessConfigUiState
 import io.legado.app.ui.book.read.ContentProcessItemUi
@@ -55,6 +55,7 @@ class ReplaceRuleViewModel(
     private val bookContentProcessGateway: BookContentProcessGateway,
     private val readSettingsRepository: ReadSettingsRepository,
     private val repository: ReplaceRuleRepository,
+    private val otherSettingsGateway: OtherSettingsGateway,
 ) : BaseRuleViewModel<ReplaceRuleItemUi, ReplaceRule, Long, ReplaceRuleUiState>(
     application,
     ReplaceRuleUiState(interaction = InteractionState(isLoading = true)),
@@ -419,7 +420,8 @@ class ReplaceRuleViewModel(
         val chapterInput = ReadBook.readerChapterInputWindow.current
         if (book != null && book.bookUrl == bookUrl) {
             val effectiveRules = chapterInput?.content?.effectiveReplaceRules.orEmpty().toImmutableList()
-            val replaceEnabled = book.getUseReplaceRule(AppConfig.replaceEnableDefault)
+            val replaceEnabled =
+                book.getUseReplaceRule(otherSettingsGateway.currentSettings.replaceEnableDefault)
             val chineseConvertActive = readSettingsRepository.currentSettings.chineseConverterType > 0
             val reSegmentActive = book.getReSegment()
             _bookState.update {
@@ -436,7 +438,9 @@ class ReplaceRuleViewModel(
 
     private fun toggleReplaceEnable() {
         ReadBook.book?.let { book ->
-            val enabled = !book.getUseReplaceRule(AppConfig.replaceEnableDefault)
+            val enabled = !book.getUseReplaceRule(
+                otherSettingsGateway.currentSettings.replaceEnableDefault
+            )
             book.setUseReplaceRule(enabled)
             ReadBook.saveRead()
             _bookState.update { it.copy(replaceEnabled = enabled) }

+ 3 - 3
app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt

@@ -17,7 +17,6 @@ import io.legado.app.domain.gateway.AppShellSettingsGateway
 import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.TTS
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.http.newCallResponseBody
 import io.legado.app.help.http.okHttpClient
 import io.legado.app.help.webView.WebJsExtensions
@@ -56,7 +55,7 @@ data class ReadRssSettings(
 class ReadRssViewModel(
     application: Application,
     appShellSettingsGateway: AppShellSettingsGateway,
-    downloadCacheSettingsGateway: DownloadCacheSettingsGateway,
+    private val downloadCacheSettingsGateway: DownloadCacheSettingsGateway,
     private val rssRepository: RssRepository,
     private val articleRepository: RssArticleRepository,
     private val favoriteRepository: RssFavoriteRepository,
@@ -106,7 +105,8 @@ class ReadRssViewModel(
             rssSource = rssRepository.getByKey(args.origin)
             hasPreloadJs = !rssSource?.preloadJs.isNullOrBlank()
             headerMap = runScriptWithContext {
-                rssSource?.getHeaderMap(AppConfig.userAgent) ?: emptyMap()
+                rssSource?.getHeaderMap(downloadCacheSettingsGateway.currentSettings.userAgent)
+                    ?: emptyMap()
             }
             isStartPage = args.startPage
             if (isStartPage) {

+ 2 - 2
app/src/main/java/io/legado/app/ui/rss/read/RssReadRouteScreen.kt

@@ -27,7 +27,7 @@ import androidx.compose.material.icons.filled.Refresh
 import androidx.compose.material.icons.filled.Share
 import androidx.compose.material.icons.filled.Star
 import androidx.compose.material.icons.filled.Stop
-import androidx.compose.material.icons.filled.VolumeUp
+import androidx.compose.material.icons.automirrored.filled.VolumeUp
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
 import androidx.compose.runtime.Composable
@@ -348,7 +348,7 @@ fun RssReadRouteScreen(
                                         R.string.read_aloud
                                     ),
                                     leadingIcon = {
-                                        MenuItemIcon(if (isSpeaking) Icons.Default.Stop else Icons.Default.VolumeUp)
+                                        MenuItemIcon(if (isSpeaking) Icons.Default.Stop else Icons.AutoMirrored.Filled.VolumeUp)
                                     },
                                     onClick = {
                                         dismiss()

+ 2 - 2
app/src/main/java/io/legado/app/ui/widget/BatteryViewOrgin.kt

@@ -11,7 +11,6 @@ import android.text.StaticLayout
 import android.util.AttributeSet
 import androidx.annotation.ColorInt
 import androidx.appcompat.widget.AppCompatTextView
-import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
 import io.legado.app.utils.canvasrecorder.recordIfNeededThenDraw
 import io.legado.app.utils.dpToPx
@@ -20,6 +19,7 @@ class BatteryViewOrgin @JvmOverloads constructor(
     context: Context,
     attrs: AttributeSet? = null
 ) : AppCompatTextView(context, attrs) {
+    private val readSettingsGateway get() = org.koin.core.context.GlobalContext.get().get<io.legado.app.domain.gateway.ReadSettingsGateway>()
     private val batteryTypeface by lazy {
         Typeface.createFromAsset(context.assets, "font/number.ttf")
     }
@@ -75,7 +75,7 @@ class BatteryViewOrgin @JvmOverloads constructor(
     }
 
     override fun onDraw(canvas: Canvas) {
-        if (ReadConfig.optimizeRender) {
+        if (readSettingsGateway.currentSettings.optimizeRender) {
             canvasRecorder.recordIfNeededThenDraw(canvas, width, height) {
                 super.onDraw(this)
                 drawBattery(this)

+ 3 - 3
app/src/main/java/io/legado/app/ui/widget/components/AppTextField.kt

@@ -41,7 +41,7 @@ fun AppTextField(
     readOnly: Boolean = false,
     backgroundColor: Color = Color.Unspecified,
     label: String? = null,
-    labelPosition: TextFieldLabelPosition = TextFieldLabelPosition.Attached(),
+    labelPosition: TextFieldLabelPosition = TextFieldLabelPosition.Inside(),
     placeholder: @Composable (() -> Unit)? = null,
     leadingIcon: @Composable (() -> Unit)? = null,
     trailingIcon: @Composable (() -> Unit)? = null,
@@ -241,7 +241,7 @@ fun AppTextFieldSurface(
     enabled: Boolean = true,
     readOnly: Boolean = false,
     label: String? = null,
-    labelPosition: TextFieldLabelPosition = TextFieldLabelPosition.Attached(),
+    labelPosition: TextFieldLabelPosition = TextFieldLabelPosition.Inside(),
     placeholder: @Composable (() -> Unit)? = null,
     leadingIcon: @Composable (() -> Unit)? = null,
     trailingIcon: @Composable (() -> Unit)? = null,
@@ -297,7 +297,7 @@ fun AppDenseTextField(
     readOnly: Boolean = false,
     backgroundColor: Color = Color.Unspecified,
     label: String? = null,
-    labelPosition: TextFieldLabelPosition = TextFieldLabelPosition.Attached(),
+    labelPosition: TextFieldLabelPosition = TextFieldLabelPosition.Inside(),
     placeholder: @Composable (() -> Unit)? = null,
     leadingIcon: @Composable (() -> Unit)? = null,
     trailingIcon: @Composable (() -> Unit)? = null,

+ 8 - 9
app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt

@@ -207,14 +207,6 @@ fun SelectionItemCardContent(
         } else {
             ListItem(
                 modifier = Modifier.weight(1f),
-                headlineContent = {
-                    AppText(
-                        text = title,
-                        style = LegadoTheme.typography.titleSmall,
-                        maxLines = 1,
-                        overflow = TextOverflow.Ellipsis
-                    )
-                },
                 supportingContent = when {
                     supportingContent != null -> supportingContent
                     !subtitle.isNullOrBlank() -> {
@@ -230,7 +222,14 @@ fun SelectionItemCardContent(
                     else -> null
                 },
                 colors = ListItemDefaults.colors(containerColor = Color.Transparent)
-            )
+            ) {
+                AppText(
+                    text = title,
+                    style = LegadoTheme.typography.titleSmall,
+                    maxLines = 1,
+                    overflow = TextOverflow.Ellipsis
+                )
+            }
         }
 
         Row(

+ 14 - 3
app/src/main/java/io/legado/app/ui/widget/dialog/BottomWebViewDialog.kt

@@ -43,10 +43,11 @@ import io.legado.app.constant.AppLog
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.BaseSource
 import io.legado.app.databinding.DialogWebViewBinding
+import io.legado.app.domain.gateway.AppShellSettingsGateway
 import io.legado.app.domain.gateway.AppUiConfigurationGateway
+import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.WebCacheManager
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.http.newCallResponse
 import io.legado.app.help.http.newCallResponseBody
@@ -69,10 +70,12 @@ import io.legado.app.utils.GSON
 import io.legado.app.utils.applyDayNight
 import io.legado.app.utils.fromJsonObject
 import io.legado.app.utils.invisible
+import io.legado.app.utils.isNightMode
 import io.legado.app.utils.longSnackbar
 import io.legado.app.utils.openUrl
 import io.legado.app.utils.setLayout
 import io.legado.app.utils.startActivity
+import io.legado.app.utils.sysConfiguration
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import io.legado.app.utils.visible
@@ -124,6 +127,14 @@ class BottomWebViewDialog() : BottomSheetDialogFragment(R.layout.dialog_web_view
     }
     private val displayMetrics by lazy { resources.displayMetrics }
     private val appUiConfigurationGateway by inject<AppUiConfigurationGateway>()
+    private val otherSettingsGateway by inject<OtherSettingsGateway>()
+    private val appShellSettingsGateway by inject<AppShellSettingsGateway>()
+    private val isNightTheme: Boolean
+        get() = when (appShellSettingsGateway.currentSettings.themeMode) {
+            "1" -> false
+            "2" -> true
+            else -> sysConfiguration.isNightMode
+        }
     private var appliedDarkTheme: Boolean? = null
     private val selectImageDir = registerForActivityResult(HandleFileContract()) {
         it.uri?.let { uri ->
@@ -727,7 +738,7 @@ class BottomWebViewDialog() : BottomSheetDialogFragment(R.layout.dialog_web_view
         }
 
         override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
-            if (!AppConfig.recordLog) return false
+            if (!otherSettingsGateway.currentSettings.recordLog) return false
             val source = source ?: return false
             val messageLevel = consoleMessage.messageLevel().name
             val message = consoleMessage.message()
@@ -765,7 +776,7 @@ class BottomWebViewDialog() : BottomSheetDialogFragment(R.layout.dialog_web_view
 
         override fun onPageFinished(view: WebView?, url: String?) {
             super.onPageFinished(view, url)
-            applyWebViewTheme(appliedDarkTheme ?: AppConfig.isNightTheme, force = true)
+            applyWebViewTheme(appliedDarkTheme ?: isNightTheme, force = true)
         }
 
         private fun shouldOverrideUrlLoading(url: Uri): Boolean {

+ 27 - 14
app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt

@@ -20,15 +20,18 @@ import com.bumptech.glide.request.RequestListener
 import com.bumptech.glide.request.RequestOptions
 import com.bumptech.glide.request.target.Target
 import io.legado.app.constant.AppPattern
-import io.legado.app.help.config.AppConfig
+import io.legado.app.domain.gateway.AppShellSettingsGateway
+import io.legado.app.domain.gateway.CoverSettingsGateway
 import io.legado.app.help.glide.ImageLoader
 import io.legado.app.help.glide.OkHttpModelLoader
 import io.legado.app.model.BookCover
-import io.legado.app.ui.config.coverConfig.CoverConfig
+import io.legado.app.utils.isNightMode
 import io.legado.app.utils.spToPx
+import io.legado.app.utils.sysConfiguration
 import io.legado.app.utils.textHeight
 import io.legado.app.utils.themeColor
 import io.legado.app.utils.toStringArray
+import org.koin.core.context.GlobalContext
 
 /**
  * 封面
@@ -51,9 +54,19 @@ class CoverImageView @JvmOverloads constructor(
     private var author: String? = null
     private var nameHeight = 0f
     private var authorHeight = 0f
-    private val isNightTheme get() = AppConfig.isNightTheme
-    private val colorKey get() = if (isNightTheme) CoverConfig.coverTextColorN else CoverConfig.coverTextColor
-    private val shadowKey get() = if (isNightTheme) CoverConfig.coverShadowColorN else CoverConfig.coverShadowColor
+    private val coverSettingsGateway
+        get() = GlobalContext.get().get<CoverSettingsGateway>()
+    private val coverSettings get() = coverSettingsGateway.currentSettings
+    private val appShellSettingsGateway
+        get() = GlobalContext.get().get<AppShellSettingsGateway>()
+    private val isNightTheme: Boolean
+        get() = when (appShellSettingsGateway.currentSettings.themeMode) {
+            "1" -> false
+            "2" -> true
+            else -> sysConfiguration.isNightMode
+        }
+    private val colorKey get() = if (isNightTheme) coverSettings.textColorDark else coverSettings.textColor
+    private val shadowKey get() = if (isNightTheme) coverSettings.shadowColorDark else coverSettings.shadowColor
     private val namePaint by lazy {
         val textPaint = TextPaint()
         textPaint.typeface = Typeface.DEFAULT_BOLD
@@ -128,14 +141,14 @@ class CoverImageView @JvmOverloads constructor(
     private fun drawNameAuthor(canvas: Canvas) {
         var startX = width * 0.2f
         var startY = viewHeight * 0.2f
-        val showName = if (isNightTheme) CoverConfig.coverShowNameN else CoverConfig.coverShowName
+        val showName = if (isNightTheme) coverSettings.showNameDark else coverSettings.showName
         if (showName) {
             name?.toStringArray()?.let { nameChars ->
                 val textSize = viewWidth / 8
                 namePaint.textSize = textSize
                 namePaint.strokeWidth = textSize / 8
 
-                if (CoverConfig.coverShowShadow) {
+                if (coverSettings.showShadow) {
                     namePaint.setShadowLayer(
                         4f,
                         0f,
@@ -147,12 +160,12 @@ class CoverImageView @JvmOverloads constructor(
                 }
 
                 nameChars.forEach { char ->
-                    if (CoverConfig.coverShowStroke) {
+                    if (coverSettings.showStroke) {
                         namePaint.color = Color.WHITE
                         namePaint.style = Paint.Style.STROKE
                         canvas.drawText(char, startX, startY, namePaint)
                     }
-                    if (CoverConfig.coverDefaultColor)
+                    if (coverSettings.useDefaultColor)
                         namePaint.color = context.themeColor(com.google.android.material.R.attr.colorSecondary)
                     else
                         namePaint.color = colorKey
@@ -171,7 +184,7 @@ class CoverImageView @JvmOverloads constructor(
         }
 
         val showAuthor =
-            if (isNightTheme) CoverConfig.coverShowAuthorN else CoverConfig.coverShowAuthor
+            if (isNightTheme) coverSettings.showAuthorDark else coverSettings.showAuthor
         if (showAuthor) {
             author?.toStringArray()?.let { author ->
                 authorPaint.textSize = viewWidth / 10
@@ -179,7 +192,7 @@ class CoverImageView @JvmOverloads constructor(
                 startX = width * 0.8f
                 startY = viewHeight * 0.95f - author.size * authorPaint.textHeight
                 startY = maxOf(startY, viewHeight * 0.2f)
-                if (CoverConfig.coverShowShadow) {
+                if (coverSettings.showShadow) {
                     authorPaint.setShadowLayer(
                         4f,
                         0f,
@@ -191,12 +204,12 @@ class CoverImageView @JvmOverloads constructor(
                 }
 
                 author.forEach {
-                    if (CoverConfig.coverShowStroke) {
+                    if (coverSettings.showStroke) {
                         authorPaint.color = Color.WHITE
                         authorPaint.style = Paint.Style.STROKE
                         canvas.drawText(it, startX, startY, authorPaint)
                     }
-                    if (CoverConfig.coverDefaultColor)
+                    if (coverSettings.useDefaultColor)
                         authorPaint.color = context.themeColor(com.google.android.material.R.attr.colorSecondary)
                     else
                         authorPaint.color = colorKey
@@ -262,7 +275,7 @@ class CoverImageView @JvmOverloads constructor(
         this.author = author?.replace(AppPattern.bdRegex, "")?.trim()
         defaultCover = true
         invalidate()
-        if (AppConfig.useDefaultCover) {
+        if (coverSettings.useDefaultCover) {
             ImageLoader.load(context, BookCover.defaultDrawable)
                 .centerCrop()
                 .into(this)

+ 7 - 3
app/src/main/java/io/legado/app/utils/AnimationExtensions.kt

@@ -4,12 +4,16 @@ import android.content.Context
 import android.view.animation.Animation
 import android.view.animation.AnimationUtils
 import androidx.annotation.AnimRes
-import io.legado.app.help.config.AppConfig
+import io.legado.app.domain.gateway.ThemeSettingsGateway
+import org.koin.core.context.GlobalContext
+
+private val themeSettingsGateway
+    get() = GlobalContext.get().get<ThemeSettingsGateway>()
 
 fun loadAnimation(context: Context, @AnimRes id: Int): Animation {
     val animation = AnimationUtils.loadAnimation(context, id)
-    if (AppConfig.isEInkMode) {
+    if (themeSettingsGateway.currentSettings.appTheme == "4") {
         animation.duration = 0
     }
     return animation
-}
+}

+ 17 - 10
app/src/main/java/io/legado/app/utils/ContextExtensions.kt

@@ -41,13 +41,14 @@ import io.legado.app.data.entities.Book
 import io.legado.app.help.IntentHelp
 import io.legado.app.help.book.isAudio
 import io.legado.app.help.book.isImage
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfigStore
 import io.legado.app.help.config.SettingsWriter
-import io.legado.app.ui.config.readMangaConfig.ReadMangaConfig
+import io.legado.app.domain.gateway.BookshelfSettingsGateway
+import io.legado.app.domain.gateway.MangaSettingsGateway
 import io.legado.app.ui.main.MainActivity
 import io.legado.app.ui.main.bookshelf.BookShelfItem
 import kotlinx.coroutines.runBlocking
+import org.koin.core.context.GlobalContext
 import splitties.systemservices.clipboardManager
 import splitties.systemservices.connectivityManager
 import splitties.systemservices.uiModeManager
@@ -62,19 +63,25 @@ inline fun <reified A : Activity> Context.startActivity(configIntent: Intent.()
     startActivity(intent)
 }
 
+private val mangaSettingsGateway
+    get() = GlobalContext.get().get<MangaSettingsGateway>()
+
+private val bookshelfSettingsGateway
+    get() = GlobalContext.get().get<BookshelfSettingsGateway>()
+
 fun Context.startActivityForBook(
     book: Book,
     configIntent: Intent.() -> Unit = {},
 ) {
     val intent = when {
         book.isAudio -> MainActivity.createAudioPlayIntent(this, book.bookUrl)
-        book.isImage && ReadMangaConfig.showMangaUi ->
+        book.isImage && mangaSettingsGateway.currentSettings.showMangaUi ->
             MainActivity.createReadMangaIntent(this, book.bookUrl)
 
         else -> MainActivity.createReadBookIntent(this, book.bookUrl)
     }
     intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-    if (book.isAudio || (book.isImage && ReadMangaConfig.showMangaUi)) {
+    if (book.isAudio || (book.isImage && mangaSettingsGateway.currentSettings.showMangaUi)) {
         intent.putExtra("bookUrl", book.bookUrl)
     }
     intent.apply(configIntent)
@@ -87,13 +94,13 @@ fun Context.startActivityForBook(
 ) {
     val intent = when {
         book.isAudio -> MainActivity.createAudioPlayIntent(this, book.bookUrl)
-        book.isImage && ReadMangaConfig.showMangaUi ->
+        book.isImage && mangaSettingsGateway.currentSettings.showMangaUi ->
             MainActivity.createReadMangaIntent(this, book.bookUrl)
 
         else -> MainActivity.createReadBookIntent(this, book.bookUrl)
     }
     intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-    if (book.isAudio || (book.isImage && ReadMangaConfig.showMangaUi)) {
+    if (book.isAudio || (book.isImage && mangaSettingsGateway.currentSettings.showMangaUi)) {
         intent.putExtra("bookUrl", book.bookUrl)
     }
     intent.apply(configIntent)
@@ -457,16 +464,16 @@ val Context.isDebuggable: Boolean
 
 val Context.bookshelfLayoutMode: Int
     get() = if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
-        AppConfig.bookshelfLayoutModeLandscape
+        bookshelfSettingsGateway.currentSettings.bookshelfLayoutModeLandscape
     } else {
-        AppConfig.bookshelfLayoutModePortrait
+        bookshelfSettingsGateway.currentSettings.bookshelfLayoutModePortrait
     }
 
 val Context.bookshelfLayoutGrid: Int
     get() = if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
-        AppConfig.bookshelfLayoutGridLandscape
+        bookshelfSettingsGateway.currentSettings.bookshelfLayoutGridLandscape
     } else {
-        AppConfig.bookshelfLayoutGridPortrait
+        bookshelfSettingsGateway.currentSettings.bookshelfLayoutGridPortrait
     }
 
 fun Context.themeColor(attr: Int): Int {

+ 7 - 2
app/src/main/java/io/legado/app/utils/CustomExportUtils.kt

@@ -1,17 +1,22 @@
 package io.legado.app.utils
 
-import io.legado.app.help.config.AppConfig
+import io.legado.app.domain.gateway.BookExportSettingsGateway
+import org.koin.core.context.GlobalContext
 
 // 匹配待“输入的章节”字符串
 private val regexEpisode = Regex("\\d+(-\\d+)?(,\\d+(-\\d+)?)*")
 
+private val bookExportSettingsGateway
+    get() = GlobalContext.get().get<BookExportSettingsGateway>()
+
 /**
  * 是否启用自定义导出
  *
  * @author Discut
  */
 fun enableCustomExport(): Boolean {
-    return AppConfig.enableCustomExport && AppConfig.exportType == 1
+    val exportSettings = bookExportSettingsGateway.currentSettings
+    return exportSettings.enableCustomExport && exportSettings.exportType == 1
 }
 
 /**

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff