fix: 阅读菜单模糊着色、调色板样式、标题胶囊布局及主题覆盖修复

This commit is contained in:
HapeLee committed 2026-06-15 01:33:28 +08:00
1 parent 5a587222eb
commit 9bb8571cc7
21 files changed
+448 -90

No files matched your search

@@ -308,6 +308,8 @@ object PreferKey {
const val readMenuBottomBarBlurStyle = "readMenuBottomBarBlurStyle" const val readMenuBottomBarBlurStyle = "readMenuBottomBarBlurStyle"
const val readMenuBlurRadius = "readMenuBlurRadius" const val readMenuBlurRadius = "readMenuBlurRadius"
const val readMenuBlurAlpha = "readMenuBlurAlpha" const val readMenuBlurAlpha = "readMenuBlurAlpha"
const val readMenuBlurColor = "readMenuBlurColor"
const val readMenuPaletteStyle = "readMenuPaletteStyle"
const val readMenuLensRadius = "readMenuLensRadius" const val readMenuLensRadius = "readMenuLensRadius"
const val readMenuBorderWidth = "readMenuBorderWidth" const val readMenuBorderWidth = "readMenuBorderWidth"
const val readMenuBorderColor = "readMenuBorderColor" const val readMenuBorderColor = "readMenuBorderColor"
@@ -94,6 +94,8 @@ data class ReadPreferences(
val readMenuBottomBarBlurStyle: Int = ReadMenuBlurStyle.Solid, val readMenuBottomBarBlurStyle: Int = ReadMenuBlurStyle.Solid,
val readMenuBlurRadius: Int = 24, val readMenuBlurRadius: Int = 24,
val readMenuBlurAlpha: Int = 60, val readMenuBlurAlpha: Int = 60,
val readMenuBlurColor: Int = 0,
val readMenuPaletteStyle: String = "",
val readMenuLensRadius: Float = 24f, val readMenuLensRadius: Float = 24f,
val readMenuBorderWidth: Int = 0, val readMenuBorderWidth: Int = 0,
val readMenuBorderColor: Int = 0, val readMenuBorderColor: Int = 0,
@@ -335,6 +337,12 @@ class ReadSettingsRepository(
suspend fun setReadMenuBlurAlpha(value: Int) = suspend fun setReadMenuBlurAlpha(value: Int) =
settingsRepository.putInt(PreferKey.readMenuBlurAlpha, value.coerceIn(0, 100)) settingsRepository.putInt(PreferKey.readMenuBlurAlpha, value.coerceIn(0, 100))
suspend fun setReadMenuBlurColor(value: Int) =
settingsRepository.putInt(PreferKey.readMenuBlurColor, value)
suspend fun setReadMenuPaletteStyle(value: String) =
settingsRepository.putString(PreferKey.readMenuPaletteStyle, value)
suspend fun setReadMenuLensRadius(value: Float) = suspend fun setReadMenuLensRadius(value: Float) =
settingsRepository.putFloat(PreferKey.readMenuLensRadius, value.coerceIn(0f, 48f)) settingsRepository.putFloat(PreferKey.readMenuLensRadius, value.coerceIn(0f, 48f))
@@ -446,6 +454,7 @@ class ReadSettingsRepository(
readMenuBottomBarBlurMode = this[Keys.ReadMenuBottomBarBlurMode] readMenuBottomBarBlurMode = this[Keys.ReadMenuBottomBarBlurMode]
?: ReadMenuBlurMode.None, ?: ReadMenuBlurMode.None,
readMenuTopBarLiquidGlassButtons = this[Keys.ReadMenuTopBarLiquidGlassButtons] ?: false, readMenuTopBarLiquidGlassButtons = this[Keys.ReadMenuTopBarLiquidGlassButtons] ?: false,
readMenuTopBarTitleCapsule = this[Keys.ReadMenuTopBarTitleCapsule] ?: false,
readMenuBottomBarLiquidGlassButtons = this[Keys.ReadMenuBottomBarLiquidGlassButtons] readMenuBottomBarLiquidGlassButtons = this[Keys.ReadMenuBottomBarLiquidGlassButtons]
?: false, ?: false,
readMenuTopBarBlurStyle = this[Keys.ReadMenuTopBarBlurStyle] readMenuTopBarBlurStyle = this[Keys.ReadMenuTopBarBlurStyle]
@@ -454,6 +463,8 @@ class ReadSettingsRepository(
?: ReadMenuBlurStyle.Solid, ?: ReadMenuBlurStyle.Solid,
readMenuBlurRadius = this[Keys.ReadMenuBlurRadius] ?: 24, readMenuBlurRadius = this[Keys.ReadMenuBlurRadius] ?: 24,
readMenuBlurAlpha = this[Keys.ReadMenuBlurAlpha] ?: 60, readMenuBlurAlpha = this[Keys.ReadMenuBlurAlpha] ?: 60,
readMenuBlurColor = this[Keys.ReadMenuBlurColor] ?: 0,
readMenuPaletteStyle = this[Keys.ReadMenuPaletteStyle] ?: "",
readMenuLensRadius = this[Keys.ReadMenuLensRadius] ?: 24f, readMenuLensRadius = this[Keys.ReadMenuLensRadius] ?: 24f,
readMenuBorderWidth = this[Keys.ReadMenuBorderWidth] ?: 0, readMenuBorderWidth = this[Keys.ReadMenuBorderWidth] ?: 0,
readMenuBorderColor = this[Keys.ReadMenuBorderColor] ?: 0, readMenuBorderColor = this[Keys.ReadMenuBorderColor] ?: 0,
@@ -540,12 +551,16 @@ class ReadSettingsRepository(
val ReadMenuBottomBarBlurMode = intPreferencesKey(PreferKey.readMenuBottomBarBlurMode) val ReadMenuBottomBarBlurMode = intPreferencesKey(PreferKey.readMenuBottomBarBlurMode)
val ReadMenuTopBarLiquidGlassButtons = val ReadMenuTopBarLiquidGlassButtons =
booleanPreferencesKey(PreferKey.readMenuTopBarLiquidGlassButtons) booleanPreferencesKey(PreferKey.readMenuTopBarLiquidGlassButtons)
val ReadMenuTopBarTitleCapsule =
booleanPreferencesKey(PreferKey.readMenuTopBarTitleCapsule)
val ReadMenuBottomBarLiquidGlassButtons = val ReadMenuBottomBarLiquidGlassButtons =
booleanPreferencesKey(PreferKey.readMenuBottomBarLiquidGlassButtons) booleanPreferencesKey(PreferKey.readMenuBottomBarLiquidGlassButtons)
val ReadMenuTopBarBlurStyle = intPreferencesKey(PreferKey.readMenuTopBarBlurStyle) val ReadMenuTopBarBlurStyle = intPreferencesKey(PreferKey.readMenuTopBarBlurStyle)
val ReadMenuBottomBarBlurStyle = intPreferencesKey(PreferKey.readMenuBottomBarBlurStyle) val ReadMenuBottomBarBlurStyle = intPreferencesKey(PreferKey.readMenuBottomBarBlurStyle)
val ReadMenuBlurRadius = intPreferencesKey(PreferKey.readMenuBlurRadius) val ReadMenuBlurRadius = intPreferencesKey(PreferKey.readMenuBlurRadius)
val ReadMenuBlurAlpha = intPreferencesKey(PreferKey.readMenuBlurAlpha) val ReadMenuBlurAlpha = intPreferencesKey(PreferKey.readMenuBlurAlpha)
val ReadMenuBlurColor = intPreferencesKey(PreferKey.readMenuBlurColor)
val ReadMenuPaletteStyle = stringPreferencesKey(PreferKey.readMenuPaletteStyle)
val ReadMenuLensRadius = floatPreferencesKey(PreferKey.readMenuLensRadius) val ReadMenuLensRadius = floatPreferencesKey(PreferKey.readMenuLensRadius)
val ReadMenuBorderWidth = intPreferencesKey(PreferKey.readMenuBorderWidth) val ReadMenuBorderWidth = intPreferencesKey(PreferKey.readMenuBorderWidth)
val ReadMenuBorderColor = intPreferencesKey(PreferKey.readMenuBorderColor) val ReadMenuBorderColor = intPreferencesKey(PreferKey.readMenuBorderColor)
@@ -25,12 +25,13 @@ import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
sealed interface ChangeSourceSearchEvent { sealed interface ChangeSourceSearchEvent {
data object Started : ChangeSourceSearchEvent data class Started(val totalSources: Int) : ChangeSourceSearchEvent
data class Progress( data class Progress(
val processedSources: Int, val processedSources: Int,
val totalSources: Int, val totalSources: Int,
@@ -69,23 +70,20 @@ class ChangeSourceSearchUseCase(
if (bookSourceParts.isEmpty()) { if (bookSourceParts.isEmpty()) {
throw io.legado.app.exception.NoStackTraceException("启用书源为空") throw io.legado.app.exception.NoStackTraceException("启用书源为空")
} }
val sources = bookSourceParts.mapNotNull { it.getBookSource() }
if (sources.isEmpty()) {
throw io.legado.app.exception.NoStackTraceException("启用书源为空")
}
tocMap.clear() tocMap.clear()
bookMap.clear() bookMap.clear()
tocMapChapterCount.set(0) tocMapChapterCount.set(0)
emit(ChangeSourceSearchEvent.Started) val totalSources = bookSourceParts.size
emit(ChangeSourceSearchEvent.Started(totalSources))
var processedSources = 0 var processedSources = 0
var resultCount = 0 var resultCount = 0
val totalSources = sources.size
val concurrency = threadCount.coerceAtLeast(1) val concurrency = threadCount.coerceAtLeast(1)
sources.asFlow() bookSourceParts.asFlow()
.mapNotNull { it.getBookSource() }
.flatMapMerge(concurrency) { source -> .flatMapMerge(concurrency) { source ->
flow { flow {
val books = try { val books = try {
@@ -231,7 +229,7 @@ class ChangeSourceSearchUseCase(
contentProcessor contentProcessor
) )
} }
return null return book.toSearchBook()
} }
private suspend fun loadBookWordCount( private suspend fun loadBookWordCount(
@@ -236,6 +236,8 @@ object ReadBookConfig {
var readMenuBottomBarBlurStyle by clampedPrefDelegate(PreferKey.readMenuBottomBarBlurStyle, ReadMenuBlurStyle.Solid, 0..1) var readMenuBottomBarBlurStyle by clampedPrefDelegate(PreferKey.readMenuBottomBarBlurStyle, ReadMenuBlurStyle.Solid, 0..1)
var readMenuBlurRadius by clampedPrefDelegate(PreferKey.readMenuBlurRadius, 24, 0..32) var readMenuBlurRadius by clampedPrefDelegate(PreferKey.readMenuBlurRadius, 24, 0..32)
var readMenuBlurAlpha by clampedPrefDelegate(PreferKey.readMenuBlurAlpha, 60, 0..100) var readMenuBlurAlpha by clampedPrefDelegate(PreferKey.readMenuBlurAlpha, 60, 0..100)
var readMenuBlurColor by prefDelegate(PreferKey.readMenuBlurColor, 0)
var readMenuPaletteStyle: String = ""
var readMenuLensRadius by clampedPrefDelegate(PreferKey.readMenuLensRadius, 24f, 0f..48f) var readMenuLensRadius by clampedPrefDelegate(PreferKey.readMenuLensRadius, 24f, 0f..48f)
var readMenuBorderWidth by clampedPrefDelegate(PreferKey.readMenuBorderWidth, 0, 0..4) var readMenuBorderWidth by clampedPrefDelegate(PreferKey.readMenuBorderWidth, 0, 0..4)
var titleBarIconPosition by clampedPrefDelegate(PreferKey.titleBarIconPosition, 0, 0..3) var titleBarIconPosition by clampedPrefDelegate(PreferKey.titleBarIconPosition, 0, 0..3)
@@ -70,8 +70,11 @@ class ChangeBookSourceComposeViewModel(
val loadWordCount: Boolean get() = ChangeSourceConfig.loadWordCount val loadWordCount: Boolean get() = ChangeSourceConfig.loadWordCount
// Internal state // Internal state
private val chapterNumRegex = "^\\[(\\d+)]".toRegex()
private var searchJob: Job? = null private var searchJob: Job? = null
private var oldBook: Book? = null private var oldBook: Book? = null
private var bookName: String = ""
private var bookAuthor: String = ""
private var fromReadBookActivity: Boolean = false private var fromReadBookActivity: Boolean = false
private var screenKey: String = "" private var screenKey: String = ""
private val searchResults = mutableListOf<SearchBook>() private val searchResults = mutableListOf<SearchBook>()
@@ -86,15 +89,42 @@ class ChangeBookSourceComposeViewModel(
fun initData(name: String, author: String, book: Book, fromReadBookActivity: Boolean) { fun initData(name: String, author: String, book: Book, fromReadBookActivity: Boolean) {
this.oldBook = book this.oldBook = book
this.bookName = name
this.bookAuthor = author
this.fromReadBookActivity = fromReadBookActivity this.fromReadBookActivity = fromReadBookActivity
if (searchJob?.isActive != true) { if (searchJob?.isActive != true) {
startSearch() viewModelScope.launch {
val dbBooks = getDbSearchBooks()
if (dbBooks.isNotEmpty()) {
searchResults.clear()
searchResults.addAll(dbBooks)
searchResults.forEach { bookMap[it.primaryStr()] = it }
filterResults()
} else {
startSearch()
}
}
}
}
private fun getDbSearchBooks(): List<SearchBook> {
val scopeRaw = ChangeSourceConfig.searchScope
return if (screenKey.isEmpty()) {
io.legado.app.data.appDb.searchBookDao.changeSourceByGroup(
bookName, bookAuthor, scopeRaw
)
} else {
io.legado.app.data.appDb.searchBookDao.changeSourceSearch(
bookName, bookAuthor, screenKey, scopeRaw
)
} }
} }
fun startSearch() { fun startSearch() {
val book = oldBook ?: return val book = oldBook ?: return
stopSearch() stopSearch()
// 清除 DB 中的旧记录
io.legado.app.data.appDb.searchBookDao.clear(bookName, bookAuthor)
searchResults.clear() searchResults.clear()
bookMap.clear() bookMap.clear()
tocMap.clear() tocMap.clear()
@@ -113,6 +143,7 @@ class ChangeBookSourceComposeViewModel(
when (event) { when (event) {
is ChangeSourceSearchEvent.Started -> { is ChangeSourceSearchEvent.Started -> {
_isSearching.value = true _isSearching.value = true
totalSourceCount = event.totalSources
} }
is ChangeSourceSearchEvent.Progress -> { is ChangeSourceSearchEvent.Progress -> {
@@ -123,6 +154,8 @@ class ChangeBookSourceComposeViewModel(
is ChangeSourceSearchEvent.Result -> { is ChangeSourceSearchEvent.Result -> {
searchResults.add(event.searchBook) searchResults.add(event.searchBook)
bookMap[event.searchBook.primaryStr()] = event.searchBook bookMap[event.searchBook.primaryStr()] = event.searchBook
// 持久化到 DB
io.legado.app.data.appDb.searchBookDao.insert(event.searchBook)
filterResults() filterResults()
} }
@@ -179,12 +212,24 @@ class ChangeBookSourceComposeViewModel(
it.name.contains(screenKey) || it.originName.contains(screenKey) it.name.contains(screenKey) || it.originName.contains(screenKey)
} }
} }
val sorted = filtered.sortedWith( val comparator = if (ChangeSourceConfig.loadWordCount) {
compareByDescending<SearchBook> { ObservableSourceConfig.getBookScore(it) }
.thenByDescending { io.legado.app.help.config.SourceConfig.getSourceScore(it.origin) }
.thenByDescending { it.chapterWordCount > 1000 }
.thenByDescending { getChapterNum(it.chapterWordCountText) }
.thenByDescending { it.chapterWordCount }
.thenBy { it.originOrder }
} else {
compareByDescending<SearchBook> { ObservableSourceConfig.getBookScore(it) } compareByDescending<SearchBook> { ObservableSourceConfig.getBookScore(it) }
.thenByDescending { io.legado.app.help.config.SourceConfig.getSourceScore(it.origin) } .thenByDescending { io.legado.app.help.config.SourceConfig.getSourceScore(it.origin) }
.thenBy { it.originOrder } .thenBy { it.originOrder }
) }
_searchDataFlow.value = sorted _searchDataFlow.value = filtered.sortedWith(comparator)
}
private fun getChapterNum(text: String?): Int {
if (text.isNullOrBlank()) return 0
return chapterNumRegex.find(text)?.groupValues?.get(1)?.toIntOrNull() ?: 0
} }
fun getToc( fun getToc(
@@ -247,7 +292,14 @@ class ChangeBookSourceComposeViewModel(
fun refresh() { fun refresh() {
searchResults.clear() searchResults.clear()
bookMap.clear() bookMap.clear()
startSearch() val dbBooks = getDbSearchBooks()
if (dbBooks.isNotEmpty()) {
searchResults.addAll(dbBooks)
searchResults.forEach { bookMap[it.primaryStr()] = it }
filterResults()
} else {
startSearch()
}
} }
// Source actions // Source actions
@@ -340,6 +392,23 @@ class ChangeBookSourceComposeViewModel(
saveScope() saveScope()
} }
fun applyScopeSelection(selection: io.legado.app.ui.book.search.ScopeSelection) {
if (selection.isSourceScope) {
if (selection.sources.isEmpty()) {
searchScope.update("")
} else {
searchScope.updateSources(selection.sources)
}
} else {
if (selection.groupNames.isEmpty()) {
searchScope.update("")
} else {
searchScope.update(selection.groupNames)
}
}
saveScope()
}
private fun saveScope() { private fun saveScope() {
ChangeSourceConfig.searchScope = searchScope.toString() ChangeSourceConfig.searchScope = searchScope.toString()
_scopeUiState.value = ScopeUiState( _scopeUiState.value = ScopeUiState(
@@ -46,12 +46,14 @@ private fun rememberReadBookColorTheme(
preferences: ReadPreferences, preferences: ReadPreferences,
): ThemeOverrideState? { ): ThemeOverrideState? {
val isAppDark = LegadoTheme.isDark val isAppDark = LegadoTheme.isDark
val paletteStyle = preferences.readMenuPaletteStyle
return when (preferences.readBarStyle) { return when (preferences.readBarStyle) {
1 -> rememberReadBackgroundTheme(styleConfig, isAppDark) 1 -> rememberReadBackgroundTheme(styleConfig, isAppDark, paletteStyle)
2 -> rememberCustomReadMenuTheme( 2 -> rememberCustomReadMenuTheme(
styleConfig = styleConfig, styleConfig = styleConfig,
preferences = preferences, preferences = preferences,
isAppDark = isAppDark, isAppDark = isAppDark,
paletteStyle = paletteStyle,
) )
else -> null else -> null
} }
@@ -61,6 +63,7 @@ private fun rememberReadBookColorTheme(
private fun rememberReadBackgroundTheme( private fun rememberReadBackgroundTheme(
styleConfig: ReadBookStyleConfig, styleConfig: ReadBookStyleConfig,
isAppDark: Boolean, isAppDark: Boolean,
paletteStyle: String,
): ThemeOverrideState? { ): ThemeOverrideState? {
val background = remember(styleConfig, isAppDark) { val background = remember(styleConfig, isAppDark) {
runCatching { ReadStyleResolver.currentBackground(ReadBookConfig.durConfig) }.getOrNull() runCatching { ReadStyleResolver.currentBackground(ReadBookConfig.durConfig) }.getOrNull()
@@ -85,6 +88,7 @@ private fun rememberReadBackgroundTheme(
backgroundColor = surfaceColor, backgroundColor = surfaceColor,
containerColor = null, containerColor = null,
fallbackDark = isAppDark, fallbackDark = isAppDark,
paletteStyle = paletteStyle,
) )
} }
@@ -93,6 +97,7 @@ private fun rememberCustomReadMenuTheme(
styleConfig: ReadBookStyleConfig, styleConfig: ReadBookStyleConfig,
preferences: ReadPreferences, preferences: ReadPreferences,
isAppDark: Boolean, isAppDark: Boolean,
paletteStyle: String,
): ThemeOverrideState { ): ThemeOverrideState {
val menuBackgroundColor = remember( val menuBackgroundColor = remember(
styleConfig, styleConfig,
@@ -127,6 +132,7 @@ private fun rememberCustomReadMenuTheme(
menuBackgroundColor = menuBackgroundColor, menuBackgroundColor = menuBackgroundColor,
menuContainerColor = menuContainerColor, menuContainerColor = menuContainerColor,
isDark = isAppDark, isDark = isAppDark,
paletteStyle = paletteStyle,
) )
} }
@@ -136,11 +142,13 @@ private fun rememberCustomReadMenuTheme(
containerColor = null, containerColor = null,
fallbackDark = isAppDark, fallbackDark = isAppDark,
deriveDarkFromColor = false, deriveDarkFromColor = false,
paletteStyle = paletteStyle,
) ?: buildReadThemeOverride( ) ?: buildReadThemeOverride(
seedColor = accentColor, seedColor = accentColor,
backgroundColor = null, backgroundColor = null,
containerColor = null, containerColor = null,
isDark = isAppDark, isDark = isAppDark,
paletteStyle = paletteStyle,
) )
} }
@@ -150,13 +158,15 @@ private fun rememberCustomReadMenuThemeOverride(
menuBackgroundColor: Color, menuBackgroundColor: Color,
menuContainerColor: Color, menuContainerColor: Color,
isDark: Boolean, isDark: Boolean,
paletteStyle: String,
): ThemeOverrideState { ): ThemeOverrideState {
return remember(accentColor, menuBackgroundColor, menuContainerColor, isDark) { return remember(accentColor, menuBackgroundColor, menuContainerColor, isDark, paletteStyle) {
buildReadThemeOverride( buildReadThemeOverride(
seedColor = accentColor, seedColor = accentColor,
backgroundColor = null, backgroundColor = null,
containerColor = null, containerColor = null,
isDark = isDark, isDark = isDark,
paletteStyle = paletteStyle,
).let { base -> ).let { base ->
base.copy( base.copy(
colorScheme = base.colorScheme.withCustomReadMenuColors( colorScheme = base.colorScheme.withCustomReadMenuColors(
@@ -176,6 +186,7 @@ private fun rememberReadThemeOverride(
containerColor: Color?, containerColor: Color?,
fallbackDark: Boolean = LegadoTheme.isDark, fallbackDark: Boolean = LegadoTheme.isDark,
deriveDarkFromColor: Boolean = true, deriveDarkFromColor: Boolean = true,
paletteStyle: String = "",
): ThemeOverrideState? { ): ThemeOverrideState? {
val isDark = remember(backgroundColor, containerColor, fallbackDark, deriveDarkFromColor) { val isDark = remember(backgroundColor, containerColor, fallbackDark, deriveDarkFromColor) {
if (deriveDarkFromColor) { if (deriveDarkFromColor) {
@@ -184,12 +195,13 @@ private fun rememberReadThemeOverride(
fallbackDark fallbackDark
} }
} }
return remember(seedColor, backgroundColor, containerColor, isDark) { return remember(seedColor, backgroundColor, containerColor, isDark, paletteStyle) {
buildReadThemeOverride( buildReadThemeOverride(
seedColor = seedColor, seedColor = seedColor,
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
containerColor = containerColor, containerColor = containerColor,
isDark = isDark, isDark = isDark,
paletteStyle = paletteStyle,
) )
} }
} }
@@ -199,12 +211,17 @@ private fun buildReadThemeOverride(
backgroundColor: Color?, backgroundColor: Color?,
containerColor: Color?, containerColor: Color?,
isDark: Boolean, isDark: Boolean,
paletteStyle: String = "",
): ThemeOverrideState { ): ThemeOverrideState {
val colorSpec = ThemeResolver.resolveColorSpecFromMaterialVersion(ThemeConfig.materialVersion) val colorSpec = ThemeResolver.resolveColorSpecFromMaterialVersion(ThemeConfig.materialVersion)
val resolvedPaletteStyle = paletteStyle
.takeIf { it.isNotBlank() }
?.let { ThemeResolver.resolvePaletteStyle(it) }
?: ThemeResolver.resolvePaletteStyle(ThemeConfig.paletteStyle)
val base = buildThemeOverrideState( val base = buildThemeOverrideState(
seedColor = seedColor, seedColor = seedColor,
isDark = isDark, isDark = isDark,
paletteStyle = ThemeResolver.resolvePaletteStyle(ThemeConfig.paletteStyle), paletteStyle = resolvedPaletteStyle,
colorSpec = colorSpec, colorSpec = colorSpec,
usePureBlack = false, usePureBlack = false,
) )
@@ -182,6 +182,8 @@ data class ReadMenuConfig(
val readMenuBorderColor: Int = 0, val readMenuBorderColor: Int = 0,
val readMenuBorderColorNight: Int = 0, val readMenuBorderColorNight: Int = 0,
val readMenuBlurAlpha: Int = 60, val readMenuBlurAlpha: Int = 60,
val readMenuBlurColor: Int = 0,
val readMenuPaletteStyle: String = "",
val readMenuBlurRadius: Int = 24, val readMenuBlurRadius: Int = 24,
val readMenuLensRadius: Float = 24f, val readMenuLensRadius: Float = 24f,
val readMenuTopBarBlurMode: Int = ReadMenuBlurMode.None, val readMenuTopBarBlurMode: Int = ReadMenuBlurMode.None,
@@ -482,6 +484,8 @@ sealed interface ReadBookIntent {
data object OpenSystemTtsSettings : ReadBookIntent data object OpenSystemTtsSettings : ReadBookIntent
data object ClearTtsCache : ReadBookIntent data object ClearTtsCache : ReadBookIntent
data class SelectFont(val path: String) : ReadBookIntent data class SelectFont(val path: String) : ReadBookIntent
data class SelectTitleFont(val path: String) : ReadBookIntent
data class SelectTitleSystemTypeface(val index: Int) : ReadBookIntent
data class SelectSystemTypeface(val index: Int) : ReadBookIntent data class SelectSystemTypeface(val index: Int) : ReadBookIntent
data class ColorSelected(val dialogId: Int, val color: Int) : ReadBookIntent data class ColorSelected(val dialogId: Int, val color: Int) : ReadBookIntent
@@ -653,6 +657,7 @@ sealed interface ReadBookSheet {
data object ShadowSet : ReadBookSheet data object ShadowSet : ReadBookSheet
data object UnderlineConfig : ReadBookSheet data object UnderlineConfig : ReadBookSheet
data object FontSelect : ReadBookSheet data object FontSelect : ReadBookSheet
data object TitleFontSelect : ReadBookSheet
data object HighlightRuleConfig : ReadBookSheet data object HighlightRuleConfig : ReadBookSheet
data object MoreConfig : ReadBookSheet data object MoreConfig : ReadBookSheet
data object BgTextConfig : ReadBookSheet data object BgTextConfig : ReadBookSheet
@@ -769,6 +774,18 @@ sealed interface ConfigUpdate {
data class TitleColor(val color: Int) : ConfigUpdate { data class TitleColor(val color: Int) : ConfigUpdate {
override val actions = setOf(ConfigUpdateAction.UpdateStyle, ConfigUpdateAction.ReloadContent, ConfigUpdateAction.InvalidateTextPage) override val actions = setOf(ConfigUpdateAction.UpdateStyle, ConfigUpdateAction.ReloadContent, ConfigUpdateAction.InvalidateTextPage)
} }
data class TitleFont(val path: String) : ConfigUpdate {
override val actions = setOf(ConfigUpdateAction.UpdateChapterStyle, ConfigUpdateAction.ReloadContent)
}
data class TitleSegType(val value: Int) : ConfigUpdate {
override val actions = setOf(ConfigUpdateAction.UpdateChapterStyle, ConfigUpdateAction.ReloadContent)
}
data class TitleSegDistance(val value: Int) : ConfigUpdate {
override val actions = setOf(ConfigUpdateAction.UpdateChapterStyle, ConfigUpdateAction.ReloadContent)
}
data class TitleSegFlag(val value: String) : ConfigUpdate {
override val actions = setOf(ConfigUpdateAction.UpdateChapterStyle, ConfigUpdateAction.ReloadContent)
}
// --- Header / footer tips --- // --- Header / footer tips ---
data class HeaderMode(val value: Int) : ConfigUpdate { data class HeaderMode(val value: Int) : ConfigUpdate {
@@ -1023,6 +1040,12 @@ sealed interface ConfigUpdate {
data class MenuBlurAlpha(val value: Int) : ConfigUpdate { data class MenuBlurAlpha(val value: Int) : ConfigUpdate {
override val actions = emptySet<ConfigUpdateAction>() override val actions = emptySet<ConfigUpdateAction>()
} }
data class MenuBlurColor(val color: Int) : ConfigUpdate {
override val actions = emptySet<ConfigUpdateAction>()
}
data class MenuPaletteStyle(val value: String) : ConfigUpdate {
override val actions = emptySet<ConfigUpdateAction>()
}
data class MenuLensRadius(val value: Float) : ConfigUpdate { data class MenuLensRadius(val value: Float) : ConfigUpdate {
override val actions = emptySet<ConfigUpdateAction>() override val actions = emptySet<ConfigUpdateAction>()
} }
@@ -195,7 +195,7 @@ fun ReadBookMenuBar(
} }
} }
val hideTopBar = dialogLikeRoute || val hideTopBar = dialogLikeRoute ||
currentRoute == ReadBookMenuRoute.ReadStyle && readStylePage >= 1 currentRoute == ReadBookMenuRoute.TextTitle
val menuColors = readMenuColors() val menuColors = readMenuColors()
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
@@ -683,6 +683,9 @@ private fun ReadBookMenuSurface(
onOpenFontSelect = { onOpenFontSelect = {
onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.FontSelect)) onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.FontSelect))
}, },
onOpenTitleFontSelect = {
onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TitleFontSelect))
},
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
onIntent = onIntent, onIntent = onIntent,
) )
@@ -902,55 +905,61 @@ private fun MenuTitleBar(
) )
) { ) {
val useTitleCapsule = readMenuTopBarTitleCapsuleEnabled(backdrop, state.menuConfig) val useTitleCapsule = readMenuTopBarTitleCapsuleEnabled(backdrop, state.menuConfig)
&& progressiveBlurActive
val capsuleIconColor = LegadoTheme.colorScheme.onSurfaceVariant
// Title row: back + title + actions + overflow // Title row: left group (back + capsule/title) + right group (actions)
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp), .padding(horizontal = 16.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
// Back button // Left group: back + capsule/title — fills remaining space
MenuTitleGlassButton( Row(
onClick = { onIntent(ReadBookIntent.CloseReadBook) }, modifier = Modifier.weight(1f),
icon = Icons.AutoMirrored.Filled.ArrowBack, verticalAlignment = Alignment.CenterVertically,
contentDescription = "Back", ) {
state = state, MenuTitleGlassButton(
colors = colors, onClick = { onIntent(ReadBookIntent.CloseReadBook) },
backdrop = backdrop, icon = Icons.AutoMirrored.Filled.ArrowBack,
) contentDescription = "Back",
if (useTitleCapsule) {
// Title capsule layout: title + chapter in a glass pill
TitleCapsuleGlassLayout(
state = state, state = state,
colors = colors, colors = colors,
onIntent = onIntent,
backdrop = backdrop, backdrop = backdrop,
titleTextColor = titleTextColor,
) )
} else if (titleBarMode != "1" && titleBarMode != "3") {
AppText( if (useTitleCapsule) {
text = state.bookName, TitleCapsuleGlassLayout(
modifier = Modifier state = state,
.weight(1f) colors = colors,
.clickable { onIntent(ReadBookIntent.OpenBookInfo) } onIntent = onIntent,
.padding(horizontal = 8.dp, vertical = 4.dp), backdrop = backdrop,
style = LegadoTheme.typography.titleMedium.copy( titleTextColor = capsuleIconColor,
shadow = androidx.compose.ui.graphics.Shadow( )
color = Color.Black.copy(alpha = 0.12f), } else if (titleBarMode != "1" && titleBarMode != "3") {
offset = Offset.Zero, AppText(
blurRadius = 12f text = state.bookName,
) modifier = Modifier
), .weight(1f)
color = titleTextColor, .clickable { onIntent(ReadBookIntent.OpenBookInfo) }
maxLines = 2, .padding(horizontal = 8.dp, vertical = 4.dp),
overflow = TextOverflow.Ellipsis, style = LegadoTheme.typography.titleMedium.copy(
) shadow = androidx.compose.ui.graphics.Shadow(
} else { color = Color.Black.copy(alpha = 0.12f),
Spacer(Modifier.weight(1f)) offset = Offset.Zero,
blurRadius = 12f
)
),
color = titleTextColor,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
} }
// Right group: actions
if (readMenuTopBarButtonLiquidGlassEnabled(backdrop, state.menuConfig)) { if (readMenuTopBarButtonLiquidGlassEnabled(backdrop, state.menuConfig)) {
MenuTitleBarMergedGlassButton( MenuTitleBarMergedGlassButton(
state = state, state = state,
@@ -963,7 +972,6 @@ private fun MenuTitleBar(
horizontalArrangement = Arrangement.spacedBy(6.dp), horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
// Source action button (non-local books only)
if (!state.isLocalBook) { if (!state.isLocalBook) {
SourceActionButton( SourceActionButton(
state = state, state = state,
@@ -1252,7 +1260,7 @@ private fun RowScope.TitleCapsuleGlassLayout(
Row( Row(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(horizontal = 8.dp) .padding(start = 12.dp)
.height(40.dp) .height(40.dp)
.readMenuLiquidGlass( .readMenuLiquidGlass(
backdrop = backdrop, backdrop = backdrop,
@@ -1320,24 +1328,20 @@ private fun MenuTitleBarMergedGlassButton(
val tint = LegadoTheme.colorScheme.onSurfaceVariant val tint = LegadoTheme.colorScheme.onSurfaceVariant
Box { Box {
Box( Row(
contentAlignment = Alignment.Center, modifier = Modifier
modifier = Modifier.size(48.dp), .height(40.dp)
) { .readMenuLiquidGlass(
Row( backdrop = backdrop,
modifier = Modifier colors = colors,
.height(40.dp) shape = pillShape,
.readMenuLiquidGlass( useTopBarStyle = true,
backdrop = backdrop, useLens = true,
colors = colors, blurRadius = 32.dp,
shape = pillShape, interactive = true,
useTopBarStyle = true, menuConfig = state.menuConfig,
useLens = true, )
blurRadius = 32.dp, .padding(horizontal = 4.dp),
interactive = true,
menuConfig = state.menuConfig,
)
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
// SwapHoriz - change source // SwapHoriz - change source
@@ -1418,7 +1422,6 @@ private fun MenuTitleBarMergedGlassButton(
) )
} }
} }
}
// Dropdown menus // Dropdown menus
if (!state.isLocalBook) { if (!state.isLocalBook) {
@@ -2827,8 +2830,9 @@ private fun Modifier.readMenuHazeEffect(
progressiveBottomToTop: Boolean = false, progressiveBottomToTop: Boolean = false,
): Modifier { ): Modifier {
val surfaceAlpha = menuConfig.readMenuBlurAlpha.coerceIn(0, 100) / 100f val surfaceAlpha = menuConfig.readMenuBlurAlpha.coerceIn(0, 100) / 100f
val blurTintColor = menuConfig.readMenuBlurColor.takeIf { it != 0 }?.let { Color(it) }
val hazeContainerColor = if (progressive) { val hazeContainerColor = if (progressive) {
Color.Black.copy(alpha = surfaceAlpha) (blurTintColor ?: Color.Black).copy(alpha = surfaceAlpha)
} else { } else {
colors.background.copy(alpha = surfaceAlpha) colors.background.copy(alpha = surfaceAlpha)
} }
@@ -174,6 +174,17 @@ fun ReadBookScreen(
onOpenFolderPicker = { onIntent(ReadBookIntent.OpenFontFolderPicker) }, onOpenFolderPicker = { onIntent(ReadBookIntent.OpenFontFolderPicker) },
systemTypefaces = fontSelectSystemTypefaces, systemTypefaces = fontSelectSystemTypefaces,
) )
FontSelectSheet(
show = state.activeSheet is ReadBookSheet.TitleFontSelect,
title = stringResource(R.string.read_config_title_settings),
fontFolderUri = fontSelectFolderUri,
selectedFontPath = ReadBookConfig.titleFont,
onDismissRequest = dismissSheet,
onSelectFont = { onIntent(ReadBookIntent.SelectTitleFont(it.uri.toString())) },
onSelectSystemTypeface = { onIntent(ReadBookIntent.SelectTitleSystemTypeface(it)) },
onOpenFolderPicker = { onIntent(ReadBookIntent.OpenFontFolderPicker) },
systemTypefaces = fontSelectSystemTypefaces,
)
ToolButtonConfigSheet( ToolButtonConfigSheet(
show = state.activeSheet is ReadBookSheet.ToolButtonConfig, show = state.activeSheet is ReadBookSheet.ToolButtonConfig,
items = state.menuConfig.bottomBarButtons, items = state.menuConfig.bottomBarButtons,
@@ -896,6 +896,14 @@ class ReadBookViewModel(
} }
is ReadBookIntent.SelectFont -> selectFont(intent.path) is ReadBookIntent.SelectFont -> selectFont(intent.path)
is ReadBookIntent.SelectTitleFont -> selectTitleFont(intent.path)
is ReadBookIntent.SelectTitleSystemTypeface -> {
ReadConfig.systemTypefaces = intent.index
ReadBookConfig.titleFont = ""
_effects.tryEmit(ReadBookEffect.UpdateReadViewConfig(
setOf(ConfigUpdateAction.UpdateStyle, ConfigUpdateAction.ReloadContent)
))
}
is ReadBookIntent.SelectSystemTypeface -> { is ReadBookIntent.SelectSystemTypeface -> {
ReadConfig.systemTypefaces = intent.index ReadConfig.systemTypefaces = intent.index
ReadBookConfig.textFont = "" ReadBookConfig.textFont = ""
@@ -1448,6 +1456,7 @@ class ReadBookViewModel(
val old = previous val old = previous
previous = preferences previous = preferences
ReadConfig.syncReadPreferences(preferences) ReadConfig.syncReadPreferences(preferences)
ReadBookConfig.readMenuPaletteStyle = preferences.readMenuPaletteStyle
_readPreferences.value = preferences _readPreferences.value = preferences
if (!preferences.hasMenuClickArea()) { if (!preferences.hasMenuClickArea()) {
ReadConfig.detectClickArea() ReadConfig.detectClickArea()
@@ -1599,6 +1608,8 @@ class ReadBookViewModel(
readMenuBorderColor = ReadBookConfig.readMenuBorderColor, readMenuBorderColor = ReadBookConfig.readMenuBorderColor,
readMenuBorderColorNight = ReadBookConfig.readMenuBorderColorNight, readMenuBorderColorNight = ReadBookConfig.readMenuBorderColorNight,
readMenuBlurAlpha = ReadBookConfig.readMenuBlurAlpha, readMenuBlurAlpha = ReadBookConfig.readMenuBlurAlpha,
readMenuBlurColor = ReadBookConfig.readMenuBlurColor,
readMenuPaletteStyle = ReadBookConfig.readMenuPaletteStyle,
readMenuBlurRadius = ReadBookConfig.readMenuBlurRadius, readMenuBlurRadius = ReadBookConfig.readMenuBlurRadius,
readMenuLensRadius = ReadBookConfig.readMenuLensRadius, readMenuLensRadius = ReadBookConfig.readMenuLensRadius,
readMenuTopBarBlurMode = ReadBookConfig.readMenuTopBarBlurMode, readMenuTopBarBlurMode = ReadBookConfig.readMenuTopBarBlurMode,
@@ -2507,6 +2518,10 @@ class ReadBookViewModel(
is ConfigUpdate.TitleTopSpacing -> ReadBookConfig.titleTopSpacing = update.value is ConfigUpdate.TitleTopSpacing -> ReadBookConfig.titleTopSpacing = update.value
is ConfigUpdate.TitleBottomSpacing -> ReadBookConfig.titleBottomSpacing = update.value is ConfigUpdate.TitleBottomSpacing -> ReadBookConfig.titleBottomSpacing = update.value
is ConfigUpdate.TitleColor -> ReadBookConfig.titleColor = update.color is ConfigUpdate.TitleColor -> ReadBookConfig.titleColor = update.color
is ConfigUpdate.TitleFont -> ReadBookConfig.titleFont = update.path
is ConfigUpdate.TitleSegType -> ReadBookConfig.titleSegType = update.value
is ConfigUpdate.TitleSegDistance -> ReadBookConfig.titleSegDistance = update.value
is ConfigUpdate.TitleSegFlag -> ReadBookConfig.titleSegFlag = update.value
// --- Header / footer tips --- // --- Header / footer tips ---
is ConfigUpdate.HeaderMode -> ReadBookConfig.headerMode = update.value is ConfigUpdate.HeaderMode -> ReadBookConfig.headerMode = update.value
@@ -2829,6 +2844,20 @@ class ReadBookViewModel(
} }
_uiState.update { it.copy(menuConfig = it.menuConfig.copy(readMenuBlurAlpha = update.value)) } _uiState.update { it.copy(menuConfig = it.menuConfig.copy(readMenuBlurAlpha = update.value)) }
} }
is ConfigUpdate.MenuBlurColor -> {
ReadBookConfig.readMenuBlurColor = update.color
viewModelScope.launch {
readSettingsRepository.setReadMenuBlurColor(update.color)
}
_uiState.update { it.copy(menuConfig = it.menuConfig.copy(readMenuBlurColor = update.color)) }
}
is ConfigUpdate.MenuPaletteStyle -> {
ReadBookConfig.readMenuPaletteStyle = update.value
viewModelScope.launch {
readSettingsRepository.setReadMenuPaletteStyle(update.value)
}
_uiState.update { it.copy(menuConfig = it.menuConfig.copy(readMenuPaletteStyle = update.value)) }
}
is ConfigUpdate.MenuLensRadius -> { is ConfigUpdate.MenuLensRadius -> {
ReadBookConfig.readMenuLensRadius = update.value ReadBookConfig.readMenuLensRadius = update.value
viewModelScope.launch { viewModelScope.launch {
@@ -3207,6 +3236,13 @@ class ReadBookViewModel(
)) ))
} }
private fun selectTitleFont(path: String) {
ReadBookConfig.titleFont = path
_effects.tryEmit(ReadBookEffect.UpdateReadViewConfig(
setOf(ConfigUpdateAction.UpdateChapterStyle, ConfigUpdateAction.ReloadContent, ConfigUpdateAction.UpdateStyle)
))
}
private fun toggleDayNight() { private fun toggleDayNight() {
val nextMode = if (ReadConfig.isNightTheme) "1" else "2" val nextMode = if (ReadConfig.isNightTheme) "1" else "2"
ThemeConfig.themeMode = nextMode ThemeConfig.themeMode = nextMode
@@ -26,7 +26,11 @@ import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.SkipNext import androidx.compose.material.icons.filled.SkipNext
import androidx.compose.material.icons.filled.SkipPrevious import androidx.compose.material.icons.filled.SkipPrevious
import androidx.compose.material.icons.filled.Translate import androidx.compose.material.icons.filled.Translate
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@@ -40,13 +44,16 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -60,9 +67,11 @@ import io.legado.app.ui.book.read.ConfigUpdate
import io.legado.app.ui.book.read.ReadBookButtonConfigItem import io.legado.app.ui.book.read.ReadBookButtonConfigItem
import io.legado.app.ui.book.read.ReadBookIntent import io.legado.app.ui.book.read.ReadBookIntent
import io.legado.app.ui.book.read.ReadBookSheet import io.legado.app.ui.book.read.ReadBookSheet
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.SectionTitle import io.legado.app.ui.widget.components.SectionTitle
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
import io.legado.app.ui.widget.components.settingItem.TinyClearColorModeSettingItem import io.legado.app.ui.widget.components.settingItem.TinyClearColorModeSettingItem
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem
import io.legado.app.ui.widget.components.settingItem.TinyColorModeSettingItem import io.legado.app.ui.widget.components.settingItem.TinyColorModeSettingItem
import io.legado.app.ui.widget.components.settingItem.TinyDropdownSettingItem import io.legado.app.ui.widget.components.settingItem.TinyDropdownSettingItem
@@ -82,6 +91,7 @@ private const val COLOR_MENU_ACCENT_NIGHT = 9
private const val COLOR_MENU_CONTAINER_NIGHT = 10 private const val COLOR_MENU_CONTAINER_NIGHT = 10
private const val COLOR_BORDER = 11 private const val COLOR_BORDER = 11
private const val COLOR_BORDER_NIGHT = 12 private const val COLOR_BORDER_NIGHT = 12
private const val COLOR_BLUR_TINT = 13
@Composable @Composable
internal fun SystemMenuPage( internal fun SystemMenuPage(
@@ -196,6 +206,7 @@ internal fun SystemMenuPage(
COLOR_MENU_CONTAINER_NIGHT -> onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.MenuContainerColorNight(color))) COLOR_MENU_CONTAINER_NIGHT -> onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.MenuContainerColorNight(color)))
COLOR_BORDER -> onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.BorderColor(color))) COLOR_BORDER -> onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.BorderColor(color)))
COLOR_BORDER_NIGHT -> onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.BorderColorNight(color))) COLOR_BORDER_NIGHT -> onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.BorderColorNight(color)))
COLOR_BLUR_TINT -> onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.MenuBlurColor(color)))
} }
showColorPicker = false showColorPicker = false
}, },
@@ -264,6 +275,23 @@ private fun GlobalMenuTab(
}, },
) )
val showPaletteStyle = bottomMode == 1 || (bottomMode == 2 && colorMode == 0)
AnimatedVisibility(visible = showPaletteStyle) {
val paletteEntries = stringArrayResource(R.array.paletteStyle)
val paletteValues = stringArrayResource(R.array.paletteStyle_value)
val allEntries = arrayOf(stringResource(R.string.flow_sys)) + paletteEntries
val allValues = arrayOf("") + paletteValues
TinyDropdownSettingItem(
title = stringResource(R.string.palette_style),
selectedValue = preferences.readMenuPaletteStyle,
displayEntries = allEntries,
entryValues = allValues,
onValueChange = {
onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.MenuPaletteStyle(it)))
},
)
}
AnimatedVisibility(visible = bottomMode == 2) { AnimatedVisibility(visible = bottomMode == 2) {
Column { Column {
TinyDropdownSettingItem( TinyDropdownSettingItem(
@@ -461,6 +489,18 @@ private fun GlobalMenuTab(
onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.MenuBlurAlpha(it.toInt()))) onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.MenuBlurAlpha(it.toInt())))
}, },
) )
TinyClickableSettingItem(
title = stringResource(R.string.read_menu_blur_color),
onClick = {
onShowColorPicker(COLOR_BLUR_TINT, preferences.readMenuBlurColor)
},
onLongClick = {
onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.MenuBlurColor(0)))
},
trailingContent = {
TinyColorSwatch(color = preferences.readMenuBlurColor)
},
)
} }
} }
@@ -794,3 +834,33 @@ internal fun readMenuButtonInfos(context: Context): List<ReadMenuButtonInfo> = l
ReadMenuButtonInfo("replace_badge", Icons.Default.AutoAwesome, context.getString(R.string.replace_purify_badge)), ReadMenuButtonInfo("replace_badge", Icons.Default.AutoAwesome, context.getString(R.string.replace_purify_badge)),
ReadMenuButtonInfo("translate", Icons.Default.Translate, context.getString(R.string.translate)), ReadMenuButtonInfo("translate", Icons.Default.Translate, context.getString(R.string.translate)),
) )
// ========== Color Swatch ==========
@Composable
private fun TinyColorSwatch(
color: Int,
) {
val swatchColor = if (color != 0) Color(color) else LegadoTheme.colorScheme.onSurfaceVariant
val borderColor = LegadoTheme.colorScheme.outline
Box(
modifier = Modifier
.size(28.dp)
.clip(CircleShape)
.background(swatchColor)
.then(
if (color == 0) Modifier.border(1.5.dp, borderColor, CircleShape)
else Modifier
),
contentAlignment = Alignment.Center,
) {
if (color == 0) {
AppText(
text = "A",
style = LegadoTheme.typography.labelSmall,
color = LegadoTheme.colorScheme.surfaceContainerLow,
textAlign = TextAlign.Center,
)
}
}
}
@@ -60,6 +60,7 @@ fun ReadStyleTextTitleContent(
onOpenUnderlineConfig: () -> Unit, onOpenUnderlineConfig: () -> Unit,
onOpenHighlightRule: () -> Unit, onOpenHighlightRule: () -> Unit,
onOpenFontSelect: () -> Unit, onOpenFontSelect: () -> Unit,
onOpenTitleFontSelect: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onIntent: (ReadBookIntent) -> Unit, onIntent: (ReadBookIntent) -> Unit,
) { ) {
@@ -85,6 +86,7 @@ fun ReadStyleTextTitleContent(
onOpenUnderlineConfig = onOpenUnderlineConfig, onOpenUnderlineConfig = onOpenUnderlineConfig,
onOpenHighlightRule = onOpenHighlightRule, onOpenHighlightRule = onOpenHighlightRule,
onOpenFontSelect = onOpenFontSelect, onOpenFontSelect = onOpenFontSelect,
onOpenTitleFontSelect = onOpenTitleFontSelect,
animateToPage = { page -> scope.launch { pagerState.animateScrollToPage(page) } }, animateToPage = { page -> scope.launch { pagerState.animateScrollToPage(page) } },
modifier = modifier, modifier = modifier,
onIntent = onIntent, onIntent = onIntent,
@@ -101,6 +103,7 @@ internal fun ReadStyleTextTitleContent(
onOpenUnderlineConfig: () -> Unit, onOpenUnderlineConfig: () -> Unit,
onOpenHighlightRule: () -> Unit, onOpenHighlightRule: () -> Unit,
onOpenFontSelect: () -> Unit, onOpenFontSelect: () -> Unit,
onOpenTitleFontSelect: () -> Unit,
animateToPage: (Int) -> Unit, animateToPage: (Int) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onIntent: (ReadBookIntent) -> Unit, onIntent: (ReadBookIntent) -> Unit,
@@ -132,7 +135,10 @@ internal fun ReadStyleTextTitleContent(
) )
1 -> LayoutSpacingPage(onIntent = onIntent) 1 -> LayoutSpacingPage(onIntent = onIntent)
2 -> TitleSettingsPage(onIntent = onIntent) 2 -> TitleSettingsPage(
onOpenTitleFontSelect = onOpenTitleFontSelect,
onIntent = onIntent,
)
} }
} }
} }
@@ -358,10 +364,14 @@ internal fun TextEffectsPage(
@Composable @Composable
internal fun TitleSettingsPage( internal fun TitleSettingsPage(
onOpenTitleFontSelect: () -> Unit,
onIntent: (ReadBookIntent) -> Unit, onIntent: (ReadBookIntent) -> Unit,
) { ) {
var titleMode by remember(ReadBookConfig.titleMode) { mutableIntStateOf(ReadBookConfig.titleMode) } var titleMode by remember(ReadBookConfig.titleMode) { mutableIntStateOf(ReadBookConfig.titleMode) }
var titleBold by remember(ReadBookConfig.titleBold) { mutableIntStateOf(ReadBookConfig.titleBold) } var titleBold by remember(ReadBookConfig.titleBold) { mutableIntStateOf(ReadBookConfig.titleBold) }
var titleSegType by remember(ReadBookConfig.titleSegType) { mutableIntStateOf(ReadBookConfig.titleSegType) }
var titleSegDistance by remember(ReadBookConfig.titleSegDistance) { mutableIntStateOf(ReadBookConfig.titleSegDistance) }
var titleSegFlag by remember(ReadBookConfig.titleSegFlag) { mutableStateOf(ReadBookConfig.titleSegFlag) }
var titleSegScaling by remember(ReadBookConfig.titleSegScaling) { mutableFloatStateOf(ReadBookConfig.titleSegScaling) } var titleSegScaling by remember(ReadBookConfig.titleSegScaling) { mutableFloatStateOf(ReadBookConfig.titleSegScaling) }
var titleLineSpacingExtra by remember(ReadBookConfig.titleLineSpacingExtra) { mutableIntStateOf(ReadBookConfig.titleLineSpacingExtra) } var titleLineSpacingExtra by remember(ReadBookConfig.titleLineSpacingExtra) { mutableIntStateOf(ReadBookConfig.titleLineSpacingExtra) }
var titleLineSpacingSub by remember(ReadBookConfig.titleLineSpacingSub) { mutableIntStateOf(ReadBookConfig.titleLineSpacingSub) } var titleLineSpacingSub by remember(ReadBookConfig.titleLineSpacingSub) { mutableIntStateOf(ReadBookConfig.titleLineSpacingSub) }
@@ -432,6 +442,85 @@ internal fun TitleSettingsPage(
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
// Title font
TinyClickableSettingItem(
title = stringResource(R.string.read_config_title_settings),
imageVector = Icons.Default.TextFields,
onClick = onOpenTitleFontSelect,
)
Spacer(Modifier.height(8.dp))
// Title segmentation
TinyDropdownSettingItem(
title = stringResource(R.string.split_title_mode),
selectedValue = titleSegType.toString(),
displayEntries = arrayOf(
stringResource(R.string.close),
stringResource(R.string.split_title_by_position),
stringResource(R.string.split_title_by_flag),
stringResource(R.string.split_title_by_regex),
),
entryValues = arrayOf("0", "1", "2", "3"),
onValueChange = {
titleSegType = it.toInt()
onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleSegType(titleSegType)))
},
)
if (titleSegType == 1) {
TinySliderSettingItem(
title = stringResource(R.string.split_title_position),
value = titleSegDistance.toFloat(),
valueRange = 1f..20f,
steps = 18,
onValueChange = { value ->
titleSegDistance = value.toInt()
onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleSegDistance(titleSegDistance)))
},
)
}
if (titleSegType == 2 || titleSegType == 3) {
var showFlagDialog by remember { mutableStateOf(false) }
TinyClickableSettingItem(
title = stringResource(R.string.rule_segment),
description = titleSegFlag.ifBlank { stringResource(R.string.split_title_mode) },
onClick = { showFlagDialog = true },
)
if (showFlagDialog) {
var flagText by remember { mutableStateOf(titleSegFlag) }
androidx.compose.material3.AlertDialog(
onDismissRequest = { showFlagDialog = false },
title = { Text(stringResource(R.string.rule_segment)) },
text = {
androidx.compose.material3.OutlinedTextField(
value = flagText,
onValueChange = { flagText = it },
singleLine = false,
modifier = Modifier.fillMaxWidth(),
)
},
confirmButton = {
androidx.compose.material3.TextButton(onClick = {
titleSegFlag = flagText
onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleSegFlag(titleSegFlag)))
showFlagDialog = false
}) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
androidx.compose.material3.TextButton(onClick = { showFlagDialog = false }) {
Text(stringResource(android.R.string.cancel))
}
},
)
}
}
Spacer(Modifier.height(8.dp))
// Title spacing sliders // Title spacing sliders
TinySliderSettingItem( TinySliderSettingItem(
title = stringResource(R.string.subtitle_scale), title = stringResource(R.string.subtitle_scale),
@@ -104,7 +104,7 @@ fun ScopeSelectSheet(
applyDraftSelection() applyDraftSelection()
onDismissRequest() onDismissRequest()
}, },
text = stringResource(R.string.confirm), icon = AppIcons.Check,
) )
} }
}, },
@@ -92,6 +92,7 @@ fun ColorScheme.toLegadoColorScheme(): LegadoColorScheme {
fun ProvideColorSchemeOverride( fun ProvideColorSchemeOverride(
colorScheme: ColorScheme, colorScheme: ColorScheme,
seedColor: Color = colorScheme.primary, seedColor: Color = colorScheme.primary,
overrideIsDark: Boolean? = null,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val themeAnimationSpec = tween<Color>( val themeAnimationSpec = tween<Color>(
@@ -106,10 +107,12 @@ fun ProvideColorSchemeOverride(
label = "theme_seed_animation" label = "theme_seed_animation"
).value ).value
val legadoColorScheme = remember(animatedColorScheme) { animatedColorScheme.toLegadoColorScheme() } val legadoColorScheme = remember(animatedColorScheme) { animatedColorScheme.toLegadoColorScheme() }
val overrideThemeMode = remember(baseThemeMode, animatedColorScheme, animatedSeedColor) { val resolvedIsDark = overrideIsDark ?: baseThemeMode.isDark
val overrideThemeMode = remember(baseThemeMode, animatedColorScheme, animatedSeedColor, resolvedIsDark) {
baseThemeMode.copy( baseThemeMode.copy(
colorScheme = animatedColorScheme, colorScheme = animatedColorScheme,
seedColor = animatedSeedColor, seedColor = animatedSeedColor,
isDark = resolvedIsDark,
) )
} }
val materialTypography = MaterialTheme.typography val materialTypography = MaterialTheme.typography
@@ -16,6 +16,7 @@ import io.legado.app.ui.config.themeConfig.ThemeConfig
data class ThemeOverrideState( data class ThemeOverrideState(
val seedColor: Color, val seedColor: Color,
val colorScheme: ColorScheme, val colorScheme: ColorScheme,
val isDark: Boolean = false,
) )
fun buildThemeOverrideState( fun buildThemeOverrideState(
@@ -45,7 +46,8 @@ fun buildThemeOverrideState(
return ThemeOverrideState( return ThemeOverrideState(
seedColor = seedColor, seedColor = seedColor,
colorScheme = colorScheme colorScheme = colorScheme,
isDark = isDark,
) )
} }
@@ -72,6 +74,7 @@ fun ProvideThemeOverride(
ProvideColorSchemeOverride( ProvideColorSchemeOverride(
colorScheme = currentTheme.colorScheme, colorScheme = currentTheme.colorScheme,
seedColor = currentTheme.seedColor, seedColor = currentTheme.seedColor,
overrideIsDark = currentTheme.isDark,
content = content content = content
) )
} else { } else {
@@ -438,8 +438,8 @@ fun ChangeSourceSheet(
selectedSources = scopeState.sourceUrls, selectedSources = scopeState.sourceUrls,
onToggleSource = { viewModel.toggleScopeSource(it) }, onToggleSource = { viewModel.toggleScopeSource(it) },
isSourceScope = scopeState.isSource, isSourceScope = scopeState.isSource,
onConfirm = { onApplyScope = { selection ->
viewModel.startSearch() viewModel.applyScopeSelection(selection)
showFilterSheet = false showFilterSheet = false
} }
) )
@@ -5,6 +5,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.LibraryBooks import androidx.compose.material.icons.automirrored.filled.LibraryBooks
import androidx.compose.material.icons.automirrored.outlined.LibraryBooks import androidx.compose.material.icons.automirrored.outlined.LibraryBooks
import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
@@ -111,6 +112,10 @@ object AppIcons {
@Composable @Composable
get() = if (isMiuix) MiuixIcons.MoreCircle else Icons.Default.MoreHoriz get() = if (isMiuix) MiuixIcons.MoreCircle else Icons.Default.MoreHoriz
val Check: ImageVector
@Composable
get() = Icons.Default.Check
@Composable @Composable
fun mainDestination(destination: MainDestination, selected: Boolean): ImageVector { fun mainDestination(destination: MainDestination, selected: Boolean): ImageVector {
@@ -65,6 +65,7 @@ fun RoundDropdownMenu(
} }
} else { } else {
val colorScheme = rememberOpaqueColorScheme() val colorScheme = rememberOpaqueColorScheme()
val popupContainerColor = LegadoTheme.colorScheme.surfaceContainerLow
DropdownMenu( DropdownMenu(
expanded = expanded, expanded = expanded,
@@ -72,7 +73,7 @@ fun RoundDropdownMenu(
modifier = modifier, modifier = modifier,
shape = shape, shape = shape,
shadowElevation = shadowElevation, shadowElevation = shadowElevation,
containerColor = colorScheme.surfaceContainerLow containerColor = popupContainerColor
) { ) {
MaterialExpressiveTheme( MaterialExpressiveTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
@@ -129,6 +130,7 @@ fun RoundDropdownMenuLazy(
} }
} else { } else {
val colorScheme = rememberOpaqueColorScheme() val colorScheme = rememberOpaqueColorScheme()
val popupContainerColor = LegadoTheme.colorScheme.surfaceContainerLow
DropdownMenu( DropdownMenu(
expanded = expanded, expanded = expanded,
@@ -136,7 +138,7 @@ fun RoundDropdownMenuLazy(
modifier = modifier, modifier = modifier,
shape = shape, shape = shape,
shadowElevation = shadowElevation, shadowElevation = shadowElevation,
containerColor = colorScheme.surfaceContainerLow containerColor = popupContainerColor
) { ) {
MaterialExpressiveTheme( MaterialExpressiveTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
@@ -35,7 +35,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.theme.rememberOpaqueColorScheme
import io.legado.app.ui.widget.components.icon.AppIcon import io.legado.app.ui.widget.components.icon.AppIcon
import top.yukonga.miuix.kmp.basic.DropdownDefaults import top.yukonga.miuix.kmp.basic.DropdownDefaults
import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.MiuixIcons
@@ -121,9 +120,9 @@ fun RoundDropdownMenuItem(
} }
} }
} else { } else {
val colorScheme = rememberOpaqueColorScheme() val legadoColorScheme = LegadoTheme.colorScheme
val selectedContentColor = colorScheme.primary val selectedContentColor = legadoColorScheme.primary
val defaultContentColor = colorScheme.onSurface val defaultContentColor = legadoColorScheme.onSurface
val contentColor = if (enabled) { val contentColor = if (enabled) {
when { when {
hasCustomContentColor -> color hasCustomContentColor -> color
@@ -137,7 +136,7 @@ fun RoundDropdownMenuItem(
else -> defaultContentColor.copy(alpha = 0.38f) else -> defaultContentColor.copy(alpha = 0.38f)
} }
} }
val containerColor = colorScheme.surface val containerColor = legadoColorScheme.surface
Surface( Surface(
onClick = onClick, onClick = onClick,
@@ -1711,6 +1711,10 @@
<string name="tool_bar_style">工具栏样式</string> <string name="tool_bar_style">工具栏样式</string>
<string name="split_title">标题分段</string> <string name="split_title">标题分段</string>
<string name="split_title_mode">分段模式</string> <string name="split_title_mode">分段模式</string>
<string name="split_title_by_position">按位置分段</string>
<string name="split_title_by_flag">按标记分段</string>
<string name="split_title_by_regex">正则分段</string>
<string name="split_title_position">分段位置</string>
<string name="subtitle_scale">次行缩放</string> <string name="subtitle_scale">次行缩放</string>
<string name="subtitle_margin">次行距离</string> <string name="subtitle_margin">次行距离</string>
<string name="heading_spacing">标题段距</string> <string name="heading_spacing">标题段距</string>
@@ -1903,6 +1907,7 @@
<string name="read_menu_blur_radius">模糊半径</string> <string name="read_menu_blur_radius">模糊半径</string>
<string name="read_menu_blur_radius_summary">模糊半径越大,系统运行越卡顿</string> <string name="read_menu_blur_radius_summary">模糊半径越大,系统运行越卡顿</string>
<string name="read_menu_blur_alpha">菜单不透明度</string> <string name="read_menu_blur_alpha">菜单不透明度</string>
<string name="read_menu_blur_color">模糊着色</string>
<string name="read_menu_border">边框</string> <string name="read_menu_border">边框</string>
<string name="read_menu_border_width">宽度</string> <string name="read_menu_border_width">宽度</string>
<string name="read_menu_border_color">颜色</string> <string name="read_menu_border_color">颜色</string>
+5
View File
@@ -1748,6 +1748,10 @@
<string name="rule_segment">Rule</string> <string name="rule_segment">Rule</string>
<string name="split_title">Split title</string> <string name="split_title">Split title</string>
<string name="split_title_mode">Split mode</string> <string name="split_title_mode">Split mode</string>
<string name="split_title_by_position">By position</string>
<string name="split_title_by_flag">By flag</string>
<string name="split_title_by_regex">By regex</string>
<string name="split_title_position">Split position</string>
<string name="subtitle_margin">Subtitle margin</string> <string name="subtitle_margin">Subtitle margin</string>
<string name="subtitle_scale">Subtitle scale</string> <string name="subtitle_scale">Subtitle scale</string>
<string name="tool_bar_style">Tool bar style</string> <string name="tool_bar_style">Tool bar style</string>
@@ -1912,6 +1916,7 @@
<string name="read_menu_blur_radius">Blur radius</string> <string name="read_menu_blur_radius">Blur radius</string>
<string name="read_menu_blur_radius_summary">Higher radius may reduce performance</string> <string name="read_menu_blur_radius_summary">Higher radius may reduce performance</string>
<string name="read_menu_blur_alpha">Menu opacity</string> <string name="read_menu_blur_alpha">Menu opacity</string>
<string name="read_menu_blur_color">Blur tint color</string>
<string name="read_menu_border">Border</string> <string name="read_menu_border">Border</string>
<string name="read_menu_border_width">Width</string> <string name="read_menu_border_width">Width</string>
<string name="read_menu_border_color">Color</string> <string name="read_menu_border_color">Color</string>