@@ -338,4 +338,16 @@ object PreferKey {
|
||||
const val homepageModuleHidden = "homepageModuleHidden"
|
||||
const val homepageLayoutMode = "homepageLayoutMode"
|
||||
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.ScrollTimer
|
||||
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.observeEyeProtectionEvents
|
||||
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.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.NetworkUtils
|
||||
import io.legado.app.utils.StartActivityContract
|
||||
import io.legado.app.utils.buildMainHandler
|
||||
import io.legado.app.utils.canScroll
|
||||
import io.legado.app.utils.fastBinarySearch
|
||||
import io.legado.app.utils.findCenterViewPosition
|
||||
@@ -138,6 +141,10 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
||||
|
||||
private var justInitData: Boolean = false
|
||||
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 {
|
||||
ScrollTimer(this, binding.recyclerView, lifecycleScope).apply {
|
||||
setSpeed(AppConfig.mangaAutoPageSpeed)
|
||||
@@ -213,6 +220,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
||||
ReadManga.loadOrUpContent()
|
||||
}
|
||||
binding.flLoading.isVisible = !AppConfig.isEInkMode
|
||||
binding.eyeProtectionOverlay.refresh()
|
||||
mAdapter.addFooterView {
|
||||
ViewLoadMoreBinding.bind(loadMoreView)
|
||||
}
|
||||
@@ -242,6 +250,10 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
||||
val item = mAdapter.getItem(binding.recyclerView.findCenterViewPosition())
|
||||
upInfoBar(item)
|
||||
}
|
||||
observeEyeProtectionEvents(
|
||||
onRefresh = { binding.eyeProtectionOverlay.refresh() },
|
||||
scheduler = eyeProtectionScheduler
|
||||
)
|
||||
}
|
||||
|
||||
private fun initRecyclerView() {
|
||||
@@ -457,10 +469,13 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
||||
}
|
||||
ReadManga.initReadTime()
|
||||
ReadManga.startAutoSaveSession()
|
||||
binding.eyeProtectionOverlay.refresh()
|
||||
eyeProtectionScheduler.schedule()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
eyeProtectionScheduler.cancel()
|
||||
if (ReadManga.inBookshelf) {
|
||||
ReadManga.saveRead()
|
||||
if (!BuildConfig.DEBUG) {
|
||||
|
||||
@@ -122,13 +122,16 @@ class MangaAdapter(private val context: Context) :
|
||||
) {
|
||||
"ARGB values must be between 0-255"
|
||||
}
|
||||
val matrix = floatArrayOf(
|
||||
val baseMatrix = floatArrayOf(
|
||||
(255 - mConfig.r) / 255f, 0f, 0f, 0f, 0f,
|
||||
0f, (255 - mConfig.g) / 255f, 0f, 0f, 0f,
|
||||
0f, 0f, (255 - mConfig.b) / 255f, 0f, 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() {
|
||||
|
||||
@@ -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-100,0 不变,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 val handler by lazy { buildMainHandler() }
|
||||
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 upSeekBarThrottle = throttle(200) {
|
||||
runOnUiThread {
|
||||
@@ -302,6 +305,7 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
ReadBook.register(this)
|
||||
binding.cursorLeft.setOnTouchListener(this)
|
||||
binding.cursorRight.setOnTouchListener(this)
|
||||
binding.eyeProtectionOverlay.refresh()
|
||||
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
if (isShowingSearchResult) {
|
||||
@@ -400,6 +404,8 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
registerReceiver(timeBatteryReceiver, timeBatteryReceiver.filter)
|
||||
binding.readView.upTime()
|
||||
screenOffTimerStart()
|
||||
binding.eyeProtectionOverlay.refresh()
|
||||
eyeProtectionScheduler.schedule()
|
||||
// 网络监听,当从无网切换到网络环境时同步进度(注意注册的同时就会收到监听,因此界面激活时无需重复执行同步操作)
|
||||
networkChangedListener.register()
|
||||
networkChangedListener.onNetworkChanged = {
|
||||
@@ -413,6 +419,7 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
autoPageStop()
|
||||
eyeProtectionScheduler.cancel()
|
||||
backupJob?.cancel()
|
||||
ReadBook.saveRead()
|
||||
ReadBook.stopAutoSaveSession()
|
||||
@@ -1985,6 +1992,10 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
viewModel.refreshContentDur(it)
|
||||
}
|
||||
}
|
||||
observeEyeProtectionEvents(
|
||||
onRefresh = { binding.eyeProtectionOverlay.refresh() },
|
||||
scheduler = eyeProtectionScheduler
|
||||
)
|
||||
}
|
||||
|
||||
private fun upScreenTimeOut() {
|
||||
|
||||
@@ -27,7 +27,8 @@ fun ConfigNavScreen(
|
||||
onNavigateToTheme: () -> Unit,
|
||||
onNavigateToBackup: () -> Unit,
|
||||
onNavigateToDownloadCache: () -> Unit,
|
||||
onNavigateToTranslation: () -> Unit
|
||||
onNavigateToTranslation: () -> Unit,
|
||||
onNavigateToLab: () -> Unit
|
||||
) {
|
||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||
|
||||
@@ -80,6 +81,10 @@ fun ConfigNavScreen(
|
||||
title = stringResource(R.string.translation_config),
|
||||
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, "")
|
||||
|
||||
// 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 =
|
||||
!(if (isDark) bgImageDark else bgImageLight).isNullOrBlank()
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ import androidx.compose.material3.ToggleButtonDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.OldThemeConfig
|
||||
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.ThemeEngine
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
@@ -296,9 +298,12 @@ fun ThemeConfigScreen(
|
||||
if (!isMiuixEngine) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
val visibleThemes = themes.filter { (_, value) ->
|
||||
value != "4" || (LabConfig.labEnabled && LabConfig.eInkDisplay)
|
||||
}
|
||||
ThemeColorSelector(
|
||||
context = context,
|
||||
themes = themes,
|
||||
themes = visibleThemes,
|
||||
selectedTheme = selectedTheme,
|
||||
isDark = isDarkTheme,
|
||||
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)) {
|
||||
if (!isMiuixEngine) {
|
||||
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.themeConfig.ThemeConfigScreen
|
||||
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.rss.article.MainRouteRssSort
|
||||
import io.legado.app.ui.rss.article.RssSortRouteScreen
|
||||
@@ -159,7 +160,8 @@ fun MainActivity.mainEntryProvider(
|
||||
onNavigateToTheme = { backStack.add(MainRouteSettingsTheme) },
|
||||
onNavigateToBackup = { backStack.add(MainRouteSettingsBackup) },
|
||||
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() })
|
||||
}
|
||||
|
||||
entry<MainRouteSettingsLabConfig> {
|
||||
LabConfigScreen(onBackClick = { onNavigateBack() })
|
||||
}
|
||||
|
||||
entry<MainRouteSettingsCustomTheme> {
|
||||
CustomThemeScreen(
|
||||
onBackClick = { onNavigateBack() }
|
||||
|
||||
@@ -33,6 +33,9 @@ data object MainRouteSettingsCustomTheme : MainRoute
|
||||
@Serializable
|
||||
data object MainRouteSettingsThemeManage : MainRoute
|
||||
|
||||
@Serializable
|
||||
data object MainRouteSettingsLabConfig : MainRoute
|
||||
|
||||
@Serializable
|
||||
data object MainRouteSettingsDownloadCache : MainRoute
|
||||
|
||||
@@ -98,6 +101,7 @@ object MainRouteConst {
|
||||
const val ROUTE_SETTINGS_THEME = "settings/theme"
|
||||
const val ROUTE_SETTINGS_BACKUP = "settings/backup"
|
||||
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_TRANSLATION = "settings/translation"
|
||||
const val ROUTE_IMPORT_LOCAL = "import/local"
|
||||
|
||||
@@ -50,4 +50,12 @@
|
||||
android:layout_height="0dp"
|
||||
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>
|
||||
@@ -41,7 +41,6 @@
|
||||
android:id="@+id/fl_loading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_loading"
|
||||
android:layout_width="match_parent"
|
||||
@@ -94,4 +93,12 @@
|
||||
|
||||
</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>
|
||||
|
||||
@@ -1786,4 +1786,27 @@
|
||||
<string name="return_to_original">返回原文</string>
|
||||
<string name="translation_progress">翻译中 %1$d%%</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>
|
||||
|
||||
@@ -1792,4 +1792,27 @@
|
||||
<string name="homepage_status_in_effect"> · In effect</string>
|
||||
<string name="homepage_status_blocked"> · Blocked</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>
|
||||
|
||||
@@ -47,7 +47,7 @@ export const useBookStore = defineStore('book', {
|
||||
},
|
||||
getters: {
|
||||
bookProgress: (state): BookProgress | undefined => {
|
||||
if (state.catalog.length == 0) return
|
||||
if (state.catalog.length === 0) return
|
||||
const { chapterIndex, chapterPos, name, author } = state.readingBook
|
||||
const title = state.catalog[chapterIndex]?.title
|
||||
if (!title) return
|
||||
@@ -63,13 +63,12 @@ export const useBookStore = defineStore('book', {
|
||||
theme: state => {
|
||||
return state.config.theme
|
||||
},
|
||||
isNight: state => state.config.theme == 6,
|
||||
isNight: state => state.config.theme === 6,
|
||||
},
|
||||
actions: {
|
||||
/** 从后端加载书架书籍,优先返回内存缓存 */
|
||||
async loadBookShelf(): Promise<Book[]> {
|
||||
const fetchBookshellf_promise = API.getBookShelf().then(resp => {
|
||||
console.log('API.getBookShelf数据返回')
|
||||
const { isSuccess, data, errorMsg } = resp.data
|
||||
if (isSuccess === true) {
|
||||
if (
|
||||
@@ -91,16 +90,13 @@ export const useBookStore = defineStore('book', {
|
||||
}
|
||||
ElMessage.error(errorMsg ?? '后端返回格式错误!')
|
||||
}
|
||||
console.log('书架数据已更新')
|
||||
return this.shelf
|
||||
})
|
||||
|
||||
if (this.shelf.length > 0) {
|
||||
// bookshelf data fetched before:do not await
|
||||
console.log('返回缓存书架数据')
|
||||
return this.shelf
|
||||
} else {
|
||||
console.log('从阅读后端获取书架数据...')
|
||||
return await fetchBookshellf_promise
|
||||
}
|
||||
},
|
||||
@@ -126,7 +122,6 @@ export const useBookStore = defineStore('book', {
|
||||
ElMessage.info(`书籍${name}: 章节目录已更新`)
|
||||
}
|
||||
this.catalog = data
|
||||
console.log(`书籍${name}: 章节目录已更新`)
|
||||
return this.catalog
|
||||
})
|
||||
if (
|
||||
@@ -134,10 +129,8 @@ export const useBookStore = defineStore('book', {
|
||||
this.catalog.length > 0 &&
|
||||
this.catalog.length - 1 >= chapterIndex
|
||||
) {
|
||||
console.log(`返回书籍《${name}》 缓存的章节目录`)
|
||||
return this.catalog
|
||||
} else {
|
||||
console.log(`从阅读后端获取书籍《${name}》 章节目录数据...`)
|
||||
return await fetchChapterList_promise
|
||||
}
|
||||
},
|
||||
@@ -155,14 +148,8 @@ export const useBookStore = defineStore('book', {
|
||||
if (webReadConfigLoadedDate === undefined) {
|
||||
const _config = await API.getReadConfig()
|
||||
webReadConfigLoadedDate = new Date()
|
||||
console.log(
|
||||
`${this.$id}.loadWebConfig: ${webReadConfigLoadedDate.toLocaleString()}成功加载阅读配置`,
|
||||
)
|
||||
return this.setConfig(_config)
|
||||
}
|
||||
console.log(
|
||||
`${this.$id}.loadWebConfig: 已于${webReadConfigLoadedDate.toLocaleString()}成功加载`,
|
||||
)
|
||||
},
|
||||
setConfig(config?: webReadConfig) {
|
||||
this.config = Object.assign({}, this.config, config)
|
||||
|
||||
Reference in New Issue
Block a user