Przeglądaj źródła

修复阅读主题、TTS 音色与字体热更新

5151561 3 miesięcy temu
rodzic
commit
ee05ae3926

+ 10 - 1
app/src/main/java/io/legado/app/data/repository/ReadSettingsRepository.kt

@@ -344,6 +344,9 @@ class ReadSettingsRepository(
             textBottomJustify = compatDsValue(Keys.TextBottomJustify, true),
             adaptSpecialStyle = compatDsValue(Keys.AdaptSpecialStyle, true),
             useZhLayout = compatDsValue(Keys.UseZhLayout, false),
+            eyeProtectionEnabled = compatDsValue(Keys.EyeProtectionEnabled, false),
+            eyeProtectionIntensity = compatDsValue(Keys.EyeProtectionIntensity, 50).coerceIn(0, 100),
+            eyeProtectionAutoNight = compatDsValue(Keys.EyeProtectionAutoNight, false),
             showBrightnessView = compatDsValue(Keys.ShowBrightnessView, "0"),
             brightnessVwPos = compatDsValue(Keys.BrightnessVwPos, "1"),
             readBrightness = compatDsValue(Keys.ReadBrightness, 100),
@@ -456,6 +459,9 @@ class ReadSettingsRepository(
         val TextBottomJustify = booleanPreferencesKey(PreferKey.textBottomJustify)
         val AdaptSpecialStyle = booleanPreferencesKey(PreferKey.adaptSpecialStyle)
         val UseZhLayout = booleanPreferencesKey(PreferKey.useZhLayout)
+        val EyeProtectionEnabled = booleanPreferencesKey(PreferKey.eyeProtectionEnabled)
+        val EyeProtectionIntensity = intPreferencesKey(PreferKey.colorTemperature)
+        val EyeProtectionAutoNight = booleanPreferencesKey(PreferKey.eyeProtectionAutoNight)
         val ShowBrightnessView = stringPreferencesKey(PreferKey.showBrightnessView)
         val BrightnessVwPos = stringPreferencesKey(PreferKey.brightnessVwPos)
         val ReadBrightness = intPreferencesKey(PreferKey.brightness)
@@ -553,7 +559,7 @@ class ReadSettingsRepository(
 }
 
 /**
- * ReadSettings 是 101 字段的读取超集;gateway update 当前只承诺持久化以下 45 个键。
+ * ReadSettings 是读取超集;gateway update 当前只承诺持久化以下 48 个键。
  * 扩展此边界时必须同步更新 ReadSettingsMappingTest 的显式键集契约。
  */
 internal fun ReadSettings.toGatewayPrefMap(): Map<String, Any?> = mapOf(
@@ -570,6 +576,9 @@ internal fun ReadSettings.toGatewayPrefMap(): Map<String, Any?> = mapOf(
     PreferKey.textBottomJustify to textBottomJustify,
     PreferKey.adaptSpecialStyle to adaptSpecialStyle,
     PreferKey.useZhLayout to useZhLayout,
+    PreferKey.eyeProtectionEnabled to eyeProtectionEnabled,
+    PreferKey.colorTemperature to eyeProtectionIntensity.coerceIn(0, 100),
+    PreferKey.eyeProtectionAutoNight to eyeProtectionAutoNight,
     PreferKey.showBrightnessView to showBrightnessView,
     PreferKey.brightnessVwPos to brightnessVwPos,
     PreferKey.brightness to readBrightness,

+ 9 - 3
app/src/main/java/io/legado/app/domain/usecase/SyncReadAloudVoicesUseCase.kt

@@ -22,6 +22,7 @@ class SyncReadAloudVoicesUseCase(
     suspend operator fun invoke(
         entries: List<VoiceCatalogEntry>,
         managedSources: Set<String>,
+        removeMissingEngineTypes: Set<String> = emptySet(),
         now: Long = System.currentTimeMillis(),
     ): SyncReadAloudVoicesResult = syncMutex.withLock {
         val existing = voiceGateway.getVoices()
@@ -79,10 +80,15 @@ class SyncReadAloudVoicesUseCase(
         var unavailable = 0
         existing.asSequence()
             .filter { it.managedBy in managedSources }
-            .filter { it.id !in incomingIds && it.available }
+            .filter { it.id !in incomingIds }
             .forEach { voice ->
-                unavailable++
-                voiceGateway.upsertVoice(voice.copy(available = false, updatedAt = now))
+                if (voice.engineType in removeMissingEngineTypes) {
+                    unavailable++
+                    voiceGateway.deleteVoice(voice)
+                } else if (voice.available) {
+                    unavailable++
+                    voiceGateway.upsertVoice(voice.copy(available = false, updatedAt = now))
+                }
             }
         SyncReadAloudVoicesResult(inserted, updated, unavailable)
     }

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

@@ -1749,6 +1749,7 @@ class ReadBookViewModel(
                 }
             },
             managedSources = setOf(ReadAloudVoice.MANAGED_BY_CONFIGURED_TTS),
+            removeMissingEngineTypes = setOf(ReadAloudVoice.ENGINE_HTTP),
         )
     }
 

+ 11 - 1
app/src/main/java/io/legado/app/ui/book/readaloud/cloudtts/CloudTtsViewModel.kt

@@ -65,7 +65,17 @@ class CloudTtsViewModel(
                         ReadAloudVoice.ENGINE_HTTP,
                     )
                 })
-            }.collect { (allEngines, allHttpEngines, savedVoices) ->
+            }.collect { (allEngines, allHttpEngines, allSavedVoices) ->
+                val configuredHttpEngineIds = allHttpEngines.mapTo(mutableSetOf()) { it.sourceId }
+                val orphanedVoiceIds = allSavedVoices.asSequence()
+                    .filter { it.engineType == ReadAloudVoice.ENGINE_HTTP }
+                    .filter { it.managedBy == ReadAloudVoice.MANAGED_BY_CONFIGURED_TTS }
+                    .filter { it.engineId !in configuredHttpEngineIds }
+                    .mapTo(mutableSetOf()) { it.id }
+                allSavedVoices
+                    .filter { it.id in orphanedVoiceIds }
+                    .forEach { voiceGateway.deleteVoice(it) }
+                val savedVoices = allSavedVoices.filterNot { it.id in orphanedVoiceIds }
                 engines = allEngines
                 httpEngines = allHttpEngines
                 voices = savedVoices

+ 11 - 3
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt

@@ -20,6 +20,7 @@ import io.legado.app.utils.inputStream
 import io.legado.app.utils.openInputStream
 import java.io.File
 import java.io.FileOutputStream
+import java.util.UUID
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -374,17 +375,24 @@ class ThemeConfigViewModel(
                     .takeIf { it.matches(Regex("[a-z0-9]{1,8}")) }
                     ?: "ttf"
                 val fontDir = File(appCtx.filesDir, "fonts").apply { mkdirs() }
-                val target = File(fontDir, "app_font.$extension")
-                val temp = File(fontDir, "app_font.$extension.tmp")
+                val target = File(fontDir, "app_font_${UUID.randomUUID()}.$extension")
+                val temp = File(fontDir, "${target.name}.tmp")
                 fileDoc.openInputStream().getOrThrow().use { input ->
                     FileOutputStream(temp).use(input::copyTo)
                 }
-                if (target.exists() && !target.delete()) error("Unable to replace app font")
                 if (!temp.renameTo(target)) {
                     temp.copyTo(target, overwrite = true)
                     temp.delete()
                 }
+                val oldPath = themeSettingsGateway.currentSettings.appFontPath
                 themeSettingsGateway.update { it.copy(appFontPath = target.absolutePath) }
+                oldPath?.let(::File)
+                    ?.takeIf {
+                        it != target &&
+                            it.parentFile == fontDir &&
+                            it.name.startsWith("app_font")
+                    }
+                    ?.delete()
             }.onFailure(Throwable::printStackTrace)
         }
     }

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

@@ -204,6 +204,7 @@ fun RssReadRouteScreen(
     LaunchedEffect(isNight, webView) {
         val currentWebView = webView ?: return@LaunchedEffect
         currentWebView.settings.setDarkeningAllowed(isNight)
+        currentWebView.postInvalidate()
     }
 
     Box(modifier = Modifier.fillMaxSize()) {

+ 14 - 17
app/src/main/java/io/legado/app/utils/WebSettingsExtensions.kt

@@ -5,7 +5,6 @@ import android.os.Build
 import android.webkit.WebSettings
 import androidx.webkit.WebSettingsCompat
 import androidx.webkit.WebViewFeature
-import io.legado.app.help.config.AppConfig
 
 /**
  * 设置是否夜间模式
@@ -20,20 +19,18 @@ fun WebSettings.setDarkeningAllowed(allow: Boolean) {
             it.printOnDebug()
         }
     }
-    if (AppConfig.isNightTheme) {
-        if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) {
-            @Suppress("DEPRECATION")
-            WebSettingsCompat.setForceDarkStrategy(
-                this,
-                WebSettingsCompat.DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING
-            )
-        }
-        if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
-            @Suppress("DEPRECATION")
-            WebSettingsCompat.setForceDark(
-                this,
-                WebSettingsCompat.FORCE_DARK_ON
-            )
-        }
+    if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) {
+        @Suppress("DEPRECATION")
+        WebSettingsCompat.setForceDarkStrategy(
+            this,
+            WebSettingsCompat.DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING
+        )
+    }
+    if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
+        @Suppress("DEPRECATION")
+        WebSettingsCompat.setForceDark(
+            this,
+            if (allow) WebSettingsCompat.FORCE_DARK_ON else WebSettingsCompat.FORCE_DARK_OFF
+        )
     }
-}
+}

+ 9 - 3
app/src/test/java/io/legado/app/data/repository/ReadSettingsMappingTest.kt

@@ -13,16 +13,16 @@ import org.junit.Test
 class ReadSettingsMappingTest {
 
     @Test
-    fun `gateway 持久化边界固定为显式声明的 45 键`() {
+    fun `gateway 持久化边界固定为显式声明的 48 键`() {
         val actualKeys = ReadSettings().toGatewayPrefMap().keys
         val expectedKeys = ReadSettings().expectedGatewayPrefMap().keys
 
-        assertEquals(45, actualKeys.size)
+        assertEquals(48, actualKeys.size)
         assertEquals(expectedKeys, actualKeys)
     }
 
     @Test
-    fun `阅读设置 gateway 45 键写读映射逐字段对应`() {
+    fun `阅读设置 gateway 48 键写读映射逐字段对应`() {
         val repository = ReadSettingsRepository(
             settingsRepository = SettingsRepository(),
             preferencesFlow = MutableStateFlow(mutablePreferencesOf()),
@@ -118,6 +118,9 @@ private fun readSettingsMappingSamples(): List<ReadSettings> {
         base.copy(textBottomJustify = false),
         base.copy(adaptSpecialStyle = false),
         base.copy(useZhLayout = true),
+        base.copy(eyeProtectionEnabled = true),
+        base.copy(eyeProtectionIntensity = 87),
+        base.copy(eyeProtectionAutoNight = true),
         base.copy(brightnessAuto = true),
         base.copy(useUnderline = true),
         base.copy(mouseWheelPage = false),
@@ -153,6 +156,9 @@ private fun ReadSettings.expectedGatewayPrefMap(): Map<String, Any?> = mapOf(
     PreferKey.textBottomJustify to textBottomJustify,
     PreferKey.adaptSpecialStyle to adaptSpecialStyle,
     PreferKey.useZhLayout to useZhLayout,
+    PreferKey.eyeProtectionEnabled to eyeProtectionEnabled,
+    PreferKey.colorTemperature to eyeProtectionIntensity.coerceIn(0, 100),
+    PreferKey.eyeProtectionAutoNight to eyeProtectionAutoNight,
     PreferKey.showBrightnessView to showBrightnessView,
     PreferKey.brightnessVwPos to brightnessVwPos,
     PreferKey.brightness to readBrightness,

+ 25 - 0
app/src/test/java/io/legado/app/domain/usecase/SyncReadAloudVoicesUseCaseTest.kt

@@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.runBlocking
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
 import org.junit.Test
 
 class SyncReadAloudVoicesUseCaseTest {
@@ -65,6 +66,30 @@ class SyncReadAloudVoicesUseCaseTest {
         assertFalse(gateway.voices.single().available)
         assertFalse(gateway.voices.single().enabled)
     }
+
+    @Test
+    fun `missing configured HTTP voice is removed with deleted TTS`() = runBlocking {
+        val old = ReadAloudVoice(
+            id = SpeechIdentity.voiceId("http", "7", ""),
+            engineType = ReadAloudVoice.ENGINE_HTTP,
+            engineId = "7",
+            speakerId = "",
+            displayName = "deleted TTS",
+            managedBy = ReadAloudVoice.MANAGED_BY_CONFIGURED_TTS,
+            available = false,
+        )
+        val gateway = MutableVoiceGateway(mutableListOf(old))
+
+        val result = SyncReadAloudVoicesUseCase(gateway)(
+            entries = emptyList(),
+            managedSources = setOf(ReadAloudVoice.MANAGED_BY_CONFIGURED_TTS),
+            removeMissingEngineTypes = setOf(ReadAloudVoice.ENGINE_HTTP),
+            now = 200,
+        )
+
+        assertEquals(1, result.unavailable)
+        assertTrue(gateway.voices.isEmpty())
+    }
 }
 
 private class MutableVoiceGateway(