feat: 实验室设置+护眼模式+Web清理

合并 PR #1278 by fjsuta
This commit is contained in:
WangXuanMeng
2026-06-02 18:32:28 +08:00
committed by GitHub
parent 5da2e85395
commit b25244fcde
18 changed files with 611 additions and 21 deletions
@@ -338,4 +338,16 @@ object PreferKey {
const val homepageModuleHidden = "homepageModuleHidden" const val homepageModuleHidden = "homepageModuleHidden"
const val homepageLayoutMode = "homepageLayoutMode" const val homepageLayoutMode = "homepageLayoutMode"
const val navIconHome = "navIconHome" const val navIconHome = "navIconHome"
// Lab
const val labEnabled = "labEnabled"
const val labEInkDisplay = "labEInkDisplay"
const val labEyeProtection = "labEyeProtection"
// Eye Protection
const val eyeProtectionEnabled = "eyeProtectionEnabled"
const val colorTemperature = "colorTemperature"
const val eyeProtectionSchedule = "eyeProtectionSchedule"
const val eyeProtectionStartTime = "eyeProtectionStartTime"
const val eyeProtectionEndTime = "eyeProtectionEndTime"
} }
@@ -70,7 +70,9 @@ import io.legado.app.ui.book.manga.recyclerview.MangaAdapter
import io.legado.app.ui.book.manga.recyclerview.MangaLayoutManager import io.legado.app.ui.book.manga.recyclerview.MangaLayoutManager
import io.legado.app.ui.book.manga.recyclerview.ScrollTimer import io.legado.app.ui.book.manga.recyclerview.ScrollTimer
import io.legado.app.ui.book.manga.recyclerview.WebtoonFrame import io.legado.app.ui.book.manga.recyclerview.WebtoonFrame
import io.legado.app.ui.book.read.EyeProtectionRefreshScheduler
import io.legado.app.ui.book.read.MangaMenu import io.legado.app.ui.book.read.MangaMenu
import io.legado.app.ui.book.read.observeEyeProtectionEvents
import io.legado.app.ui.book.read.ReadBookActivity.Companion.RESULT_DELETED import io.legado.app.ui.book.read.ReadBookActivity.Companion.RESULT_DELETED
import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.book.toc.TocActivityResult import io.legado.app.ui.book.toc.TocActivityResult
@@ -81,6 +83,7 @@ import io.legado.app.ui.widget.recycler.LoadMoreView
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.StartActivityContract import io.legado.app.utils.StartActivityContract
import io.legado.app.utils.buildMainHandler
import io.legado.app.utils.canScroll import io.legado.app.utils.canScroll
import io.legado.app.utils.fastBinarySearch import io.legado.app.utils.fastBinarySearch
import io.legado.app.utils.findCenterViewPosition import io.legado.app.utils.findCenterViewPosition
@@ -138,6 +141,10 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
private var justInitData: Boolean = false private var justInitData: Boolean = false
private var syncDialog: AlertDialog? = null private var syncDialog: AlertDialog? = null
private val handler by lazy { buildMainHandler() }
private val eyeProtectionScheduler by lazy {
EyeProtectionRefreshScheduler(handler) { binding.eyeProtectionOverlay.refresh() }
}
private val mScrollTimer by lazy { private val mScrollTimer by lazy {
ScrollTimer(this, binding.recyclerView, lifecycleScope).apply { ScrollTimer(this, binding.recyclerView, lifecycleScope).apply {
setSpeed(AppConfig.mangaAutoPageSpeed) setSpeed(AppConfig.mangaAutoPageSpeed)
@@ -213,6 +220,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
ReadManga.loadOrUpContent() ReadManga.loadOrUpContent()
} }
binding.flLoading.isVisible = !AppConfig.isEInkMode binding.flLoading.isVisible = !AppConfig.isEInkMode
binding.eyeProtectionOverlay.refresh()
mAdapter.addFooterView { mAdapter.addFooterView {
ViewLoadMoreBinding.bind(loadMoreView) ViewLoadMoreBinding.bind(loadMoreView)
} }
@@ -242,6 +250,10 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
val item = mAdapter.getItem(binding.recyclerView.findCenterViewPosition()) val item = mAdapter.getItem(binding.recyclerView.findCenterViewPosition())
upInfoBar(item) upInfoBar(item)
} }
observeEyeProtectionEvents(
onRefresh = { binding.eyeProtectionOverlay.refresh() },
scheduler = eyeProtectionScheduler
)
} }
private fun initRecyclerView() { private fun initRecyclerView() {
@@ -457,10 +469,13 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
} }
ReadManga.initReadTime() ReadManga.initReadTime()
ReadManga.startAutoSaveSession() ReadManga.startAutoSaveSession()
binding.eyeProtectionOverlay.refresh()
eyeProtectionScheduler.schedule()
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
eyeProtectionScheduler.cancel()
if (ReadManga.inBookshelf) { if (ReadManga.inBookshelf) {
ReadManga.saveRead() ReadManga.saveRead()
if (!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
@@ -122,13 +122,16 @@ class MangaAdapter(private val context: Context) :
) { ) {
"ARGB values must be between 0-255" "ARGB values must be between 0-255"
} }
val matrix = floatArrayOf( val baseMatrix = floatArrayOf(
(255 - mConfig.r) / 255f, 0f, 0f, 0f, 0f, (255 - mConfig.r) / 255f, 0f, 0f, 0f, 0f,
0f, (255 - mConfig.g) / 255f, 0f, 0f, 0f, 0f, (255 - mConfig.g) / 255f, 0f, 0f, 0f,
0f, 0f, (255 - mConfig.b) / 255f, 0f, 0f, 0f, 0f, (255 - mConfig.b) / 255f, 0f, 0f,
0f, 0f, 0f, (255 - mConfig.a) / 255f, 0f 0f, 0f, 0f, (255 - mConfig.a) / 255f, 0f
) )
binding.image.colorFilter = ColorMatrixColorFilter(ColorMatrix(matrix)) val epMatrix = io.legado.app.ui.book.read.EyeProtectionHelper.buildColorMatrix()
val combined = android.graphics.ColorMatrix(baseMatrix)
combined.postConcat(epMatrix)
binding.image.colorFilter = ColorMatrixColorFilter(combined)
} }
fun setBackground() { fun setBackground() {
@@ -0,0 +1,167 @@
package io.legado.app.ui.book.read
import android.graphics.Color
import android.graphics.ColorMatrix
import android.graphics.Paint
import android.os.Handler
import androidx.appcompat.app.AppCompatActivity
import io.legado.app.constant.PreferKey
import io.legado.app.ui.config.labConfig.LabConfig
import io.legado.app.ui.config.themeConfig.ThemeConfig
import io.legado.app.utils.observeEvent
import java.util.Calendar
/**
* 护眼色温工具类
* 提供色温(暖色)转 ColorMatrix 以及覆盖层颜色计算
*
* 激活条件:
* Lab 总开关开启 && 实验室护眼模式开启 && 主题护眼模式开启 &&
* (未开启定时 OR 当前时间在定时区间内)
*/
object EyeProtectionHelper {
/**
* 判断当前时间是否在护眼定时区间内
*/
fun isInSchedule(): Boolean {
if (!ThemeConfig.eyeProtectionSchedule) return true
val now = Calendar.getInstance()
val curMinute = now.get(Calendar.HOUR_OF_DAY) * 60 + now.get(Calendar.MINUTE)
val start = parseTime(ThemeConfig.eyeProtectionStartTime)
val end = parseTime(ThemeConfig.eyeProtectionEndTime)
if (start == null || end == null) return true
return if (start == end) {
true
} else if (start < end) {
curMinute in start..end
} else {
curMinute >= start || curMinute <= end
}
}
private fun parseTime(value: String?): Int? {
if (value.isNullOrBlank()) return null
return try {
val parts = value.split(":")
if (parts.size < 2) return null
val h = parts[0].trim().toInt().coerceIn(0, 23)
val m = parts[1].trim().toInt().coerceIn(0, 59)
h * 60 + m
} catch (e: Exception) {
null
}
}
/**
* 是否应该激活护眼色温
* 仅当总开关打开,并且满足定时条件时返回 true
*/
fun isActive(): Boolean {
if (!LabConfig.labEnabled) return false
if (!LabConfig.eyeProtection) return false
if (!ThemeConfig.eyeProtectionEnabled) return false
return isInSchedule()
}
/**
* 根据当前色温设置获取暖色覆盖颜色
* temperature: 0-1000 不变,100 最暖
*/
fun overlayColor(): Int {
if (!isActive()) return Color.TRANSPARENT
val t = ThemeConfig.colorTemperature.coerceIn(0, 100)
if (t == 0) return Color.TRANSPARENT
val alpha = (t / 100f * 90f).toInt().coerceIn(0, 255)
return Color.argb(alpha, 255, 180, 90)
}
/**
* 创建色温 ColorMatrix(暖色调,降低蓝光通道)
*/
fun buildColorMatrix(): ColorMatrix {
val t = if (isActive()) ThemeConfig.colorTemperature.coerceIn(0, 100) else 0
val ratio = t / 100f
val r = 1f
val g = 1f - 0.45f * ratio
val b = 1f - 0.85f * ratio
val brightness = 1f - 0.05f * ratio
val matrix = ColorMatrix(
floatArrayOf(
r * brightness, 0f, 0f, 0f, 8f * ratio,
0f, g * brightness, 0f, 0f, 4f * ratio,
0f, 0f, b * brightness, 0f, 0f,
0f, 0f, 0f, 1f, 0f
)
)
return matrix
}
/**
* 为 Paint 应用护眼色温 ColorFilter
*/
fun applyColorFilter(paint: Paint) {
if (isActive()) {
paint.colorFilter = android.graphics.ColorMatrixColorFilter(buildColorMatrix())
} else {
paint.colorFilter = null
}
}
}
/**
* 护眼定时刷新调度器
* 在定时区间边界(每分钟)自动刷新护眼状态
*/
class EyeProtectionRefreshScheduler(
private val handler: Handler,
private val onRefresh: () -> Unit
) {
private val refreshRunnable = Runnable {
onRefresh()
schedule()
}
fun schedule() {
cancel()
if (!ThemeConfig.eyeProtectionSchedule) return
val now = Calendar.getInstance()
val delayMs = (60 - now.get(Calendar.SECOND)) * 1000L -
now.get(Calendar.MILLISECOND)
val safeDelay = delayMs.coerceAtLeast(1000L)
handler.postDelayed(refreshRunnable, safeDelay)
}
fun cancel() {
handler.removeCallbacks(refreshRunnable)
}
}
/**
* 在 Activity 中注册护眼模式相关的 EventBus 监听
* 设置变化时自动刷新覆盖层,定时/时间变化时重新调度
*/
fun AppCompatActivity.observeEyeProtectionEvents(
onRefresh: () -> Unit,
scheduler: EyeProtectionRefreshScheduler
) {
observeEvent<Boolean>(PreferKey.eyeProtectionEnabled) {
onRefresh()
scheduler.schedule()
}
observeEvent<Int>(PreferKey.colorTemperature) {
onRefresh()
}
observeEvent<Boolean>(PreferKey.eyeProtectionSchedule) {
onRefresh()
scheduler.schedule()
}
observeEvent<String>(PreferKey.eyeProtectionStartTime) {
onRefresh()
scheduler.schedule()
}
observeEvent<String>(PreferKey.eyeProtectionEndTime) {
onRefresh()
scheduler.schedule()
}
}
@@ -0,0 +1,53 @@
package io.legado.app.ui.book.read
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
/**
* 护眼色温覆盖层
* 在父 View 之上叠加一层半透明暖色,模拟护眼色温
*/
class EyeProtectionOverlay @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint().apply {
style = Paint.Style.FILL
}
init {
isClickable = false
isFocusable = false
setWillNotDraw(false)
}
/**
* 刷新当前护眼状态
*/
fun refresh() {
val color = EyeProtectionHelper.overlayColor()
if (color == Color.TRANSPARENT) {
if (visibility != GONE) {
visibility = GONE
}
} else {
if (visibility != VISIBLE) {
visibility = VISIBLE
}
paint.color = color
invalidate()
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (paint.color == Color.TRANSPARENT) return
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
}
}
@@ -278,6 +278,9 @@ class ReadBookActivity : BaseReadBookActivity(),
private var pageChanged = false private var pageChanged = false
private val handler by lazy { buildMainHandler() } private val handler by lazy { buildMainHandler() }
private val screenOffRunnable by lazy { Runnable { keepScreenOn(false) } } private val screenOffRunnable by lazy { Runnable { keepScreenOn(false) } }
private val eyeProtectionScheduler by lazy {
EyeProtectionRefreshScheduler(handler) { binding.eyeProtectionOverlay.refresh() }
}
private val executor = ReadBook.executor private val executor = ReadBook.executor
private val upSeekBarThrottle = throttle(200) { private val upSeekBarThrottle = throttle(200) {
runOnUiThread { runOnUiThread {
@@ -302,6 +305,7 @@ class ReadBookActivity : BaseReadBookActivity(),
ReadBook.register(this) ReadBook.register(this)
binding.cursorLeft.setOnTouchListener(this) binding.cursorLeft.setOnTouchListener(this)
binding.cursorRight.setOnTouchListener(this) binding.cursorRight.setOnTouchListener(this)
binding.eyeProtectionOverlay.refresh()
onBackPressedDispatcher.addCallback(this) { onBackPressedDispatcher.addCallback(this) {
if (isShowingSearchResult) { if (isShowingSearchResult) {
@@ -400,6 +404,8 @@ class ReadBookActivity : BaseReadBookActivity(),
registerReceiver(timeBatteryReceiver, timeBatteryReceiver.filter) registerReceiver(timeBatteryReceiver, timeBatteryReceiver.filter)
binding.readView.upTime() binding.readView.upTime()
screenOffTimerStart() screenOffTimerStart()
binding.eyeProtectionOverlay.refresh()
eyeProtectionScheduler.schedule()
// 网络监听,当从无网切换到网络环境时同步进度(注意注册的同时就会收到监听,因此界面激活时无需重复执行同步操作) // 网络监听,当从无网切换到网络环境时同步进度(注意注册的同时就会收到监听,因此界面激活时无需重复执行同步操作)
networkChangedListener.register() networkChangedListener.register()
networkChangedListener.onNetworkChanged = { networkChangedListener.onNetworkChanged = {
@@ -413,6 +419,7 @@ class ReadBookActivity : BaseReadBookActivity(),
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
autoPageStop() autoPageStop()
eyeProtectionScheduler.cancel()
backupJob?.cancel() backupJob?.cancel()
ReadBook.saveRead() ReadBook.saveRead()
ReadBook.stopAutoSaveSession() ReadBook.stopAutoSaveSession()
@@ -1985,6 +1992,10 @@ class ReadBookActivity : BaseReadBookActivity(),
viewModel.refreshContentDur(it) viewModel.refreshContentDur(it)
} }
} }
observeEyeProtectionEvents(
onRefresh = { binding.eyeProtectionOverlay.refresh() },
scheduler = eyeProtectionScheduler
)
} }
private fun upScreenTimeOut() { private fun upScreenTimeOut() {
@@ -27,7 +27,8 @@ fun ConfigNavScreen(
onNavigateToTheme: () -> Unit, onNavigateToTheme: () -> Unit,
onNavigateToBackup: () -> Unit, onNavigateToBackup: () -> Unit,
onNavigateToDownloadCache: () -> Unit, onNavigateToDownloadCache: () -> Unit,
onNavigateToTranslation: () -> Unit onNavigateToTranslation: () -> Unit,
onNavigateToLab: () -> Unit
) { ) {
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior() val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
@@ -80,6 +81,10 @@ fun ConfigNavScreen(
title = stringResource(R.string.translation_config), title = stringResource(R.string.translation_config),
onClick = onNavigateToTranslation onClick = onNavigateToTranslation
) )
ClickableSettingItem(
title = stringResource(R.string.lab_setting),
onClick = onNavigateToLab
)
} }
} }
} }
@@ -0,0 +1,23 @@
package io.legado.app.ui.config.labConfig
import io.legado.app.constant.PreferKey
import io.legado.app.ui.config.prefDelegate
object LabConfig {
var labEnabled by prefDelegate(
PreferKey.labEnabled,
false
)
var eInkDisplay by prefDelegate(
PreferKey.labEInkDisplay,
false
)
var eyeProtection by prefDelegate(
PreferKey.labEyeProtection,
false
)
}
@@ -0,0 +1,108 @@
package io.legado.app.ui.config.labConfig
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LabConfigScreen(
onBackClick: () -> Unit
) {
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
GlassMediumFlexibleTopAppBar(
title = stringResource(R.string.lab_setting),
scrollBehavior = scrollBehavior,
navigationIcon = {
TopBarNavigationButton(onClick = onBackClick)
}
)
}
) { paddingValues ->
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = adaptiveContentPadding(
top = paddingValues.calculateTopPadding(),
bottom = 120.dp
)
) {
item {
SplicedColumnGroup {
SwitchSettingItem(
title = stringResource(R.string.lab_enabled_title),
description = stringResource(R.string.lab_enabled_summary),
checked = LabConfig.labEnabled,
onCheckedChange = { LabConfig.labEnabled = it }
)
}
AnimatedVisibility(visible = LabConfig.labEnabled) {
SplicedColumnGroup(title = stringResource(R.string.lab_display)) {
SwitchSettingItem(
title = stringResource(R.string.lab_eink_display_title),
description = stringResource(R.string.lab_eink_display_summary),
checked = LabConfig.eInkDisplay,
onCheckedChange = {
LabConfig.eInkDisplay = it
}
)
if (LabConfig.eInkDisplay) {
Text(
text = stringResource(R.string.lab_eink_display_hint),
style = LegadoTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp)
)
}
SwitchSettingItem(
title = stringResource(R.string.lab_eye_protection_title),
description = stringResource(R.string.lab_eye_protection_summary),
checked = LabConfig.eyeProtection,
onCheckedChange = {
LabConfig.eyeProtection = it
}
)
if (LabConfig.eyeProtection) {
Text(
text = stringResource(R.string.lab_eye_protection_hint),
style = LegadoTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp)
)
}
}
}
}
}
}
}
@@ -182,6 +182,23 @@ object ThemeConfig {
var navIconMy by prefDelegate(PreferKey.navIconMy, "") var navIconMy by prefDelegate(PreferKey.navIconMy, "")
// Eye Protection
var eyeProtectionEnabled by prefDelegate(PreferKey.eyeProtectionEnabled, false) {
postEvent(PreferKey.eyeProtectionEnabled, it)
}
var colorTemperature by prefDelegate(PreferKey.colorTemperature, 50) {
postEvent(PreferKey.colorTemperature, it)
}
var eyeProtectionSchedule by prefDelegate(PreferKey.eyeProtectionSchedule, false) {
postEvent(PreferKey.eyeProtectionSchedule, it)
}
var eyeProtectionStartTime by prefDelegate(PreferKey.eyeProtectionStartTime, "22:00") {
postEvent(PreferKey.eyeProtectionStartTime, it)
}
var eyeProtectionEndTime by prefDelegate(PreferKey.eyeProtectionEndTime, "07:00") {
postEvent(PreferKey.eyeProtectionEndTime, it)
}
fun hasImageBg(isDark: Boolean): Boolean = fun hasImageBg(isDark: Boolean): Boolean =
!(if (isDark) bgImageDark else bgImageLight).isNullOrBlank() !(if (isDark) bgImageDark else bgImageLight).isNullOrBlank()
@@ -55,6 +55,7 @@ import androidx.compose.material3.ToggleButtonDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -84,6 +85,7 @@ import io.legado.app.help.LauncherIconHelp
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.OldThemeConfig import io.legado.app.help.config.OldThemeConfig
import io.legado.app.help.loadFontFiles import io.legado.app.help.loadFontFiles
import io.legado.app.ui.config.labConfig.LabConfig
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeEngine import io.legado.app.ui.theme.ThemeEngine
import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.ThemeResolver
@@ -296,9 +298,12 @@ fun ThemeConfigScreen(
if (!isMiuixEngine) { if (!isMiuixEngine) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
val visibleThemes = themes.filter { (_, value) ->
value != "4" || (LabConfig.labEnabled && LabConfig.eInkDisplay)
}
ThemeColorSelector( ThemeColorSelector(
context = context, context = context,
themes = themes, themes = visibleThemes,
selectedTheme = selectedTheme, selectedTheme = selectedTheme,
isDark = isDarkTheme, isDark = isDarkTheme,
isAmoled = ThemeConfig.isPureBlack, isAmoled = ThemeConfig.isPureBlack,
@@ -464,6 +469,119 @@ fun ThemeConfigScreen(
) )
} }
SplicedColumnGroup(title = stringResource(R.string.eye_protection)) {
var eyeProtectionEnabled by remember {
mutableStateOf(ThemeConfig.eyeProtectionEnabled)
}
var colorTemperature by remember {
mutableIntStateOf(ThemeConfig.colorTemperature)
}
var eyeProtectionSchedule by remember {
mutableStateOf(ThemeConfig.eyeProtectionSchedule)
}
var eyeProtectionStartTime by remember {
mutableStateOf(ThemeConfig.eyeProtectionStartTime)
}
var eyeProtectionEndTime by remember {
mutableStateOf(ThemeConfig.eyeProtectionEndTime)
}
SwitchSettingItem(
title = stringResource(R.string.eye_protection_enabled),
description = stringResource(R.string.eye_protection_enabled_summary),
checked = eyeProtectionEnabled,
onCheckedChange = {
eyeProtectionEnabled = it
ThemeConfig.eyeProtectionEnabled = it
}
)
AnimatedVisibility(visible = eyeProtectionEnabled) {
Column {
SliderSettingItem(
title = stringResource(R.string.color_temperature),
description = stringResource(
R.string.color_temperature_summary,
colorTemperature
),
value = colorTemperature.toFloat(),
defaultValue = 50f,
valueRange = 0f..100f,
steps = 99,
onValueChange = {
colorTemperature = it.toInt()
ThemeConfig.colorTemperature = it.toInt()
}
)
SwitchSettingItem(
title = stringResource(R.string.eye_protection_schedule),
description = stringResource(R.string.eye_protection_schedule_summary),
checked = eyeProtectionSchedule,
onCheckedChange = {
eyeProtectionSchedule = it
ThemeConfig.eyeProtectionSchedule = it
}
)
AnimatedVisibility(visible = eyeProtectionSchedule) {
Column {
ClickableSettingItem(
title = stringResource(R.string.eye_protection_start_time),
option = eyeProtectionStartTime,
onClick = {
val parts = eyeProtectionStartTime.split(":")
val hour = parts.getOrNull(0)?.toIntOrNull() ?: 22
val minute = parts.getOrNull(1)?.toIntOrNull() ?: 0
android.app.TimePickerDialog(
context,
{ _, h, m ->
val timeStr = String.format(
java.util.Locale.US,
"%02d:%02d",
h.coerceIn(0, 23),
m.coerceIn(0, 59)
)
eyeProtectionStartTime = timeStr
ThemeConfig.eyeProtectionStartTime = timeStr
},
hour.coerceIn(0, 23),
minute.coerceIn(0, 59),
true
).show()
}
)
ClickableSettingItem(
title = stringResource(R.string.eye_protection_end_time),
option = eyeProtectionEndTime,
onClick = {
val parts = eyeProtectionEndTime.split(":")
val hour = parts.getOrNull(0)?.toIntOrNull() ?: 7
val minute = parts.getOrNull(1)?.toIntOrNull() ?: 0
android.app.TimePickerDialog(
context,
{ _, h, m ->
val timeStr = String.format(
java.util.Locale.US,
"%02d:%02d",
h.coerceIn(0, 23),
m.coerceIn(0, 59)
)
eyeProtectionEndTime = timeStr
ThemeConfig.eyeProtectionEndTime = timeStr
},
hour.coerceIn(0, 23),
minute.coerceIn(0, 59),
true
).show()
}
)
}
}
}
}
}
SplicedColumnGroup(title = stringResource(R.string.compose_related)) { SplicedColumnGroup(title = stringResource(R.string.compose_related)) {
if (!isMiuixEngine) { if (!isMiuixEngine) {
SwitchSettingItem( SwitchSettingItem(
@@ -42,6 +42,7 @@ import io.legado.app.ui.config.otherConfig.OtherConfigScreen
import io.legado.app.ui.config.readConfig.ReadConfigScreen import io.legado.app.ui.config.readConfig.ReadConfigScreen
import io.legado.app.ui.config.themeConfig.ThemeConfigScreen import io.legado.app.ui.config.themeConfig.ThemeConfigScreen
import io.legado.app.ui.config.themeManage.ThemeManageScreen import io.legado.app.ui.config.themeManage.ThemeManageScreen
import io.legado.app.ui.config.labConfig.LabConfigScreen
import io.legado.app.ui.config.translation.TranslationConfigScreen import io.legado.app.ui.config.translation.TranslationConfigScreen
import io.legado.app.ui.rss.article.MainRouteRssSort import io.legado.app.ui.rss.article.MainRouteRssSort
import io.legado.app.ui.rss.article.RssSortRouteScreen import io.legado.app.ui.rss.article.RssSortRouteScreen
@@ -159,7 +160,8 @@ fun MainActivity.mainEntryProvider(
onNavigateToTheme = { backStack.add(MainRouteSettingsTheme) }, onNavigateToTheme = { backStack.add(MainRouteSettingsTheme) },
onNavigateToBackup = { backStack.add(MainRouteSettingsBackup) }, onNavigateToBackup = { backStack.add(MainRouteSettingsBackup) },
onNavigateToDownloadCache = { backStack.add(MainRouteSettingsDownloadCache) }, onNavigateToDownloadCache = { backStack.add(MainRouteSettingsDownloadCache) },
onNavigateToTranslation = { backStack.add(MainRouteSettingsTranslation) } onNavigateToTranslation = { backStack.add(MainRouteSettingsTranslation) },
onNavigateToLab = { backStack.add(MainRouteSettingsLabConfig) }
) )
} }
@@ -195,6 +197,10 @@ fun MainActivity.mainEntryProvider(
TranslationConfigScreen(onBackClick = { onNavigateBack() }) TranslationConfigScreen(onBackClick = { onNavigateBack() })
} }
entry<MainRouteSettingsLabConfig> {
LabConfigScreen(onBackClick = { onNavigateBack() })
}
entry<MainRouteSettingsCustomTheme> { entry<MainRouteSettingsCustomTheme> {
CustomThemeScreen( CustomThemeScreen(
onBackClick = { onNavigateBack() } onBackClick = { onNavigateBack() }
@@ -33,6 +33,9 @@ data object MainRouteSettingsCustomTheme : MainRoute
@Serializable @Serializable
data object MainRouteSettingsThemeManage : MainRoute data object MainRouteSettingsThemeManage : MainRoute
@Serializable
data object MainRouteSettingsLabConfig : MainRoute
@Serializable @Serializable
data object MainRouteSettingsDownloadCache : MainRoute data object MainRouteSettingsDownloadCache : MainRoute
@@ -98,6 +101,7 @@ object MainRouteConst {
const val ROUTE_SETTINGS_THEME = "settings/theme" const val ROUTE_SETTINGS_THEME = "settings/theme"
const val ROUTE_SETTINGS_BACKUP = "settings/backup" const val ROUTE_SETTINGS_BACKUP = "settings/backup"
const val ROUTE_SETTINGS_CUSTOM_THEME = "settings/custom_theme" const val ROUTE_SETTINGS_CUSTOM_THEME = "settings/custom_theme"
const val ROUTE_SETTINGS_LAB_CONFIG = "settings/lab_config"
const val ROUTE_SETTINGS_DOWNLOAD_CACHE = "settings/download_cache" const val ROUTE_SETTINGS_DOWNLOAD_CACHE = "settings/download_cache"
const val ROUTE_SETTINGS_TRANSLATION = "settings/translation" const val ROUTE_SETTINGS_TRANSLATION = "settings/translation"
const val ROUTE_IMPORT_LOCAL = "import/local" const val ROUTE_IMPORT_LOCAL = "import/local"
@@ -50,4 +50,12 @@
android:layout_height="0dp" android:layout_height="0dp"
android:layout_gravity="bottom" /> android:layout_gravity="bottom" />
<io.legado.app.ui.book.read.EyeProtectionOverlay
android:id="@+id/eye_protection_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="false"
android:focusable="false"
android:visibility="gone" />
</FrameLayout> </FrameLayout>
+8 -1
View File
@@ -41,7 +41,6 @@
android:id="@+id/fl_loading" android:id="@+id/fl_loading"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<LinearLayout <LinearLayout
android:id="@+id/ll_loading" android:id="@+id/ll_loading"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -94,4 +93,12 @@
</FrameLayout> </FrameLayout>
<io.legado.app.ui.book.read.EyeProtectionOverlay
android:id="@+id/eye_protection_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="false"
android:focusable="false"
android:visibility="gone" />
</FrameLayout> </FrameLayout>
@@ -1786,4 +1786,27 @@
<string name="return_to_original">返回原文</string> <string name="return_to_original">返回原文</string>
<string name="translation_progress">翻译中 %1$d%%</string> <string name="translation_progress">翻译中 %1$d%%</string>
<string name="translation_started">开始进行翻译,请耐心等待……</string> <string name="translation_started">开始进行翻译,请耐心等待……</string>
<!-- 实验室 -->
<string name="lab_setting">实验室</string>
<string name="lab_enabled_title">启用实验室</string>
<string name="lab_enabled_summary">开启后显示实验性功能,可能不稳定</string>
<string name="lab_display">显示</string>
<string name="lab_eink_display_title">墨水屏显示</string>
<string name="lab_eink_display_summary">在平板界面显示墨水屏选项</string>
<string name="lab_eink_display_hint">可在 主题 → 显示 中调整墨水屏设置</string>
<string name="lab_eye_protection_title">护眼模式</string>
<string name="lab_eye_protection_summary">为漫画和小说阅读器叠加暖色滤镜,减少蓝光</string>
<string name="lab_eye_protection_hint">可在 主题 → 外观 中调整护眼模式</string>
<!-- 护眼模式 -->
<string name="eye_protection">护眼模式</string>
<string name="eye_protection_enabled">启用护眼模式</string>
<string name="eye_protection_enabled_summary">使用暖色调降低蓝光</string>
<string name="color_temperature">屏幕色温</string>
<string name="color_temperature_summary">当前色温:%1$d</string>
<string name="eye_protection_schedule">按时间段启用</string>
<string name="eye_protection_schedule_summary">仅在配置的时间段内生效</string>
<string name="eye_protection_start_time">开始时间</string>
<string name="eye_protection_end_time">结束时间</string>
</resources> </resources>
+23
View File
@@ -1792,4 +1792,27 @@
<string name="homepage_status_in_effect"> · In effect</string> <string name="homepage_status_in_effect"> · In effect</string>
<string name="homepage_status_blocked"> · Blocked</string> <string name="homepage_status_blocked"> · Blocked</string>
<string name="homepage_status_joined"> · Already joined</string> <string name="homepage_status_joined"> · Already joined</string>
<!-- Lab -->
<string name="lab_setting">Lab</string>
<string name="lab_enabled_title">Enable Lab</string>
<string name="lab_enabled_summary">Turn on to show experimental features. These may be unstable.</string>
<string name="lab_display">Display</string>
<string name="lab_eink_display_title">E-Ink Display</string>
<string name="lab_eink_display_summary">Show E-Ink display option in the tablet interface</string>
<string name="lab_eink_display_hint">You can adjust E-Ink display settings in Theme → Display options</string>
<string name="lab_eye_protection_title">Eye Protection</string>
<string name="lab_eye_protection_summary">Apply warm color tint to comic and novel readers to reduce eye strain</string>
<string name="lab_eye_protection_hint">You can adjust eye protection settings in Theme → Appearance</string>
<!-- Eye Protection -->
<string name="eye_protection">Eye Protection</string>
<string name="eye_protection_enabled">Enable Eye Protection</string>
<string name="eye_protection_enabled_summary">Reduce blue light via warm color tint</string>
<string name="color_temperature">Color Temperature</string>
<string name="color_temperature_summary">Current temperature: %1$d</string>
<string name="eye_protection_schedule">Scheduled</string>
<string name="eye_protection_schedule_summary">Only apply during the configured time range</string>
<string name="eye_protection_start_time">Start Time</string>
<string name="eye_protection_end_time">End Time</string>
</resources> </resources>
+2 -15
View File
@@ -47,7 +47,7 @@ export const useBookStore = defineStore('book', {
}, },
getters: { getters: {
bookProgress: (state): BookProgress | undefined => { bookProgress: (state): BookProgress | undefined => {
if (state.catalog.length == 0) return if (state.catalog.length === 0) return
const { chapterIndex, chapterPos, name, author } = state.readingBook const { chapterIndex, chapterPos, name, author } = state.readingBook
const title = state.catalog[chapterIndex]?.title const title = state.catalog[chapterIndex]?.title
if (!title) return if (!title) return
@@ -63,13 +63,12 @@ export const useBookStore = defineStore('book', {
theme: state => { theme: state => {
return state.config.theme return state.config.theme
}, },
isNight: state => state.config.theme == 6, isNight: state => state.config.theme === 6,
}, },
actions: { actions: {
/** 从后端加载书架书籍,优先返回内存缓存 */ /** 从后端加载书架书籍,优先返回内存缓存 */
async loadBookShelf(): Promise<Book[]> { async loadBookShelf(): Promise<Book[]> {
const fetchBookshellf_promise = API.getBookShelf().then(resp => { const fetchBookshellf_promise = API.getBookShelf().then(resp => {
console.log('API.getBookShelf数据返回')
const { isSuccess, data, errorMsg } = resp.data const { isSuccess, data, errorMsg } = resp.data
if (isSuccess === true) { if (isSuccess === true) {
if ( if (
@@ -91,16 +90,13 @@ export const useBookStore = defineStore('book', {
} }
ElMessage.error(errorMsg ?? '后端返回格式错误!') ElMessage.error(errorMsg ?? '后端返回格式错误!')
} }
console.log('书架数据已更新')
return this.shelf return this.shelf
}) })
if (this.shelf.length > 0) { if (this.shelf.length > 0) {
// bookshelf data fetched before:do not await // bookshelf data fetched before:do not await
console.log('返回缓存书架数据')
return this.shelf return this.shelf
} else { } else {
console.log('从阅读后端获取书架数据...')
return await fetchBookshellf_promise return await fetchBookshellf_promise
} }
}, },
@@ -126,7 +122,6 @@ export const useBookStore = defineStore('book', {
ElMessage.info(`书籍${name}: 章节目录已更新`) ElMessage.info(`书籍${name}: 章节目录已更新`)
} }
this.catalog = data this.catalog = data
console.log(`书籍${name}: 章节目录已更新`)
return this.catalog return this.catalog
}) })
if ( if (
@@ -134,10 +129,8 @@ export const useBookStore = defineStore('book', {
this.catalog.length > 0 && this.catalog.length > 0 &&
this.catalog.length - 1 >= chapterIndex this.catalog.length - 1 >= chapterIndex
) { ) {
console.log(`返回书籍《${name}》 缓存的章节目录`)
return this.catalog return this.catalog
} else { } else {
console.log(`从阅读后端获取书籍《${name}》 章节目录数据...`)
return await fetchChapterList_promise return await fetchChapterList_promise
} }
}, },
@@ -155,14 +148,8 @@ export const useBookStore = defineStore('book', {
if (webReadConfigLoadedDate === undefined) { if (webReadConfigLoadedDate === undefined) {
const _config = await API.getReadConfig() const _config = await API.getReadConfig()
webReadConfigLoadedDate = new Date() webReadConfigLoadedDate = new Date()
console.log(
`${this.$id}.loadWebConfig: ${webReadConfigLoadedDate.toLocaleString()}成功加载阅读配置`,
)
return this.setConfig(_config) return this.setConfig(_config)
} }
console.log(
`${this.$id}.loadWebConfig: 已于${webReadConfigLoadedDate.toLocaleString()}成功加载`,
)
}, },
setConfig(config?: webReadConfig) { setConfig(config?: webReadConfig) {
this.config = Object.assign({}, this.config, config) this.config = Object.assign({}, this.config, config)