فهرست منبع

refactor: 迁移 Other 设置网关

5151561 3 ماه پیش
والد
کامیت
37b0c40193
17فایلهای تغییر یافته به همراه188 افزوده شده و 128 حذف شده
  1. 34 28
      app/src/main/java/io/legado/app/data/repository/FeatureSettingsRepositories.kt
  2. 1 27
      app/src/main/java/io/legado/app/domain/gateway/OtherSettingsGateway.kt
  3. 1 2
      app/src/main/java/io/legado/app/service/WebService.kt
  4. 3 2
      app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt
  5. 6 6
      app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt
  6. 6 6
      app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt
  7. 3 6
      app/src/main/java/io/legado/app/ui/book/audio/AudioPlayActivity.kt
  8. 8 8
      app/src/main/java/io/legado/app/ui/book/import/local/ImportBookViewModel.kt
  9. 1 2
      app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookViewModel.kt
  10. 1 2
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  11. 1 2
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  12. 26 27
      app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt
  13. 1 2
      app/src/main/java/io/legado/app/ui/welcome/BookFolderFragment.kt
  14. 1 2
      app/src/main/java/io/legado/app/ui/widget/components/FontSelectGrid.kt
  15. 90 0
      app/src/test/java/io/legado/app/data/repository/OtherSettingsMappingTest.kt
  16. 3 2
      app/src/test/java/io/legado/app/ui/about/AboutViewModelTest.kt
  17. 2 4
      app/src/test/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModelTest.kt

+ 34 - 28
app/src/main/java/io/legado/app/data/repository/FeatureSettingsRepositories.kt

@@ -12,7 +12,6 @@ import io.legado.app.domain.gateway.CoverSettingsGateway
 import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
 import io.legado.app.domain.gateway.LabSettingsGateway
 import io.legado.app.domain.gateway.OtherSettingsGateway
-import io.legado.app.domain.gateway.OtherSettingsUpdate
 import io.legado.app.domain.gateway.ThemeBooleanSetting
 import io.legado.app.domain.gateway.ThemeColorSlot
 import io.legado.app.domain.gateway.ThemeFloatSetting
@@ -535,6 +534,34 @@ internal fun Preferences.toOtherSettings(): OtherSettings {
     )
 }
 
+internal fun OtherSettings.toPrefMap(): Map<String, Any?> = mapOf(
+    PreferKey.updateToVariant to updateToVariant,
+    PreferKey.autoCheckUpdateOnStart to autoCheckUpdateOnStart,
+    PreferKey.webServiceAutoStart to webServiceAutoStart,
+    PreferKey.autoRefresh to autoRefresh,
+    PreferKey.defaultToRead to defaultToRead,
+    PreferKey.notificationsPost to notificationsPost,
+    PreferKey.ignoreBatteryPermission to ignoreBatteryPermission,
+    PreferKey.firebaseEnable to firebaseEnable,
+    PreferKey.defaultBookTreeUri to defaultBookTreeUri,
+    PreferKey.antiAlias to antiAlias,
+    PreferKey.replaceEnableDefault to replaceEnableDefault,
+    PreferKey.autoClearExpired to autoClearExpired,
+    PreferKey.showAddToShelfAlert to showAddToShelfAlert,
+    PreferKey.showMangaUi to showMangaUi,
+    PreferKey.webServiceWakeLock to webServiceWakeLock,
+    PreferKey.sourceEditMaxLine to sourceEditMaxLine,
+    PreferKey.webPort to webPort,
+    PreferKey.processText to processText,
+    PreferKey.recordLog to recordLog,
+    PreferKey.recordHeapDump to recordHeapDump,
+    PreferKey.audioPlayWakeLock to audioPlayUseWakeLock,
+    PreferKey.importKeepName to importKeepName,
+    PreferKey.importKeepGroup to importKeepGroup,
+    PreferKey.importKeepEnable to importKeepEnable,
+    PreferKey.fontSort to fontSort,
+)
+
 class OtherSettingsRepository : OtherSettingsGateway {
     override val currentSettings: OtherSettings
         get() = AppConfigStore.preferences.toOtherSettings()
@@ -543,32 +570,11 @@ class OtherSettingsRepository : OtherSettingsGateway {
         .map { it.toOtherSettings() }
         .distinctUntilChanged()
 
-    override suspend fun update(update: OtherSettingsUpdate) {
-        val (key, value) = when (update) {
-            is OtherSettingsUpdate.UpdateToVariant -> PreferKey.updateToVariant to update.value
-            is OtherSettingsUpdate.AutoCheckUpdateOnStart -> PreferKey.autoCheckUpdateOnStart to update.value
-            is OtherSettingsUpdate.WebServiceAutoStart -> PreferKey.webServiceAutoStart to update.value
-            is OtherSettingsUpdate.AutoRefresh -> PreferKey.autoRefresh to update.value
-            is OtherSettingsUpdate.DefaultToRead -> PreferKey.defaultToRead to update.value
-            is OtherSettingsUpdate.FirebaseEnable -> PreferKey.firebaseEnable to update.value
-            is OtherSettingsUpdate.DefaultBookTreeUri -> PreferKey.defaultBookTreeUri to update.value
-            is OtherSettingsUpdate.AntiAlias -> PreferKey.antiAlias to update.value
-            is OtherSettingsUpdate.ReplaceEnableDefault -> PreferKey.replaceEnableDefault to update.value
-            is OtherSettingsUpdate.AutoClearExpired -> PreferKey.autoClearExpired to update.value
-            is OtherSettingsUpdate.ShowAddToShelfAlert -> PreferKey.showAddToShelfAlert to update.value
-            is OtherSettingsUpdate.ShowMangaUi -> PreferKey.showMangaUi to update.value
-            is OtherSettingsUpdate.WebServiceWakeLock -> PreferKey.webServiceWakeLock to update.value
-            is OtherSettingsUpdate.SourceEditMaxLine -> PreferKey.sourceEditMaxLine to update.value
-            is OtherSettingsUpdate.WebPort -> PreferKey.webPort to update.value
-            is OtherSettingsUpdate.ProcessText -> PreferKey.processText to update.value
-            is OtherSettingsUpdate.RecordLog -> PreferKey.recordLog to update.value
-            is OtherSettingsUpdate.RecordHeapDump -> PreferKey.recordHeapDump to update.value
-            is OtherSettingsUpdate.AudioPlayUseWakeLock -> PreferKey.audioPlayWakeLock to update.value
-            is OtherSettingsUpdate.ImportKeepName -> PreferKey.importKeepName to update.value
-            is OtherSettingsUpdate.ImportKeepGroup -> PreferKey.importKeepGroup to update.value
-            is OtherSettingsUpdate.ImportKeepEnable -> PreferKey.importKeepEnable to update.value
-            is OtherSettingsUpdate.FontSort -> PreferKey.fontSort to update.value
-        }
-        AppConfigStore.putAllAndAwait(mapOf(key to value))
+    override suspend fun update(transform: (OtherSettings) -> OtherSettings) {
+        AppConfigStore.atomicUpdateAndAwait(
+            read = Preferences::toOtherSettings,
+            toPrefMap = OtherSettings::toPrefMap,
+            transform = transform,
+        )
     }
 }

+ 1 - 27
app/src/main/java/io/legado/app/domain/gateway/OtherSettingsGateway.kt

@@ -6,31 +6,5 @@ import kotlinx.coroutines.flow.Flow
 interface OtherSettingsGateway {
     val currentSettings: OtherSettings
     val settings: Flow<OtherSettings>
-    suspend fun update(update: OtherSettingsUpdate)
-}
-
-sealed interface OtherSettingsUpdate {
-    data class UpdateToVariant(val value: String) : OtherSettingsUpdate
-    data class AutoCheckUpdateOnStart(val value: Boolean) : OtherSettingsUpdate
-    data class WebServiceAutoStart(val value: Boolean) : OtherSettingsUpdate
-    data class AutoRefresh(val value: Boolean) : OtherSettingsUpdate
-    data class DefaultToRead(val value: Boolean) : OtherSettingsUpdate
-    data class FirebaseEnable(val value: Boolean) : OtherSettingsUpdate
-    data class DefaultBookTreeUri(val value: String?) : OtherSettingsUpdate
-    data class AntiAlias(val value: Boolean) : OtherSettingsUpdate
-    data class ReplaceEnableDefault(val value: Boolean) : OtherSettingsUpdate
-    data class AutoClearExpired(val value: Boolean) : OtherSettingsUpdate
-    data class ShowAddToShelfAlert(val value: Boolean) : OtherSettingsUpdate
-    data class ShowMangaUi(val value: Boolean) : OtherSettingsUpdate
-    data class WebServiceWakeLock(val value: Boolean) : OtherSettingsUpdate
-    data class SourceEditMaxLine(val value: Int) : OtherSettingsUpdate
-    data class WebPort(val value: Int) : OtherSettingsUpdate
-    data class ProcessText(val value: Boolean) : OtherSettingsUpdate
-    data class RecordLog(val value: Boolean) : OtherSettingsUpdate
-    data class RecordHeapDump(val value: Boolean) : OtherSettingsUpdate
-    data class AudioPlayUseWakeLock(val value: Boolean) : OtherSettingsUpdate
-    data class ImportKeepName(val value: Boolean) : OtherSettingsUpdate
-    data class ImportKeepGroup(val value: Boolean) : OtherSettingsUpdate
-    data class ImportKeepEnable(val value: Boolean) : OtherSettingsUpdate
-    data class FontSort(val value: Int) : OtherSettingsUpdate
+    suspend fun update(transform: (OtherSettings) -> OtherSettings)
 }

+ 1 - 2
app/src/main/java/io/legado/app/service/WebService.kt

@@ -17,7 +17,6 @@ import io.legado.app.constant.NotificationId
 import io.legado.app.constant.PreferKey
 import io.legado.app.receiver.NetworkChangedListener
 import io.legado.app.domain.gateway.OtherSettingsGateway
-import io.legado.app.domain.gateway.OtherSettingsUpdate
 import io.legado.app.utils.NetworkUtils
 import io.legado.app.utils.eventBus.FlowEventBus
 import io.legado.app.utils.getPrefBoolean
@@ -126,7 +125,7 @@ class WebService : BaseService() {
             IntentAction.stop -> {
                 // ——————【修改开始】通知栏点击停止时,也记录关闭状态——————
                 lifecycleScope.launch {
-                    otherSettingsGateway.update(OtherSettingsUpdate.WebServiceAutoStart(false))
+                    otherSettingsGateway.update { it.copy(webServiceAutoStart = false) }
                 }
                 stopSelf()
                 // ——————【修改结束】——————

+ 3 - 2
app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt

@@ -11,7 +11,6 @@ import io.legado.app.base.VMBaseActivity
 import io.legado.app.constant.AppLog
 import io.legado.app.databinding.ActivityTranslucenceBinding
 import io.legado.app.domain.gateway.OtherSettingsGateway
-import io.legado.app.domain.gateway.OtherSettingsUpdate
 import io.legado.app.exception.InvalidBooksDirException
 import io.legado.app.lib.dialogs.alert
 import io.legado.app.lib.permission.Permissions
@@ -48,7 +47,9 @@ class FileAssociationActivity :
         intent.data?.let { uri ->
             it.uri?.let { treeUri ->
                 lifecycleScope.launch {
-                    otherSettingsGateway.update(OtherSettingsUpdate.DefaultBookTreeUri(treeUri.toString()))
+                    otherSettingsGateway.update {
+                        it.copy(defaultBookTreeUri = treeUri.toString())
+                    }
                 }
                 importBook(treeUri, uri)
             } ?: let {

+ 6 - 6
app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt

@@ -22,7 +22,7 @@ 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.gateway.OtherSettingsUpdate
+import io.legado.app.domain.model.settings.OtherSettings
 import io.legado.app.lib.dialogs.alert
 //import io.legado.app.lib.theme.primaryColor
 import io.legado.app.ui.widget.dialog.CodeDialog
@@ -178,24 +178,24 @@ class ImportBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_r
 
             R.id.menu_keep_original_name -> {
                 item.isChecked = !item.isChecked
-                updateImportSetting(OtherSettingsUpdate.ImportKeepName(item.isChecked))
+                updateImportSetting { it.copy(importKeepName = item.isChecked) }
             }
 
             R.id.menu_keep_group -> {
                 item.isChecked = !item.isChecked
-                updateImportSetting(OtherSettingsUpdate.ImportKeepGroup(item.isChecked))
+                updateImportSetting { it.copy(importKeepGroup = item.isChecked) }
             }
 
             R.id.menu_keep_enable -> {
                 item.isChecked = !item.isChecked
-                updateImportSetting(OtherSettingsUpdate.ImportKeepEnable(item.isChecked))
+                updateImportSetting { it.copy(importKeepEnable = item.isChecked) }
             }
         }
         return false
     }
 
-    private fun updateImportSetting(update: OtherSettingsUpdate) {
-        lifecycleScope.launch { otherSettingsGateway.update(update) }
+    private fun updateImportSetting(transform: (OtherSettings) -> OtherSettings) {
+        lifecycleScope.launch { otherSettingsGateway.update(transform) }
     }
 
     private fun alertCustomGroup(item: MenuItem) {

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

@@ -22,7 +22,7 @@ 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.gateway.OtherSettingsUpdate
+import io.legado.app.domain.model.settings.OtherSettings
 import io.legado.app.lib.dialogs.alert
 //import io.legado.app.lib.theme.primaryColor
 import io.legado.app.ui.widget.dialog.CodeDialog
@@ -158,24 +158,24 @@ class ImportRssSourceDialog() : BaseDialogFragment(R.layout.dialog_recycler_view
             R.id.menu_new_group -> alertCustomGroup(item)
             R.id.menu_keep_original_name -> {
                 item.isChecked = !item.isChecked
-                updateImportSetting(OtherSettingsUpdate.ImportKeepName(item.isChecked))
+                updateImportSetting { it.copy(importKeepName = item.isChecked) }
             }
 
             R.id.menu_keep_group -> {
                 item.isChecked = !item.isChecked
-                updateImportSetting(OtherSettingsUpdate.ImportKeepGroup(item.isChecked))
+                updateImportSetting { it.copy(importKeepGroup = item.isChecked) }
             }
 
             R.id.menu_keep_enable -> {
                 item.isChecked = !item.isChecked
-                updateImportSetting(OtherSettingsUpdate.ImportKeepEnable(item.isChecked))
+                updateImportSetting { it.copy(importKeepEnable = item.isChecked) }
             }
         }
         return false
     }
 
-    private fun updateImportSetting(update: OtherSettingsUpdate) {
-        lifecycleScope.launch { otherSettingsGateway.update(update) }
+    private fun updateImportSetting(transform: (OtherSettings) -> OtherSettings) {
+        lifecycleScope.launch { otherSettingsGateway.update(transform) }
     }
 
     private fun alertCustomGroup(item: MenuItem) {

+ 3 - 6
app/src/main/java/io/legado/app/ui/book/audio/AudioPlayActivity.kt

@@ -45,7 +45,6 @@ import io.legado.app.data.entities.BookSource
 import io.legado.app.databinding.ActivityAudioPlayBinding
 import io.legado.app.domain.model.PlaybackTimer
 import io.legado.app.domain.gateway.OtherSettingsGateway
-import io.legado.app.domain.gateway.OtherSettingsUpdate
 import io.legado.app.domain.gateway.ReadAloudSettingsGateway
 import io.legado.app.help.book.isAudio
 import io.legado.app.help.book.removeType
@@ -215,11 +214,9 @@ class AudioPlayActivity :
                 }
             }
             R.id.menu_wake_lock -> lifecycleScope.launch {
-                otherSettingsGateway.update(
-                    OtherSettingsUpdate.AudioPlayUseWakeLock(
-                        !otherSettingsGateway.currentSettings.audioPlayUseWakeLock
-                    )
-                )
+                otherSettingsGateway.update {
+                    it.copy(audioPlayUseWakeLock = !it.audioPlayUseWakeLock)
+                }
             }
             R.id.menu_copy_audio_url -> {
                 AudioPlay.book?.let {

+ 8 - 8
app/src/main/java/io/legado/app/ui/book/import/local/ImportBookViewModel.kt

@@ -17,8 +17,8 @@ import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
 import io.legado.app.domain.gateway.ImportBookSettingsGateway
 import io.legado.app.domain.gateway.OtherSettingsGateway
-import io.legado.app.domain.gateway.OtherSettingsUpdate
 import io.legado.app.domain.model.settings.ImportBookSettings
+import io.legado.app.domain.model.settings.OtherSettings
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.canSafelyRebindTo
 import io.legado.app.model.localBook.LocalBook
@@ -259,7 +259,7 @@ class ImportBookViewModel(
             return
         }
         if (otherSettingsGateway.currentSettings.defaultBookTreeUri != effectiveDefaultPath) {
-            updateOtherSetting(OtherSettingsUpdate.DefaultBookTreeUri(effectiveDefaultPath))
+            updateOtherSetting { it.copy(defaultBookTreeUri = effectiveDefaultPath) }
         }
         val importPath = importBookSettingsGateway.currentSettings.importBookPath
         if (importPath.isNullOrBlank() || !importPath.isUri()) {
@@ -274,7 +274,7 @@ class ImportBookViewModel(
         val pickedUri = persistFolderPermission(uri)
         when (target) {
             ImportFolderPickTarget.DEFAULT_BOOK -> {
-                updateOtherSetting(OtherSettingsUpdate.DefaultBookTreeUri(pickedUri.toString()))
+                updateOtherSetting { it.copy(defaultBookTreeUri = pickedUri.toString()) }
                 if (importBookSettingsGateway.currentSettings.importBookPath.isNullOrBlank()) {
                     updateImportBookSetting { it.copy(importBookPath = pickedUri.toString()) }
                 }
@@ -283,7 +283,7 @@ class ImportBookViewModel(
             ImportFolderPickTarget.IMPORT_FOLDER -> {
                 updateImportBookSetting { it.copy(importBookPath = pickedUri.toString()) }
                 if (otherSettingsGateway.currentSettings.defaultBookTreeUri.isNullOrBlank()) {
-                    updateOtherSetting(OtherSettingsUpdate.DefaultBookTreeUri(pickedUri.toString()))
+                    updateOtherSetting { it.copy(defaultBookTreeUri = pickedUri.toString()) }
                 }
             }
         }
@@ -328,7 +328,7 @@ class ImportBookViewModel(
         if (candidates.isEmpty()) {
             firstPersistedTreeUri()?.toString()?.let { persistedPath ->
                 if (trySetRootDoc(persistedPath)) {
-                    updateOtherSetting(OtherSettingsUpdate.DefaultBookTreeUri(persistedPath))
+                    updateOtherSetting { it.copy(defaultBookTreeUri = persistedPath) }
                     updateImportBookSetting { it.copy(importBookPath = persistedPath) }
                     return
                 }
@@ -360,7 +360,7 @@ class ImportBookViewModel(
 
         firstPersistedTreeUri()?.toString()?.let { persistedPath ->
             if (trySetRootDoc(persistedPath)) {
-                updateOtherSetting(OtherSettingsUpdate.DefaultBookTreeUri(persistedPath))
+                updateOtherSetting { it.copy(defaultBookTreeUri = persistedPath) }
                 updateImportBookSetting { it.copy(importBookPath = persistedPath) }
                 return
             }
@@ -844,9 +844,9 @@ class ImportBookViewModel(
         }
     }
 
-    private fun updateOtherSetting(update: OtherSettingsUpdate) {
+    private fun updateOtherSetting(transform: (OtherSettings) -> OtherSettings) {
         viewModelScope.launch(start = CoroutineStart.UNDISPATCHED) {
-            otherSettingsGateway.update(update)
+            otherSettingsGateway.update(transform)
         }
     }
 

+ 1 - 2
app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookViewModel.kt

@@ -15,7 +15,6 @@ import io.legado.app.data.entities.Server
 import io.legado.app.data.repository.RemoteBookRepository
 import io.legado.app.domain.gateway.ImportBookSettingsGateway
 import io.legado.app.domain.gateway.OtherSettingsGateway
-import io.legado.app.domain.gateway.OtherSettingsUpdate
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.model.analyzeRule.CustomUrl
 import io.legado.app.model.localBook.LocalBook
@@ -175,7 +174,7 @@ class RemoteBookViewModel(
         uri ?: return
         uri.takePersistablePermissionSafely(context)
         viewModelScope.launch {
-            otherSettingsGateway.update(OtherSettingsUpdate.DefaultBookTreeUri(uri.toString()))
+            otherSettingsGateway.update { it.copy(defaultBookTreeUri = uri.toString()) }
         }
     }
 

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

@@ -29,7 +29,6 @@ import io.legado.app.domain.usecase.ChangeBookSourceUseCase
 import io.legado.app.domain.gateway.ThemeSettingsGateway
 import io.legado.app.domain.gateway.CoverSettingsGateway
 import io.legado.app.domain.gateway.OtherSettingsGateway
-import io.legado.app.domain.gateway.OtherSettingsUpdate
 import io.legado.app.domain.model.settings.CoverSettings
 import io.legado.app.domain.model.settings.OtherSettings
 import io.legado.app.domain.model.settings.ThemeSettings
@@ -359,7 +358,7 @@ class BookInfoViewModel(
             BookInfoIntent.KnowledgeListClick -> openKnowledgeList()
             BookInfoIntent.EventListClick -> openEventList()
             is BookInfoIntent.SetDefaultBookTreeUri -> viewModelScope.launch {
-                otherSettingsGateway.update(OtherSettingsUpdate.DefaultBookTreeUri(intent.value))
+                otherSettingsGateway.update { it.copy(defaultBookTreeUri = intent.value) }
             }
         }
     }

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

@@ -47,7 +47,6 @@ import io.legado.app.domain.gateway.BookContentProcessGateway
 import io.legado.app.domain.gateway.ChangeSourceSettingsGateway
 import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
 import io.legado.app.domain.gateway.OtherSettingsGateway
-import io.legado.app.domain.gateway.OtherSettingsUpdate
 import io.legado.app.domain.gateway.ReadSettingsUpdate
 import io.legado.app.domain.gateway.ReadStyleBooleanKey
 import io.legado.app.domain.gateway.ReadStyleColorKey
@@ -6327,7 +6326,7 @@ class ReadBookViewModel(
 
     private fun onBooksDirSelected(uri: Uri) {
         viewModelScope.launch {
-            otherSettingsGateway.update(OtherSettingsUpdate.DefaultBookTreeUri(uri.toString()))
+            otherSettingsGateway.update { it.copy(defaultBookTreeUri = uri.toString()) }
         }
         val reloadChapterList = pendingBooksDirReloadChapterList
         pendingBooksDirReloadChapterList = false

+ 26 - 27
app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt

@@ -14,7 +14,6 @@ import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
 import io.legado.app.domain.gateway.LocalPasswordGateway
 import io.legado.app.domain.gateway.OtherConfigSystemGateway
 import io.legado.app.domain.gateway.OtherSettingsGateway
-import io.legado.app.domain.gateway.OtherSettingsUpdate
 import io.legado.app.domain.gateway.ReadAloudSettingsGateway
 import io.legado.app.domain.model.settings.OtherSettings
 import io.legado.app.domain.model.settings.ReadAloudSettings
@@ -64,9 +63,7 @@ class OtherConfigViewModel(
                 _uiState.update { it.copy(language = language) }
             }
         }
-        updateOtherSetting(
-            OtherSettingsUpdate.ProcessText(systemGateway.isProcessTextEnabled())
-        )
+        updateOtherSetting { it.copy(processText = systemGateway.isProcessTextEnabled()) }
         viewModelScope.launch {
             otherSettingsGateway.settings.collect { settings ->
                 _uiState.update { settings.toUiState(it) }
@@ -95,23 +92,23 @@ class OtherConfigViewModel(
         when (intent) {
             is OtherConfigIntent.LanguageChanged -> appLocaleGateway.setLanguage(intent.value)
             is OtherConfigIntent.UpdateToVariantChanged ->
-                updateOtherSetting(OtherSettingsUpdate.UpdateToVariant(intent.value))
+                updateOtherSetting { it.copy(updateToVariant = intent.value) }
             is OtherConfigIntent.AutoCheckUpdateOnStartChanged ->
-                updateOtherSetting(OtherSettingsUpdate.AutoCheckUpdateOnStart(intent.value))
+                updateOtherSetting { it.copy(autoCheckUpdateOnStart = intent.value) }
             is OtherConfigIntent.WebServiceAutoStartChanged ->
-                updateOtherSetting(OtherSettingsUpdate.WebServiceAutoStart(intent.value))
+                updateOtherSetting { it.copy(webServiceAutoStart = intent.value) }
             is OtherConfigIntent.AutoRefreshChanged ->
-                updateOtherSetting(OtherSettingsUpdate.AutoRefresh(intent.value))
+                updateOtherSetting { it.copy(autoRefresh = intent.value) }
             is OtherConfigIntent.DefaultToReadChanged ->
-                updateOtherSetting(OtherSettingsUpdate.DefaultToRead(intent.value))
+                updateOtherSetting { it.copy(defaultToRead = intent.value) }
             is OtherConfigIntent.FirebaseEnableChanged ->
-                updateOtherSetting(OtherSettingsUpdate.FirebaseEnable(intent.value))
+                updateOtherSetting { it.copy(firebaseEnable = intent.value) }
             is OtherConfigIntent.DefaultBookTreeUriChanged ->
-                updateOtherSetting(OtherSettingsUpdate.DefaultBookTreeUri(intent.value))
+                updateOtherSetting { it.copy(defaultBookTreeUri = intent.value) }
             is OtherConfigIntent.AntiAliasChanged ->
-                updateOtherSetting(OtherSettingsUpdate.AntiAlias(intent.value))
+                updateOtherSetting { it.copy(antiAlias = intent.value) }
             is OtherConfigIntent.ReplaceEnableDefaultChanged ->
-                updateOtherSetting(OtherSettingsUpdate.ReplaceEnableDefault(intent.value))
+                updateOtherSetting { it.copy(replaceEnableDefault = intent.value) }
             is OtherConfigIntent.MediaButtonOnExitChanged ->
                 updateReadAloudSetting { it.copy(mediaButtonOnExit = intent.value) }
             is OtherConfigIntent.ReadAloudByMediaButtonChanged ->
@@ -119,25 +116,27 @@ class OtherConfigViewModel(
             is OtherConfigIntent.IgnoreAudioFocusChanged ->
                 updateReadAloudSetting { it.copy(ignoreAudioFocus = intent.value) }
             is OtherConfigIntent.AutoClearExpiredChanged ->
-                updateOtherSetting(OtherSettingsUpdate.AutoClearExpired(intent.value))
+                updateOtherSetting { it.copy(autoClearExpired = intent.value) }
             is OtherConfigIntent.ShowAddToShelfAlertChanged ->
-                updateOtherSetting(OtherSettingsUpdate.ShowAddToShelfAlert(intent.value))
+                updateOtherSetting { it.copy(showAddToShelfAlert = intent.value) }
             is OtherConfigIntent.ShowMangaUiChanged ->
-                updateOtherSetting(OtherSettingsUpdate.ShowMangaUi(intent.value))
+                updateOtherSetting { it.copy(showMangaUi = intent.value) }
             is OtherConfigIntent.WebServiceWakeLockChanged ->
-                updateOtherSetting(OtherSettingsUpdate.WebServiceWakeLock(intent.value))
+                updateOtherSetting { it.copy(webServiceWakeLock = intent.value) }
             is OtherConfigIntent.SourceEditMaxLineChanged ->
-                updateOtherSetting(OtherSettingsUpdate.SourceEditMaxLine(intent.value))
+                updateOtherSetting { it.copy(sourceEditMaxLine = intent.value) }
             is OtherConfigIntent.WebPortChanged -> {
-                updateOtherSetting(OtherSettingsUpdate.WebPort(intent.value)) {
-                    _effects.tryEmit(OtherConfigEffect.RestartWebService)
-                }
+                updateOtherSetting(
+                    onSuccess = {
+                        _effects.tryEmit(OtherConfigEffect.RestartWebService)
+                    },
+                ) { it.copy(webPort = intent.value) }
             }
             is OtherConfigIntent.ProcessTextChanged -> setProcessTextEnable(intent.value)
             is OtherConfigIntent.RecordLogChanged ->
-                updateOtherSetting(OtherSettingsUpdate.RecordLog(intent.value))
+                updateOtherSetting { it.copy(recordLog = intent.value) }
             is OtherConfigIntent.RecordHeapDumpChanged ->
-                updateOtherSetting(OtherSettingsUpdate.RecordHeapDump(intent.value))
+                updateOtherSetting { it.copy(recordHeapDump = intent.value) }
             is OtherConfigIntent.CheckSourceTimeoutChanged ->
                 _uiState.update { it.copy(checkSourceTimeoutSeconds = intent.value) }
             is OtherConfigIntent.CheckSearchChanged -> _uiState.update {
@@ -224,11 +223,11 @@ class OtherConfigViewModel(
     }
 
     private fun updateOtherSetting(
-        update: OtherSettingsUpdate,
         onSuccess: () -> Unit = {},
+        transform: (OtherSettings) -> OtherSettings,
     ) {
         viewModelScope.launch {
-            runCatching { otherSettingsGateway.update(update) }
+            runCatching { otherSettingsGateway.update(transform) }
                 .onSuccess { onSuccess() }
                 .onFailure { error ->
                     showMessage(error.message ?: error.javaClass.simpleName)
@@ -250,7 +249,7 @@ class OtherConfigViewModel(
             val previous = systemGateway.isProcessTextEnabled()
             runCatching {
                 systemGateway.setProcessTextEnabled(enable)
-                otherSettingsGateway.update(OtherSettingsUpdate.ProcessText(enable))
+                otherSettingsGateway.update { it.copy(processText = enable) }
             }.onFailure {
                 if (previous != enable) {
                     runCatching { systemGateway.setProcessTextEnabled(previous) }
@@ -293,7 +292,7 @@ class OtherConfigViewModel(
     }
 
     fun updateLocalBookDir(path: String) {
-        updateOtherSetting(OtherSettingsUpdate.DefaultBookTreeUri(path))
+        updateOtherSetting { it.copy(defaultBookTreeUri = path) }
     }
 
     private fun saveCheckSourceConfig() {

+ 1 - 2
app/src/main/java/io/legado/app/ui/welcome/BookFolderFragment.kt

@@ -6,7 +6,6 @@ import io.legado.app.R
 import io.legado.app.base.BaseFragment
 import io.legado.app.databinding.FragmentBookFolderBinding
 import io.legado.app.domain.gateway.OtherSettingsGateway
-import io.legado.app.domain.gateway.OtherSettingsUpdate
 import io.legado.app.ui.file.HandleFileContract
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import androidx.lifecycle.lifecycleScope
@@ -21,7 +20,7 @@ class BookFolderFragment : BaseFragment(R.layout.fragment_book_folder) {
     private val selectBookFolder = registerForActivityResult(HandleFileContract()) { result ->
         result.uri?.let { treeUri ->
             lifecycleScope.launch {
-                otherSettingsGateway.update(OtherSettingsUpdate.DefaultBookTreeUri(treeUri.toString()))
+                otherSettingsGateway.update { it.copy(defaultBookTreeUri = treeUri.toString()) }
                 updatePathText()
             }
         }

+ 1 - 2
app/src/main/java/io/legado/app/ui/widget/components/FontSelectGrid.kt

@@ -49,7 +49,6 @@ import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.domain.gateway.OtherSettingsGateway
-import io.legado.app.domain.gateway.OtherSettingsUpdate
 import io.legado.app.help.loadFontFiles
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.utils.FileDoc
@@ -172,7 +171,7 @@ fun FontSelectGrid(
                                 text = { Text(stringResource(sort.labelRes)) },
                                 onClick = {
                                     scope.launch {
-                                        otherSettings.update(OtherSettingsUpdate.FontSort(sort.ordinal))
+                                        otherSettings.update { it.copy(fontSort = sort.ordinal) }
                                     }
                                     expanded = false
                                 },

+ 90 - 0
app/src/test/java/io/legado/app/data/repository/OtherSettingsMappingTest.kt

@@ -0,0 +1,90 @@
+package io.legado.app.data.repository
+
+import io.legado.app.constant.PreferKey
+import io.legado.app.domain.model.settings.OtherSettings
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class OtherSettingsMappingTest {
+
+    @Test
+    fun `其他设置 25 键写读映射逐字段对应`() {
+        otherMappingSamples().forEach { expected ->
+            assertEquals(expected.expectedPrefMap(), expected.toPrefMap())
+            assertEquals(expected, expected.expectedPrefMap().toTestPreferences().toOtherSettings())
+        }
+    }
+
+    @Test
+    fun `默认书籍目录 nullable 删除走真实原子路径`() {
+        val values = captureAtomicUpdateValues(
+            current = OtherSettings(defaultBookTreeUri = "content://old"),
+            read = { it.toOtherSettings() },
+            toPrefMap = OtherSettings::toPrefMap,
+            transform = { it.copy(defaultBookTreeUri = null) },
+        )
+
+        assertEquals(mapOf(PreferKey.defaultBookTreeUri to null), values)
+    }
+}
+
+private fun otherMappingSamples(): List<OtherSettings> {
+    val base = OtherSettings(
+        updateToVariant = "beta",
+        defaultBookTreeUri = "content://books",
+        sourceEditMaxLine = 123,
+        webPort = 4321,
+        fontSort = 7,
+    )
+    return listOf(
+        base,
+        base.copy(autoCheckUpdateOnStart = true),
+        base.copy(webServiceAutoStart = true),
+        base.copy(autoRefresh = true),
+        base.copy(defaultToRead = true),
+        base.copy(notificationsPost = false),
+        base.copy(ignoreBatteryPermission = false),
+        base.copy(firebaseEnable = false),
+        base.copy(antiAlias = true),
+        base.copy(replaceEnableDefault = false),
+        base.copy(autoClearExpired = false),
+        base.copy(showAddToShelfAlert = false),
+        base.copy(showMangaUi = false),
+        base.copy(webServiceWakeLock = true),
+        base.copy(processText = false),
+        base.copy(recordLog = true),
+        base.copy(recordHeapDump = true),
+        base.copy(audioPlayUseWakeLock = true),
+        base.copy(importKeepName = true),
+        base.copy(importKeepGroup = true),
+        base.copy(importKeepEnable = true),
+    )
+}
+
+private fun OtherSettings.expectedPrefMap(): Map<String, Any?> = mapOf(
+    PreferKey.updateToVariant to updateToVariant,
+    PreferKey.autoCheckUpdateOnStart to autoCheckUpdateOnStart,
+    PreferKey.webServiceAutoStart to webServiceAutoStart,
+    PreferKey.autoRefresh to autoRefresh,
+    PreferKey.defaultToRead to defaultToRead,
+    PreferKey.notificationsPost to notificationsPost,
+    PreferKey.ignoreBatteryPermission to ignoreBatteryPermission,
+    PreferKey.firebaseEnable to firebaseEnable,
+    PreferKey.defaultBookTreeUri to defaultBookTreeUri,
+    PreferKey.antiAlias to antiAlias,
+    PreferKey.replaceEnableDefault to replaceEnableDefault,
+    PreferKey.autoClearExpired to autoClearExpired,
+    PreferKey.showAddToShelfAlert to showAddToShelfAlert,
+    PreferKey.showMangaUi to showMangaUi,
+    PreferKey.webServiceWakeLock to webServiceWakeLock,
+    PreferKey.sourceEditMaxLine to sourceEditMaxLine,
+    PreferKey.webPort to webPort,
+    PreferKey.processText to processText,
+    PreferKey.recordLog to recordLog,
+    PreferKey.recordHeapDump to recordHeapDump,
+    PreferKey.audioPlayWakeLock to audioPlayUseWakeLock,
+    PreferKey.importKeepName to importKeepName,
+    PreferKey.importKeepGroup to importKeepGroup,
+    PreferKey.importKeepEnable to importKeepEnable,
+    PreferKey.fontSort to fontSort,
+)

+ 3 - 2
app/src/test/java/io/legado/app/ui/about/AboutViewModelTest.kt

@@ -3,7 +3,6 @@ package io.legado.app.ui.about
 import android.app.Application
 import android.os.Looper
 import io.legado.app.domain.gateway.OtherSettingsGateway
-import io.legado.app.domain.gateway.OtherSettingsUpdate
 import io.legado.app.domain.model.settings.OtherSettings
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -51,6 +50,8 @@ class AboutViewModelTest {
             state.value = state.value.copy(updateToVariant = variant)
         }
 
-        override suspend fun update(update: OtherSettingsUpdate) = Unit
+        override suspend fun update(transform: (OtherSettings) -> OtherSettings) {
+            state.value = transform(state.value)
+        }
     }
 }

+ 2 - 4
app/src/test/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModelTest.kt

@@ -12,7 +12,6 @@ import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
 import io.legado.app.domain.gateway.LocalPasswordGateway
 import io.legado.app.domain.gateway.OtherConfigSystemGateway
 import io.legado.app.domain.gateway.OtherSettingsGateway
-import io.legado.app.domain.gateway.OtherSettingsUpdate
 import io.legado.app.domain.gateway.ReadAloudSettingsGateway
 import io.legado.app.domain.model.settings.OtherSettings
 import io.legado.app.domain.model.settings.ReadAloudSettings
@@ -191,16 +190,15 @@ class OtherConfigViewModelTest {
 
     private class FakeOtherSettingsGateway : OtherSettingsGateway {
         private val state = MutableStateFlow(OtherSettings())
-        val updates = mutableListOf<OtherSettingsUpdate>()
         var failure: Throwable? = null
 
         override val currentSettings: OtherSettings
             get() = state.value
         override val settings: Flow<OtherSettings> = state
 
-        override suspend fun update(update: OtherSettingsUpdate) {
+        override suspend fun update(transform: (OtherSettings) -> OtherSettings) {
             failure?.let { throw it }
-            updates += update
+            state.value = transform(state.value)
         }
     }