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

护眼模式全局生效并支持定时时段

设置-外观里的护眼色温此前只被阅读器正文视图消费,其他界面无任何图层
渲染该值,故设置页调节看不到效果。将同一滤镜提升到 BaseComposeActivity
根部,使护眼对所有 Compose 界面全局生效,并移除阅读器中重复的滤镜避免叠加。

补齐定时逻辑:EyeProtection.isActiveAt 按时段(支持跨零点)判定是否生效,
rememberEyeProtectionActive 在启用定时时按分钟自动重算。
5151561 3 месяцев назад
Родитель
Сommit
c4f19d5cea

+ 26 - 1
app/src/main/java/io/legado/app/base/BaseComposeActivity.kt

@@ -6,9 +6,12 @@ import android.os.Bundle
 import androidx.activity.compose.setContent
 import androidx.activity.enableEdgeToEdge
 import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.toArgb
 import androidx.core.graphics.drawable.toDrawable
 import androidx.core.os.LocaleListCompat
@@ -22,9 +25,12 @@ import io.legado.app.constant.EventBus
 import io.legado.app.constant.Theme
 import io.legado.app.domain.gateway.AppLocaleGateway
 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.diffFrom
 import io.legado.app.help.config.ThemeConfigStore
+import io.legado.app.ui.book.read.eyeProtectionColorFilter
+import io.legado.app.ui.book.read.rememberEyeProtectionActive
 import io.legado.app.ui.theme.AppTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.ThemeResolver
@@ -48,6 +54,7 @@ abstract class BaseComposeActivity(
 
     private val appLocaleGateway by inject<AppLocaleGateway>()
     private val appUiConfigurationGateway by inject<AppUiConfigurationGateway>()
+    private val themeSettingsGateway by inject<ThemeSettingsGateway>()
     private var lastUiConfiguration: AppUiConfiguration? = null
 
     @Composable
@@ -67,12 +74,30 @@ abstract class BaseComposeActivity(
         setupSystemBar()
         // Compose 入口
         val initialUiConfiguration = appUiConfigurationGateway.currentConfiguration
+        val initialThemeSettings = themeSettingsGateway.currentSettings
         setContent {
             val uiConfiguration by appUiConfigurationGateway.configuration
                 .collectAsStateWithLifecycle(initialUiConfiguration)
             AppTheme(configuration = uiConfiguration) {
                 SyncWindowBackground()
-                Content()
+                val themeSettings by themeSettingsGateway.settings
+                    .collectAsStateWithLifecycle(initialThemeSettings)
+                val eyeProtectionActive = rememberEyeProtectionActive(
+                    enabled = themeSettings.eyeProtectionEnabled,
+                    schedule = themeSettings.eyeProtectionSchedule,
+                    startTime = themeSettings.eyeProtectionStartTime,
+                    endTime = themeSettings.eyeProtectionEndTime,
+                )
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .eyeProtectionColorFilter(
+                            enabled = eyeProtectionActive,
+                            intensity = themeSettings.colorTemperature,
+                        )
+                ) {
+                    Content()
+                }
             }
         }
 

+ 57 - 0
app/src/main/java/io/legado/app/ui/book/read/EyeProtection.kt

@@ -1,11 +1,17 @@
 package io.legado.app.ui.book.read
 
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.ColorFilter
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.graphics.ColorMatrix as ComposeColorMatrix
+import kotlinx.coroutines.delay
+import java.time.LocalTime
 
 
 object EyeProtection {
@@ -46,6 +52,35 @@ object EyeProtection {
     fun syncEnabledForNight(isNight: Boolean, autoNight: Boolean): Boolean? =
         if (autoNight) isNight else null
 
+    /**
+     * 计算护眼在给定时刻是否应生效。
+     * - 总开关关闭 → 永远不生效;
+     * - 未启用定时 → 只要总开关开就生效;
+     * - 启用定时但时间无法解析 → 退化为仅按总开关生效;
+     * - 启用定时 → 仅在 [startTime, endTime) 时段内生效(支持跨零点,如 22:00–07:00)。
+     */
+    fun isActiveAt(
+        enabled: Boolean,
+        schedule: Boolean,
+        startTime: String,
+        endTime: String,
+        now: LocalTime,
+    ): Boolean {
+        if (!enabled) return false
+        if (!schedule) return true
+        val start = parseTime(startTime) ?: return true
+        val end = parseTime(endTime) ?: return true
+        if (start == end) return true
+        return if (start < end) {
+            now >= start && now < end
+        } else {
+            now >= start || now < end
+        }
+    }
+
+    private fun parseTime(value: String): LocalTime? =
+        runCatching { LocalTime.parse(value.trim()) }.getOrNull()
+
     private val IDENTITY_MATRIX = floatArrayOf(
         1f, 0f, 0f, 0f, 0f,
         0f, 1f, 0f, 0f, 0f,
@@ -74,3 +109,25 @@ fun Modifier.eyeProtectionColorFilter(
         colorFilter = cachedColorFilter
     }
 }
+
+/**
+ * 解析护眼当前是否生效,启用定时时按分钟自动重算,使时段边界能实时切换。
+ */
+@Composable
+fun rememberEyeProtectionActive(
+    enabled: Boolean,
+    schedule: Boolean,
+    startTime: String,
+    endTime: String,
+): Boolean {
+    if (!enabled) return false
+    if (!schedule) return true
+    var now by remember { mutableStateOf(LocalTime.now()) }
+    LaunchedEffect(startTime, endTime) {
+        while (true) {
+            now = LocalTime.now()
+            delay(60_000L)
+        }
+    }
+    return EyeProtection.isActiveAt(enabled, schedule, startTime, endTime, now)
+}

+ 1 - 6
app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt

@@ -523,12 +523,7 @@ fun ReadBookRouteScreen(
     var showSelectMenuConfigSheet by rememberSaveable { mutableStateOf(false) }
 
     Box(
-        Modifier
-            .fillMaxSize()
-            .eyeProtectionColorFilter(
-                enabled = readPreferences.eyeProtectionEnabled,
-                intensity = readPreferences.eyeProtectionIntensity,
-            )
+        Modifier.fillMaxSize()
     ) {
         key(controller) {
             ReadBookViewLayer(

+ 53 - 0
app/src/test/java/io/legado/app/ui/book/read/EyeProtectionTest.kt

@@ -2,8 +2,11 @@ package io.legado.app.ui.book.read
 
 import org.junit.Assert.assertArrayEquals
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
 import org.junit.Test
+import java.time.LocalTime
 
 class EyeProtectionTest {
 
@@ -41,4 +44,54 @@ class EyeProtectionTest {
         assertEquals(false, EyeProtection.syncEnabledForNight(isNight = false, autoNight = true))
         assertNull(EyeProtection.syncEnabledForNight(isNight = true, autoNight = false))
     }
+
+    private fun activeAt(
+        hour: Int,
+        minute: Int = 0,
+        enabled: Boolean = true,
+        schedule: Boolean = true,
+        start: String = "22:00",
+        end: String = "07:00",
+    ) = EyeProtection.isActiveAt(enabled, schedule, start, end, LocalTime.of(hour, minute))
+
+    @Test
+    fun `总开关关闭时永不生效`() {
+        assertFalse(activeAt(23, enabled = false))
+        assertFalse(activeAt(23, enabled = false, schedule = false))
+    }
+
+    @Test
+    fun `未启用定时时总开关开即全天生效`() {
+        assertTrue(activeAt(3, schedule = false))
+        assertTrue(activeAt(15, schedule = false))
+    }
+
+    @Test
+    fun `跨零点时段仅在窗口内生效`() {
+        assertTrue(activeAt(22))       // 起点
+        assertTrue(activeAt(23, 30))   // 午夜前
+        assertTrue(activeAt(0, 30))    // 午夜后
+        assertTrue(activeAt(6, 59))    // 终点前
+        assertFalse(activeAt(7))       // 终点(不含)
+        assertFalse(activeAt(12))      // 白天
+        assertFalse(activeAt(21, 59))  // 起点前
+    }
+
+    @Test
+    fun `同日时段仅在窗口内生效`() {
+        assertTrue(activeAt(9, start = "08:00", end = "18:00"))
+        assertFalse(activeAt(7, 59, start = "08:00", end = "18:00"))
+        assertFalse(activeAt(18, start = "08:00", end = "18:00"))
+    }
+
+    @Test
+    fun `时间无法解析时退化为按总开关生效`() {
+        assertTrue(activeAt(12, start = "bad", end = "07:00"))
+        assertTrue(activeAt(12, start = "22:00", end = ""))
+    }
+
+    @Test
+    fun `起止时间相同视为全天生效`() {
+        assertTrue(activeAt(3, start = "08:00", end = "08:00"))
+    }
 }