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

This commit is contained in:
HapeLee
2026-06-15 01:33:28 +08:00
parent 5a587222eb
commit 9bb8571cc7
21 changed files with 448 additions and 90 deletions
@@ -308,6 +308,8 @@ object PreferKey {
const val readMenuBottomBarBlurStyle = "readMenuBottomBarBlurStyle"
const val readMenuBlurRadius = "readMenuBlurRadius"
const val readMenuBlurAlpha = "readMenuBlurAlpha"
const val readMenuBlurColor = "readMenuBlurColor"
const val readMenuPaletteStyle = "readMenuPaletteStyle"
const val readMenuLensRadius = "readMenuLensRadius"
const val readMenuBorderWidth = "readMenuBorderWidth"
const val readMenuBorderColor = "readMenuBorderColor"
@@ -94,6 +94,8 @@ data class ReadPreferences(
val readMenuBottomBarBlurStyle: Int = ReadMenuBlurStyle.Solid,
val readMenuBlurRadius: Int = 24,
val readMenuBlurAlpha: Int = 60,
val readMenuBlurColor: Int = 0,
val readMenuPaletteStyle: String = "",
val readMenuLensRadius: Float = 24f,
val readMenuBorderWidth: Int = 0,
val readMenuBorderColor: Int = 0,
@@ -335,6 +337,12 @@ class ReadSettingsRepository(
suspend fun setReadMenuBlurAlpha(value: Int) =
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) =
settingsRepository.putFloat(PreferKey.readMenuLensRadius, value.coerceIn(0f, 48f))
@@ -446,6 +454,7 @@ class ReadSettingsRepository(
readMenuBottomBarBlurMode = this[Keys.ReadMenuBottomBarBlurMode]
?: ReadMenuBlurMode.None,
readMenuTopBarLiquidGlassButtons = this[Keys.ReadMenuTopBarLiquidGlassButtons] ?: false,
readMenuTopBarTitleCapsule = this[Keys.ReadMenuTopBarTitleCapsule] ?: false,
readMenuBottomBarLiquidGlassButtons = this[Keys.ReadMenuBottomBarLiquidGlassButtons]
?: false,
readMenuTopBarBlurStyle = this[Keys.ReadMenuTopBarBlurStyle]
@@ -454,6 +463,8 @@ class ReadSettingsRepository(
?: ReadMenuBlurStyle.Solid,
readMenuBlurRadius = this[Keys.ReadMenuBlurRadius] ?: 24,
readMenuBlurAlpha = this[Keys.ReadMenuBlurAlpha] ?: 60,
readMenuBlurColor = this[Keys.ReadMenuBlurColor] ?: 0,
readMenuPaletteStyle = this[Keys.ReadMenuPaletteStyle] ?: "",
readMenuLensRadius = this[Keys.ReadMenuLensRadius] ?: 24f,
readMenuBorderWidth = this[Keys.ReadMenuBorderWidth] ?: 0,
readMenuBorderColor = this[Keys.ReadMenuBorderColor] ?: 0,
@@ -540,12 +551,16 @@ class ReadSettingsRepository(
val ReadMenuBottomBarBlurMode = intPreferencesKey(PreferKey.readMenuBottomBarBlurMode)
val ReadMenuTopBarLiquidGlassButtons =
booleanPreferencesKey(PreferKey.readMenuTopBarLiquidGlassButtons)
val ReadMenuTopBarTitleCapsule =
booleanPreferencesKey(PreferKey.readMenuTopBarTitleCapsule)
val ReadMenuBottomBarLiquidGlassButtons =
booleanPreferencesKey(PreferKey.readMenuBottomBarLiquidGlassButtons)
val ReadMenuTopBarBlurStyle = intPreferencesKey(PreferKey.readMenuTopBarBlurStyle)
val ReadMenuBottomBarBlurStyle = intPreferencesKey(PreferKey.readMenuBottomBarBlurStyle)
val ReadMenuBlurRadius = intPreferencesKey(PreferKey.readMenuBlurRadius)
val ReadMenuBlurAlpha = intPreferencesKey(PreferKey.readMenuBlurAlpha)
val ReadMenuBlurColor = intPreferencesKey(PreferKey.readMenuBlurColor)
val ReadMenuPaletteStyle = stringPreferencesKey(PreferKey.readMenuPaletteStyle)
val ReadMenuLensRadius = floatPreferencesKey(PreferKey.readMenuLensRadius)
val ReadMenuBorderWidth = intPreferencesKey(PreferKey.readMenuBorderWidth)
val ReadMenuBorderColor = intPreferencesKey(PreferKey.readMenuBorderColor)
@@ -25,12 +25,13 @@ import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.withTimeout
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
sealed interface ChangeSourceSearchEvent {
data object Started : ChangeSourceSearchEvent
data class Started(val totalSources: Int) : ChangeSourceSearchEvent
data class Progress(
val processedSources: Int,
val totalSources: Int,
@@ -69,23 +70,20 @@ class ChangeSourceSearchUseCase(
if (bookSourceParts.isEmpty()) {
throw io.legado.app.exception.NoStackTraceException("启用书源为空")
}
val sources = bookSourceParts.mapNotNull { it.getBookSource() }
if (sources.isEmpty()) {
throw io.legado.app.exception.NoStackTraceException("启用书源为空")
}
tocMap.clear()
bookMap.clear()
tocMapChapterCount.set(0)
emit(ChangeSourceSearchEvent.Started)
val totalSources = bookSourceParts.size
emit(ChangeSourceSearchEvent.Started(totalSources))
var processedSources = 0
var resultCount = 0
val totalSources = sources.size
val concurrency = threadCount.coerceAtLeast(1)
sources.asFlow()
bookSourceParts.asFlow()
.mapNotNull { it.getBookSource() }
.flatMapMerge(concurrency) { source ->
flow {
val books = try {
@@ -231,7 +229,7 @@ class ChangeSourceSearchUseCase(
contentProcessor
)
}
return null
return book.toSearchBook()
}
private suspend fun loadBookWordCount(
@@ -236,6 +236,8 @@ object ReadBookConfig {
var readMenuBottomBarBlurStyle by clampedPrefDelegate(PreferKey.readMenuBottomBarBlurStyle, ReadMenuBlurStyle.Solid, 0..1)
var readMenuBlurRadius by clampedPrefDelegate(PreferKey.readMenuBlurRadius, 24, 0..32)
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 readMenuBorderWidth by clampedPrefDelegate(PreferKey.readMenuBorderWidth, 0, 0..4)
var titleBarIconPosition by clampedPrefDelegate(PreferKey.titleBarIconPosition, 0, 0..3)
@@ -70,8 +70,11 @@ class ChangeBookSourceComposeViewModel(
val loadWordCount: Boolean get() = ChangeSourceConfig.loadWordCount
// Internal state
private val chapterNumRegex = "^\\[(\\d+)]".toRegex()
private var searchJob: Job? = null
private var oldBook: Book? = null
private var bookName: String = ""
private var bookAuthor: String = ""
private var fromReadBookActivity: Boolean = false
private var screenKey: String = ""
private val searchResults = mutableListOf<SearchBook>()
@@ -86,15 +89,42 @@ class ChangeBookSourceComposeViewModel(
fun initData(name: String, author: String, book: Book, fromReadBookActivity: Boolean) {
this.oldBook = book
this.bookName = name
this.bookAuthor = author
this.fromReadBookActivity = fromReadBookActivity
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() {
val book = oldBook ?: return
stopSearch()
// 清除 DB 中的旧记录
io.legado.app.data.appDb.searchBookDao.clear(bookName, bookAuthor)
searchResults.clear()
bookMap.clear()
tocMap.clear()
@@ -113,6 +143,7 @@ class ChangeBookSourceComposeViewModel(
when (event) {
is ChangeSourceSearchEvent.Started -> {
_isSearching.value = true
totalSourceCount = event.totalSources
}
is ChangeSourceSearchEvent.Progress -> {
@@ -123,6 +154,8 @@ class ChangeBookSourceComposeViewModel(
is ChangeSourceSearchEvent.Result -> {
searchResults.add(event.searchBook)
bookMap[event.searchBook.primaryStr()] = event.searchBook
// 持久化到 DB
io.legado.app.data.appDb.searchBookDao.insert(event.searchBook)
filterResults()
}
@@ -179,12 +212,24 @@ class ChangeBookSourceComposeViewModel(
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) }
.thenByDescending { io.legado.app.help.config.SourceConfig.getSourceScore(it.origin) }
.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(
@@ -247,7 +292,14 @@ class ChangeBookSourceComposeViewModel(
fun refresh() {
searchResults.clear()
bookMap.clear()
startSearch()
val dbBooks = getDbSearchBooks()
if (dbBooks.isNotEmpty()) {
searchResults.addAll(dbBooks)
searchResults.forEach { bookMap[it.primaryStr()] = it }
filterResults()
} else {
startSearch()
}
}
// Source actions
@@ -340,6 +392,23 @@ class ChangeBookSourceComposeViewModel(
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() {
ChangeSourceConfig.searchScope = searchScope.toString()
_scopeUiState.value = ScopeUiState(
@@ -46,12 +46,14 @@ private fun rememberReadBookColorTheme(
preferences: ReadPreferences,
): ThemeOverrideState? {
val isAppDark = LegadoTheme.isDark
val paletteStyle = preferences.readMenuPaletteStyle
return when (preferences.readBarStyle) {
1 -> rememberReadBackgroundTheme(styleConfig, isAppDark)
1 -> rememberReadBackgroundTheme(styleConfig, isAppDark, paletteStyle)
2 -> rememberCustomReadMenuTheme(
styleConfig = styleConfig,
preferences = preferences,
isAppDark = isAppDark,
paletteStyle = paletteStyle,
)
else -> null
}
@@ -61,6 +63,7 @@ private fun rememberReadBookColorTheme(
private fun rememberReadBackgroundTheme(
styleConfig: ReadBookStyleConfig,
isAppDark: Boolean,
paletteStyle: String,
): ThemeOverrideState? {
val background = remember(styleConfig, isAppDark) {
runCatching { ReadStyleResolver.currentBackground(ReadBookConfig.durConfig) }.getOrNull()
@@ -85,6 +88,7 @@ private fun rememberReadBackgroundTheme(
backgroundColor = surfaceColor,
containerColor = null,
fallbackDark = isAppDark,
paletteStyle = paletteStyle,
)
}
@@ -93,6 +97,7 @@ private fun rememberCustomReadMenuTheme(
styleConfig: ReadBookStyleConfig,
preferences: ReadPreferences,
isAppDark: Boolean,
paletteStyle: String,
): ThemeOverrideState {
val menuBackgroundColor = remember(
styleConfig,
@@ -127,6 +132,7 @@ private fun rememberCustomReadMenuTheme(
menuBackgroundColor = menuBackgroundColor,
menuContainerColor = menuContainerColor,
isDark = isAppDark,
paletteStyle = paletteStyle,
)
}
@@ -136,11 +142,13 @@ private fun rememberCustomReadMenuTheme(
containerColor = null,
fallbackDark = isAppDark,
deriveDarkFromColor = false,
paletteStyle = paletteStyle,
) ?: buildReadThemeOverride(
seedColor = accentColor,
backgroundColor = null,
containerColor = null,
isDark = isAppDark,
paletteStyle = paletteStyle,
)
}
@@ -150,13 +158,15 @@ private fun rememberCustomReadMenuThemeOverride(
menuBackgroundColor: Color,
menuContainerColor: Color,
isDark: Boolean,
paletteStyle: String,
): ThemeOverrideState {
return remember(accentColor, menuBackgroundColor, menuContainerColor, isDark) {
return remember(accentColor, menuBackgroundColor, menuContainerColor, isDark, paletteStyle) {
buildReadThemeOverride(
seedColor = accentColor,
backgroundColor = null,
containerColor = null,
isDark = isDark,
paletteStyle = paletteStyle,
).let { base ->
base.copy(
colorScheme = base.colorScheme.withCustomReadMenuColors(
@@ -176,6 +186,7 @@ private fun rememberReadThemeOverride(
containerColor: Color?,
fallbackDark: Boolean = LegadoTheme.isDark,
deriveDarkFromColor: Boolean = true,
paletteStyle: String = "",
): ThemeOverrideState? {
val isDark = remember(backgroundColor, containerColor, fallbackDark, deriveDarkFromColor) {
if (deriveDarkFromColor) {
@@ -184,12 +195,13 @@ private fun rememberReadThemeOverride(
fallbackDark
}
}
return remember(seedColor, backgroundColor, containerColor, isDark) {
return remember(seedColor, backgroundColor, containerColor, isDark, paletteStyle) {
buildReadThemeOverride(
seedColor = seedColor,
backgroundColor = backgroundColor,
containerColor = containerColor,
isDark = isDark,
paletteStyle = paletteStyle,
)
}
}
@@ -199,12 +211,17 @@ private fun buildReadThemeOverride(
backgroundColor: Color?,
containerColor: Color?,
isDark: Boolean,
paletteStyle: String = "",
): ThemeOverrideState {
val colorSpec = ThemeResolver.resolveColorSpecFromMaterialVersion(ThemeConfig.materialVersion)
val resolvedPaletteStyle = paletteStyle
.takeIf { it.isNotBlank() }
?.let { ThemeResolver.resolvePaletteStyle(it) }
?: ThemeResolver.resolvePaletteStyle(ThemeConfig.paletteStyle)
val base = buildThemeOverrideState(
seedColor = seedColor,
isDark = isDark,
paletteStyle = ThemeResolver.resolvePaletteStyle(ThemeConfig.paletteStyle),
paletteStyle = resolvedPaletteStyle,
colorSpec = colorSpec,
usePureBlack = false,
)
@@ -182,6 +182,8 @@ data class ReadMenuConfig(
val readMenuBorderColor: Int = 0,
val readMenuBorderColorNight: Int = 0,
val readMenuBlurAlpha: Int = 60,
val readMenuBlurColor: Int = 0,
val readMenuPaletteStyle: String = "",
val readMenuBlurRadius: Int = 24,
val readMenuLensRadius: Float = 24f,
val readMenuTopBarBlurMode: Int = ReadMenuBlurMode.None,
@@ -482,6 +484,8 @@ sealed interface ReadBookIntent {
data object OpenSystemTtsSettings : ReadBookIntent
data object ClearTtsCache : 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 ColorSelected(val dialogId: Int, val color: Int) : ReadBookIntent
@@ -653,6 +657,7 @@ sealed interface ReadBookSheet {
data object ShadowSet : ReadBookSheet
data object UnderlineConfig : ReadBookSheet
data object FontSelect : ReadBookSheet
data object TitleFontSelect : ReadBookSheet
data object HighlightRuleConfig : ReadBookSheet
data object MoreConfig : ReadBookSheet
data object BgTextConfig : ReadBookSheet
@@ -769,6 +774,18 @@ sealed interface ConfigUpdate {
data class TitleColor(val color: Int) : ConfigUpdate {
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 ---
data class HeaderMode(val value: Int) : ConfigUpdate {
@@ -1023,6 +1040,12 @@ sealed interface ConfigUpdate {
data class MenuBlurAlpha(val value: Int) : ConfigUpdate {
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 {
override val actions = emptySet<ConfigUpdateAction>()
}
@@ -195,7 +195,7 @@ fun ReadBookMenuBar(
}
}
val hideTopBar = dialogLikeRoute ||
currentRoute == ReadBookMenuRoute.ReadStyle && readStylePage >= 1
currentRoute == ReadBookMenuRoute.TextTitle
val menuColors = readMenuColors()
Box(Modifier.fillMaxSize()) {
@@ -683,6 +683,9 @@ private fun ReadBookMenuSurface(
onOpenFontSelect = {
onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.FontSelect))
},
onOpenTitleFontSelect = {
onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TitleFontSelect))
},
modifier = Modifier.padding(horizontal = 16.dp),
onIntent = onIntent,
)
@@ -902,55 +905,61 @@ private fun MenuTitleBar(
)
) {
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(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
// Back button
MenuTitleGlassButton(
onClick = { onIntent(ReadBookIntent.CloseReadBook) },
icon = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
state = state,
colors = colors,
backdrop = backdrop,
)
if (useTitleCapsule) {
// Title capsule layout: title + chapter in a glass pill
TitleCapsuleGlassLayout(
// Left group: back + capsule/title — fills remaining space
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically,
) {
MenuTitleGlassButton(
onClick = { onIntent(ReadBookIntent.CloseReadBook) },
icon = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
state = state,
colors = colors,
onIntent = onIntent,
backdrop = backdrop,
titleTextColor = titleTextColor,
)
} else if (titleBarMode != "1" && titleBarMode != "3") {
AppText(
text = state.bookName,
modifier = Modifier
.weight(1f)
.clickable { onIntent(ReadBookIntent.OpenBookInfo) }
.padding(horizontal = 8.dp, vertical = 4.dp),
style = LegadoTheme.typography.titleMedium.copy(
shadow = androidx.compose.ui.graphics.Shadow(
color = Color.Black.copy(alpha = 0.12f),
offset = Offset.Zero,
blurRadius = 12f
)
),
color = titleTextColor,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
} else {
Spacer(Modifier.weight(1f))
if (useTitleCapsule) {
TitleCapsuleGlassLayout(
state = state,
colors = colors,
onIntent = onIntent,
backdrop = backdrop,
titleTextColor = capsuleIconColor,
)
} else if (titleBarMode != "1" && titleBarMode != "3") {
AppText(
text = state.bookName,
modifier = Modifier
.weight(1f)
.clickable { onIntent(ReadBookIntent.OpenBookInfo) }
.padding(horizontal = 8.dp, vertical = 4.dp),
style = LegadoTheme.typography.titleMedium.copy(
shadow = androidx.compose.ui.graphics.Shadow(
color = Color.Black.copy(alpha = 0.12f),
offset = Offset.Zero,
blurRadius = 12f
)
),
color = titleTextColor,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
// Right group: actions
if (readMenuTopBarButtonLiquidGlassEnabled(backdrop, state.menuConfig)) {
MenuTitleBarMergedGlassButton(
state = state,
@@ -963,7 +972,6 @@ private fun MenuTitleBar(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
// Source action button (non-local books only)
if (!state.isLocalBook) {
SourceActionButton(
state = state,
@@ -1252,7 +1260,7 @@ private fun RowScope.TitleCapsuleGlassLayout(
Row(
modifier = Modifier
.weight(1f)
.padding(horizontal = 8.dp)
.padding(start = 12.dp)
.height(40.dp)
.readMenuLiquidGlass(
backdrop = backdrop,
@@ -1320,24 +1328,20 @@ private fun MenuTitleBarMergedGlassButton(
val tint = LegadoTheme.colorScheme.onSurfaceVariant
Box {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.size(48.dp),
) {
Row(
modifier = Modifier
.height(40.dp)
.readMenuLiquidGlass(
backdrop = backdrop,
colors = colors,
shape = pillShape,
useTopBarStyle = true,
useLens = true,
blurRadius = 32.dp,
interactive = true,
menuConfig = state.menuConfig,
)
.padding(horizontal = 4.dp),
Row(
modifier = Modifier
.height(40.dp)
.readMenuLiquidGlass(
backdrop = backdrop,
colors = colors,
shape = pillShape,
useTopBarStyle = true,
useLens = true,
blurRadius = 32.dp,
interactive = true,
menuConfig = state.menuConfig,
)
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
// SwapHoriz - change source
@@ -1418,7 +1422,6 @@ private fun MenuTitleBarMergedGlassButton(
)
}
}
}
// Dropdown menus
if (!state.isLocalBook) {
@@ -2827,8 +2830,9 @@ private fun Modifier.readMenuHazeEffect(
progressiveBottomToTop: Boolean = false,
): Modifier {
val surfaceAlpha = menuConfig.readMenuBlurAlpha.coerceIn(0, 100) / 100f
val blurTintColor = menuConfig.readMenuBlurColor.takeIf { it != 0 }?.let { Color(it) }
val hazeContainerColor = if (progressive) {
Color.Black.copy(alpha = surfaceAlpha)
(blurTintColor ?: Color.Black).copy(alpha = surfaceAlpha)
} else {
colors.background.copy(alpha = surfaceAlpha)
}
@@ -174,6 +174,17 @@ fun ReadBookScreen(
onOpenFolderPicker = { onIntent(ReadBookIntent.OpenFontFolderPicker) },
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(
show = state.activeSheet is ReadBookSheet.ToolButtonConfig,
items = state.menuConfig.bottomBarButtons,
@@ -896,6 +896,14 @@ class ReadBookViewModel(
}
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 -> {
ReadConfig.systemTypefaces = intent.index
ReadBookConfig.textFont = ""
@@ -1448,6 +1456,7 @@ class ReadBookViewModel(
val old = previous
previous = preferences
ReadConfig.syncReadPreferences(preferences)
ReadBookConfig.readMenuPaletteStyle = preferences.readMenuPaletteStyle
_readPreferences.value = preferences
if (!preferences.hasMenuClickArea()) {
ReadConfig.detectClickArea()
@@ -1599,6 +1608,8 @@ class ReadBookViewModel(
readMenuBorderColor = ReadBookConfig.readMenuBorderColor,
readMenuBorderColorNight = ReadBookConfig.readMenuBorderColorNight,
readMenuBlurAlpha = ReadBookConfig.readMenuBlurAlpha,
readMenuBlurColor = ReadBookConfig.readMenuBlurColor,
readMenuPaletteStyle = ReadBookConfig.readMenuPaletteStyle,
readMenuBlurRadius = ReadBookConfig.readMenuBlurRadius,
readMenuLensRadius = ReadBookConfig.readMenuLensRadius,
readMenuTopBarBlurMode = ReadBookConfig.readMenuTopBarBlurMode,
@@ -2507,6 +2518,10 @@ class ReadBookViewModel(
is ConfigUpdate.TitleTopSpacing -> ReadBookConfig.titleTopSpacing = update.value
is ConfigUpdate.TitleBottomSpacing -> ReadBookConfig.titleBottomSpacing = update.value
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 ---
is ConfigUpdate.HeaderMode -> ReadBookConfig.headerMode = update.value
@@ -2829,6 +2844,20 @@ class ReadBookViewModel(
}
_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 -> {
ReadBookConfig.readMenuLensRadius = update.value
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() {
val nextMode = if (ReadConfig.isNightTheme) "1" else "2"
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.SkipPrevious
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.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@@ -40,13 +44,16 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringArrayResource
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.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.ReadBookIntent
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.dialog.ColorPickerSheet
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.TinyColorModeSettingItem
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_BORDER = 11
private const val COLOR_BORDER_NIGHT = 12
private const val COLOR_BLUR_TINT = 13
@Composable
internal fun SystemMenuPage(
@@ -196,6 +206,7 @@ internal fun SystemMenuPage(
COLOR_MENU_CONTAINER_NIGHT -> onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.MenuContainerColorNight(color)))
COLOR_BORDER -> onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.BorderColor(color)))
COLOR_BORDER_NIGHT -> onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.BorderColorNight(color)))
COLOR_BLUR_TINT -> onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.MenuBlurColor(color)))
}
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) {
Column {
TinyDropdownSettingItem(
@@ -461,6 +489,18 @@ private fun GlobalMenuTab(
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("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,
onOpenHighlightRule: () -> Unit,
onOpenFontSelect: () -> Unit,
onOpenTitleFontSelect: () -> Unit,
modifier: Modifier = Modifier,
onIntent: (ReadBookIntent) -> Unit,
) {
@@ -85,6 +86,7 @@ fun ReadStyleTextTitleContent(
onOpenUnderlineConfig = onOpenUnderlineConfig,
onOpenHighlightRule = onOpenHighlightRule,
onOpenFontSelect = onOpenFontSelect,
onOpenTitleFontSelect = onOpenTitleFontSelect,
animateToPage = { page -> scope.launch { pagerState.animateScrollToPage(page) } },
modifier = modifier,
onIntent = onIntent,
@@ -101,6 +103,7 @@ internal fun ReadStyleTextTitleContent(
onOpenUnderlineConfig: () -> Unit,
onOpenHighlightRule: () -> Unit,
onOpenFontSelect: () -> Unit,
onOpenTitleFontSelect: () -> Unit,
animateToPage: (Int) -> Unit,
modifier: Modifier = Modifier,
onIntent: (ReadBookIntent) -> Unit,
@@ -132,7 +135,10 @@ internal fun ReadStyleTextTitleContent(
)
1 -> LayoutSpacingPage(onIntent = onIntent)
2 -> TitleSettingsPage(onIntent = onIntent)
2 -> TitleSettingsPage(
onOpenTitleFontSelect = onOpenTitleFontSelect,
onIntent = onIntent,
)
}
}
}
@@ -358,10 +364,14 @@ internal fun TextEffectsPage(
@Composable
internal fun TitleSettingsPage(
onOpenTitleFontSelect: () -> Unit,
onIntent: (ReadBookIntent) -> Unit,
) {
var titleMode by remember(ReadBookConfig.titleMode) { mutableIntStateOf(ReadBookConfig.titleMode) }
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 titleLineSpacingExtra by remember(ReadBookConfig.titleLineSpacingExtra) { mutableIntStateOf(ReadBookConfig.titleLineSpacingExtra) }
var titleLineSpacingSub by remember(ReadBookConfig.titleLineSpacingSub) { mutableIntStateOf(ReadBookConfig.titleLineSpacingSub) }
@@ -432,6 +442,85 @@ internal fun TitleSettingsPage(
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
TinySliderSettingItem(
title = stringResource(R.string.subtitle_scale),
@@ -104,7 +104,7 @@ fun ScopeSelectSheet(
applyDraftSelection()
onDismissRequest()
},
text = stringResource(R.string.confirm),
icon = AppIcons.Check,
)
}
},
@@ -92,6 +92,7 @@ fun ColorScheme.toLegadoColorScheme(): LegadoColorScheme {
fun ProvideColorSchemeOverride(
colorScheme: ColorScheme,
seedColor: Color = colorScheme.primary,
overrideIsDark: Boolean? = null,
content: @Composable () -> Unit,
) {
val themeAnimationSpec = tween<Color>(
@@ -106,10 +107,12 @@ fun ProvideColorSchemeOverride(
label = "theme_seed_animation"
).value
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(
colorScheme = animatedColorScheme,
seedColor = animatedSeedColor,
isDark = resolvedIsDark,
)
}
val materialTypography = MaterialTheme.typography
@@ -16,6 +16,7 @@ import io.legado.app.ui.config.themeConfig.ThemeConfig
data class ThemeOverrideState(
val seedColor: Color,
val colorScheme: ColorScheme,
val isDark: Boolean = false,
)
fun buildThemeOverrideState(
@@ -45,7 +46,8 @@ fun buildThemeOverrideState(
return ThemeOverrideState(
seedColor = seedColor,
colorScheme = colorScheme
colorScheme = colorScheme,
isDark = isDark,
)
}
@@ -72,6 +74,7 @@ fun ProvideThemeOverride(
ProvideColorSchemeOverride(
colorScheme = currentTheme.colorScheme,
seedColor = currentTheme.seedColor,
overrideIsDark = currentTheme.isDark,
content = content
)
} else {
@@ -438,8 +438,8 @@ fun ChangeSourceSheet(
selectedSources = scopeState.sourceUrls,
onToggleSource = { viewModel.toggleScopeSource(it) },
isSourceScope = scopeState.isSource,
onConfirm = {
viewModel.startSearch()
onApplyScope = { selection ->
viewModel.applyScopeSelection(selection)
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.outlined.LibraryBooks
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.Delete
import androidx.compose.material.icons.filled.Edit
@@ -111,6 +112,10 @@ object AppIcons {
@Composable
get() = if (isMiuix) MiuixIcons.MoreCircle else Icons.Default.MoreHoriz
val Check: ImageVector
@Composable
get() = Icons.Default.Check
@Composable
fun mainDestination(destination: MainDestination, selected: Boolean): ImageVector {
@@ -65,6 +65,7 @@ fun RoundDropdownMenu(
}
} else {
val colorScheme = rememberOpaqueColorScheme()
val popupContainerColor = LegadoTheme.colorScheme.surfaceContainerLow
DropdownMenu(
expanded = expanded,
@@ -72,7 +73,7 @@ fun RoundDropdownMenu(
modifier = modifier,
shape = shape,
shadowElevation = shadowElevation,
containerColor = colorScheme.surfaceContainerLow
containerColor = popupContainerColor
) {
MaterialExpressiveTheme(
colorScheme = colorScheme,
@@ -129,6 +130,7 @@ fun RoundDropdownMenuLazy(
}
} else {
val colorScheme = rememberOpaqueColorScheme()
val popupContainerColor = LegadoTheme.colorScheme.surfaceContainerLow
DropdownMenu(
expanded = expanded,
@@ -136,7 +138,7 @@ fun RoundDropdownMenuLazy(
modifier = modifier,
shape = shape,
shadowElevation = shadowElevation,
containerColor = colorScheme.surfaceContainerLow
containerColor = popupContainerColor
) {
MaterialExpressiveTheme(
colorScheme = colorScheme,
@@ -35,7 +35,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.theme.rememberOpaqueColorScheme
import io.legado.app.ui.widget.components.icon.AppIcon
import top.yukonga.miuix.kmp.basic.DropdownDefaults
import top.yukonga.miuix.kmp.icon.MiuixIcons
@@ -121,9 +120,9 @@ fun RoundDropdownMenuItem(
}
}
} else {
val colorScheme = rememberOpaqueColorScheme()
val selectedContentColor = colorScheme.primary
val defaultContentColor = colorScheme.onSurface
val legadoColorScheme = LegadoTheme.colorScheme
val selectedContentColor = legadoColorScheme.primary
val defaultContentColor = legadoColorScheme.onSurface
val contentColor = if (enabled) {
when {
hasCustomContentColor -> color
@@ -137,7 +136,7 @@ fun RoundDropdownMenuItem(
else -> defaultContentColor.copy(alpha = 0.38f)
}
}
val containerColor = colorScheme.surface
val containerColor = legadoColorScheme.surface
Surface(
onClick = onClick,