[优化] 重写书籍详情页、本地书籍页和优化一些界面,为Miuix增加自定义取色功能,优化颜色选择器

This commit is contained in:
HapeLee
2026-04-13 01:56:03 +08:00
parent 1d585ecec9
commit 1bd61c719a
48 changed files with 4375 additions and 2165 deletions
+2 -1
View File
@@ -176,7 +176,8 @@
<activity
android:name=".ui.book.info.edit.BookInfoEditActivity"
android:enableOnBackInvokedCallback="true"
android:launchMode="singleTask" />
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize|stateHidden" />
<!-- 音频播放界面 -->
<activity
android:name="io.legado.app.ui.book.audio.AudioPlayActivity"
@@ -185,6 +185,7 @@ object PreferKey {
const val paletteStyle = "paletteStyle"
const val materialVersion = "materialVersion"
const val composeEngine = "composeEngine"
const val useMiuixMonet = "useMiuixMonet"
const val containerOpacity = "containerOpacity"
const val topBarOpacity = "topBarOpacity"
const val bottomBarOpacity = "bottomBarOpacity"
@@ -20,6 +20,9 @@ import io.legado.app.help.coil.CoverInterceptor
import io.legado.app.help.http.okHttpClient
import io.legado.app.help.http.okHttpClientManga
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
import io.legado.app.ui.book.changecover.ChangeCoverViewModel
import io.legado.app.ui.book.changesource.ChangeBookSourceComposeViewModel
import io.legado.app.ui.book.changesource.ChangeBookSourceViewModel
import io.legado.app.ui.book.explore.ExploreShowViewModel
import io.legado.app.ui.book.group.GroupViewModel
import io.legado.app.ui.book.import.local.ImportBookViewModel
@@ -101,6 +104,9 @@ val appModule = module {
viewModelOf(::ImportBookViewModel)
viewModelOf(::RemoteBookViewModel)
viewModelOf(::BookInfoViewModel)
viewModelOf(::ChangeCoverViewModel)
viewModelOf(::ChangeBookSourceComposeViewModel)
viewModelOf(::ChangeBookSourceViewModel)
viewModelOf(::ExploreViewModel)
viewModelOf(::RssViewModel)
@@ -25,8 +25,10 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import java.util.Collections
@@ -51,6 +53,8 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
}
private var task: Job? = null
val searchStateData = MutableLiveData<Boolean>()
private val _isSearching = MutableStateFlow(false)
val isSearching = _isSearching.asStateFlow()
var name: String = ""
var author: String = ""
val searchBooks: MutableList<SearchBook> = Collections.synchronizedList(arrayListOf())
@@ -94,6 +98,11 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
}
}
fun initData(name: String, author: String) {
this.name = name
this.author = author.replace(AppPattern.authorRegex, "")
}
private fun initSearchPool() {
searchPool = Executors
.newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher()
@@ -121,12 +130,14 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
}
}.onStart {
searchStateData.postValue(true)
_isSearching.value = true
}.mapParallelSafe(threadCount) {
withTimeout(60000L) {
search(it)
}
}.onCompletion {
searchStateData.postValue(false)
_isSearching.value = false
}.catch {
AppLog.put("封面换源搜索出错\n${it.localizedMessage}", it)
}.collect()
@@ -156,10 +167,11 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
}
}
private fun stopSearch() {
fun stopSearch() {
task?.cancel()
searchPool?.close()
searchStateData.postValue(false)
_isSearching.value = false
}
override fun onCleared() {
@@ -167,4 +179,4 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
searchPool?.close()
}
}
}
@@ -0,0 +1,65 @@
package io.legado.app.ui.book.changesource
import android.app.Application
import io.legado.app.data.entities.SearchBook
import kotlinx.coroutines.flow.StateFlow
class ChangeBookSourceComposeViewModel(application: Application) :
ChangeBookSourceViewModel(application) {
val searchGroup: String
get() = ChangeSourceConfig.searchGroup
val checkAuthor: Boolean
get() = ChangeSourceConfig.checkAuthor
val loadInfo: Boolean
get() = ChangeSourceConfig.loadInfo
val loadToc: Boolean
get() = ChangeSourceConfig.loadToc
val loadWordCount: Boolean
get() = ChangeSourceConfig.loadWordCount
fun onSearchGroupSelected(group: String) {
if (ChangeSourceConfig.searchGroup == group) return
ChangeSourceConfig.searchGroup = group
if (refresh()) startSearch()
}
fun onCheckAuthorChange(enabled: Boolean) {
if (ChangeSourceConfig.checkAuthor == enabled) return
ChangeSourceConfig.checkAuthor = enabled
refresh()
}
fun onLoadInfoChange(enabled: Boolean) {
if (ChangeSourceConfig.loadInfo == enabled) return
ChangeSourceConfig.loadInfo = enabled
}
fun onLoadTocChange(enabled: Boolean) {
if (ChangeSourceConfig.loadToc == enabled) return
ChangeSourceConfig.loadToc = enabled
}
fun onLoadWordCountChange(enabled: Boolean) {
if (ChangeSourceConfig.loadWordCount == enabled) return
ChangeSourceConfig.loadWordCount = enabled
if (enabled) {
onLoadWordCountChecked(true)
} else {
refresh()
}
}
fun bookScoreFlow(searchBook: SearchBook): StateFlow<Int> {
return ObservableSourceConfig.bookScoreFlow(searchBook)
}
fun onBookScoreClick(searchBook: SearchBook) {
val currentScore = ObservableSourceConfig.getBookScore(searchBook)
setBookScore(searchBook, if (currentScore > 0) 0 else 1)
}
}
@@ -62,6 +62,8 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
private val threadCount = AppConfig.threadCount
private var searchPool: ExecutorCoroutineDispatcher? = null
val searchStateData = MutableLiveData<Boolean>()
private val _isSearching = MutableStateFlow(false)
val isSearching = _isSearching.asStateFlow()
var searchFinishCallback: ((isEmpty: Boolean) -> Unit)? = null
var name: String = ""
var author: String = ""
@@ -163,6 +165,19 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
}
}
@CallSuper
open fun initData(
name: String,
author: String,
book: Book?,
fromReadBookActivity: Boolean
) {
this.name = name
this.author = author.replace(AppPattern.authorRegex, "")
this.fromReadBookActivity = fromReadBookActivity
oldBook = book
}
private fun initSearchPool() {
searchPool = Executors
.newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher()
@@ -234,6 +249,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
}
}.onStart {
searchStateData.postValue(true)
_isSearching.value = true
}.mapParallel(threadCount) {
try {
withTimeout(60000L) {
@@ -250,6 +266,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
}.onCompletion {
ensureActive()
searchStateData.postValue(false)
_isSearching.value = false
searchFinishCallback?.invoke(searchBooks.isEmpty())
}.catch {
AppLog.put("换源搜索出错\n${it.localizedMessage}", it)
@@ -383,6 +400,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
}
}.onStart {
searchStateData.postValue(true)
_isSearching.value = true
}.mapParallelSafe(threadCount) {
val source = appDb.bookSourceDao.getBookSource(it.origin)!!
withTimeout(60000L) {
@@ -390,6 +408,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
}
}.onCompletion {
searchStateData.postValue(false)
_isSearching.value = false
}.catch {
AppLog.put("换源刷新列表出错\n${it.localizedMessage}", it)
}.collect()
@@ -446,6 +465,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
task?.cancel()
searchPool?.close()
searchStateData.postValue(false)
_isSearching.value = false
}
fun getToc(
@@ -554,10 +574,8 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
}
fun setBookScore(searchBook: SearchBook, score: Int) {
execute {
SourceConfig.setBookScore(searchBook.origin, searchBook.name, searchBook.author, score)
searchCallback?.upAdapter()
}
SourceConfig.setBookScore(searchBook.origin, searchBook.name, searchBook.author, score)
searchCallback?.upAdapter()
}
fun getBookScore(searchBook: SearchBook): Int {
@@ -577,4 +595,4 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
}
}
}
@@ -0,0 +1,32 @@
package io.legado.app.ui.book.changesource
import io.legado.app.constant.PreferKey
import io.legado.app.ui.config.prefDelegate
object ChangeSourceConfig {
var searchGroup by prefDelegate(
key = "searchGroup",
defaultValue = ""
)
var checkAuthor by prefDelegate(
key = PreferKey.changeSourceCheckAuthor,
defaultValue = false
)
var loadInfo by prefDelegate(
key = PreferKey.changeSourceLoadInfo,
defaultValue = false
)
var loadToc by prefDelegate(
key = PreferKey.changeSourceLoadToc,
defaultValue = false
)
var loadWordCount by prefDelegate(
key = PreferKey.changeSourceLoadWordCount,
defaultValue = false
)
}
@@ -0,0 +1,71 @@
package io.legado.app.ui.book.changesource
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import io.legado.app.data.entities.SearchBook
import io.legado.app.help.config.SourceConfig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import splitties.init.appCtx
import java.util.concurrent.ConcurrentHashMap
object ObservableSourceConfig : SharedPreferences.OnSharedPreferenceChangeListener {
private val sp = appCtx.getSharedPreferences("SourceConfig", MODE_PRIVATE)
private val bookScoreFlows = ConcurrentHashMap<String, MutableStateFlow<Int>>()
private val sourceScoreFlows = ConcurrentHashMap<String, MutableStateFlow<Int>>()
init {
sp.registerOnSharedPreferenceChangeListener(this)
}
fun bookScoreFlow(searchBook: SearchBook): StateFlow<Int> {
return bookScoreFlow(searchBook.origin, searchBook.name, searchBook.author)
}
fun bookScoreFlow(origin: String, name: String, author: String): StateFlow<Int> {
val key = bookScoreKey(origin, name, author)
return bookScoreFlows.getOrPut(key) {
MutableStateFlow(sp.getInt(key, 0))
}.asStateFlow()
}
fun sourceScoreFlow(origin: String): StateFlow<Int> {
return sourceScoreFlows.getOrPut(origin) {
MutableStateFlow(sp.getInt(origin, 0))
}.asStateFlow()
}
fun getBookScore(searchBook: SearchBook): Int {
return bookScoreFlow(searchBook).value
}
fun setBookScore(searchBook: SearchBook, score: Int) {
setBookScore(searchBook.origin, searchBook.name, searchBook.author, score)
}
fun setBookScore(origin: String, name: String, author: String, score: Int) {
SourceConfig.setBookScore(origin, name, author, score)
syncBookScore(bookScoreKey(origin, name, author))
syncSourceScore(origin)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
key ?: return
syncBookScore(key)
syncSourceScore(key)
}
private fun syncBookScore(key: String) {
bookScoreFlows[key]?.value = sp.getInt(key, 0)
}
private fun syncSourceScore(origin: String) {
sourceScoreFlows[origin]?.value = sp.getInt(origin, 0)
}
private fun bookScoreKey(origin: String, name: String, author: String): String {
return "${origin}_${name}_${author}"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,154 @@
package io.legado.app.ui.book.info
import android.net.Uri
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource
data class BookInfoUiState(
val book: Book? = null,
val chapterList: List<BookChapter> = emptyList(),
val webFiles: List<BookInfoWebFile> = emptyList(),
val kindLabels: List<String> = emptyList(),
val groupNames: String? = null,
val inBookshelf: Boolean = false,
val bookSource: BookSource? = null,
val isTocLoading: Boolean = true,
val isBusy: Boolean = false,
val showAppLogSheet: Boolean = false,
val sheet: BookInfoSheet = BookInfoSheet.None,
val dialog: BookInfoDialog? = null,
)
sealed interface BookInfoSheet {
data object None : BookInfoSheet
data object CoverPicker : BookInfoSheet
data object GroupPicker : BookInfoSheet
data object SourcePicker : BookInfoSheet
data class WebFiles(val openAfterImport: Boolean) : BookInfoSheet
data class ArchiveEntries(
val archiveUri: Uri,
val entries: List<String>,
val openAfterImport: Boolean,
) : BookInfoSheet
}
sealed interface BookInfoDialog {
data object AddToShelfOnBack : BookInfoDialog
data class DeleteBook(val isLocal: Boolean) : BookInfoDialog
data class EditRemark(val remark: String?) : BookInfoDialog
data class PhotoPreview(val path: String) : BookInfoDialog
data class UnsupportedWebFile(
val webFile: BookInfoWebFile,
val openAfterImport: Boolean,
) : BookInfoDialog
}
data class BookInfoWebFile(
val url: String,
val name: String,
) {
override fun toString(): String = name
}
sealed interface BookInfoIntent {
data object BackPressed : BookInfoIntent
data object DismissSheet : BookInfoIntent
data object DismissDialog : BookInfoIntent
data object DismissAppLogSheet : BookInfoIntent
data class MenuAction(val action: BookInfoMenuAction) : BookInfoIntent
data class AuthorClick(val longClick: Boolean) : BookInfoIntent
data class BookNameClick(val longClick: Boolean) : BookInfoIntent
data object OriginClick : BookInfoIntent
data object ReadClick : BookInfoIntent
data object ShelfClick : BookInfoIntent
data object TocClick : BookInfoIntent
data object CoverClick : BookInfoIntent
data object CoverLongClick : BookInfoIntent
data object GroupClick : BookInfoIntent
data object ChangeSourceClick : BookInfoIntent
data object RemarkClick : BookInfoIntent
data object ConfirmBackAddToShelf : BookInfoIntent
data class ConfirmDelete(val deleteOriginal: Boolean) : BookInfoIntent
data class UpdateRemark(val remark: String) : BookInfoIntent
data class SelectGroup(val groupId: Long) : BookInfoIntent
data class SelectCover(val coverUrl: String) : BookInfoIntent
data class ReplaceWithSource(
val source: BookSource,
val book: Book,
val toc: List<BookChapter>,
) : BookInfoIntent
data class AddSourceAsNewBook(
val book: Book,
val toc: List<BookChapter>,
) : BookInfoIntent
data class SelectWebFile(
val webFile: BookInfoWebFile,
val openAfterImport: Boolean,
) : BookInfoIntent
data class OpenUnsupportedWebFile(
val webFile: BookInfoWebFile,
) : BookInfoIntent
data class SelectArchiveEntry(
val archiveUri: Uri,
val entryName: String,
val openAfterImport: Boolean,
) : BookInfoIntent
}
sealed interface BookInfoEffect {
data class Finish(
val resultCode: Int? = null,
val afterTransition: Boolean = false,
) : BookInfoEffect
data class OpenBookInfoEdit(val bookUrl: String) : BookInfoEffect
data class OpenToc(val bookUrl: String) : BookInfoEffect
data class OpenReader(
val book: Book,
val inBookshelf: Boolean,
val chapterChanged: Boolean,
) : BookInfoEffect
data class OpenBookSourceEdit(val sourceUrl: String) : BookInfoEffect
data class OpenSourceLogin(val sourceUrl: String) : BookInfoEffect
data object OpenSelectBooksDir : BookInfoEffect
data class OpenFile(val uri: Uri, val mimeType: String) : BookInfoEffect
data class RunSourceCallback(
val event: String,
val source: BookSource?,
val book: Book,
val action: BookInfoCallbackAction,
) : BookInfoEffect
data class ShowVariableDialog(
val title: String,
val key: String,
val variable: String?,
val comment: String,
) : BookInfoEffect
}
sealed interface BookInfoCallbackAction {
data class Search(val keyword: String) : BookInfoCallbackAction
data class ShareText(val chooserTitle: String, val text: String) : BookInfoCallbackAction
data class CopyText(val text: String) : BookInfoCallbackAction
data object ClearCache : BookInfoCallbackAction
}
enum class BookInfoMenuAction {
Edit,
Share,
Upload,
SyncRemote,
Refresh,
Login,
Top,
SetSourceVariable,
SetBookVariable,
CopyBookUrl,
CopyTocUrl,
ToggleCanUpdate,
ToggleSplitLongChapter,
ToggleDeleteAlert,
ClearCache,
ShowLog,
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,510 @@
package io.legado.app.ui.book.info
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.FilterList
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.PauseCircleOutline
import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.outlined.FolderZip
import androidx.compose.material.icons.outlined.Image
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.legado.app.R
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookGroup
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook
import io.legado.app.ui.book.changecover.ChangeCoverViewModel
import io.legado.app.ui.book.changesource.ChangeBookSourceComposeViewModel
import io.legado.app.ui.book.group.GroupEditSheet
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.book.source.manage.BookSourceActivity
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
import io.legado.app.ui.widget.components.button.MediumIconButton
import io.legado.app.ui.widget.components.button.SmallIconButton
import io.legado.app.ui.widget.components.button.TopBarButton
import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.card.SelectionItemCard
import io.legado.app.ui.widget.components.checkBox.AppCheckbox
import io.legado.app.ui.widget.components.cover.BookCover
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.StartActivityContract
import io.legado.app.utils.startActivity
import io.legado.app.utils.toastOnUi
import org.koin.androidx.compose.koinViewModel
@Composable
fun WebFileSheet(
show: Boolean,
files: List<BookInfoWebFile>,
title: String,
onDismissRequest: () -> Unit,
onSelect: (BookInfoWebFile) -> Unit,
) {
AppModalBottomSheet(show = show, onDismissRequest = onDismissRequest, title = title) {
if (files.isEmpty()) {
Box(modifier = Modifier.fillMaxWidth().padding(24.dp), contentAlignment = Alignment.Center) {
Text(text = stringResource(R.string.empty))
}
} else {
LazyColumn(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
items(files, key = { it.name }) { file ->
GlassCard(onClick = { onSelect(file) }) {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(if (file.name.endsWith("zip") || file.name.endsWith("rar") || file.name.endsWith("7z")) Icons.Outlined.FolderZip else Icons.Outlined.Image, null)
Text(text = file.name, modifier = Modifier.weight(1f), style = LegadoTheme.typography.bodyMedium)
}
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
@Composable
fun GroupSelectSheet(
show: Boolean,
currentGroupId: Long,
onDismissRequest: () -> Unit,
onConfirm: (Long) -> Unit,
) {
val groups by appDb.bookGroupDao.flowSelect().collectAsStateWithLifecycle(initialValue = emptyList())
var selectedGroupId by remember(currentGroupId) { mutableLongStateOf(currentGroupId) }
var editingGroup by remember { mutableStateOf<BookGroup?>(null) }
AppModalBottomSheet(
show = show,
onDismissRequest = onDismissRequest,
title = stringResource(R.string.group_select),
endAction = { IconButton(onClick = { editingGroup = BookGroup() }) { Icon(Icons.Default.Add, null) } }
) {
LazyColumn(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
items(groups, key = { it.groupId }) { group ->
val isSelected = selectedGroupId and group.groupId > 0
SelectionItemCard(
title = group.groupName,
isSelected = isSelected,
onToggleSelection = {
selectedGroupId = if (isSelected) {
selectedGroupId - group.groupId
} else {
selectedGroupId + group.groupId
}
},
leadingContent = {
AppCheckbox(
checked = isSelected,
onCheckedChange = {
selectedGroupId = if (it) {
selectedGroupId + group.groupId
} else {
selectedGroupId - group.groupId
}
}
)
},
trailingAction = {
SmallIconButton(
onClick = { editingGroup = group },
imageVector = Icons.Default.Edit
)
},
containerColor = LegadoTheme.colorScheme.surfaceContainerLow
)
}
}
Spacer(modifier = Modifier.height(12.dp))
ConfirmDismissButtonsRow(
onDismiss = onDismissRequest,
onConfirm = { onConfirm(selectedGroupId) },
dismissText = stringResource(R.string.cancel),
confirmText = stringResource(R.string.ok),
)
Spacer(modifier = Modifier.height(12.dp))
}
GroupEditSheet(show = editingGroup != null, group = editingGroup, onDismissRequest = { editingGroup = null })
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ChangeCoverSheet(
show: Boolean,
name: String,
author: String,
onDismissRequest: () -> Unit,
onSelect: (String) -> Unit,
viewModel: ChangeCoverViewModel = koinViewModel(key = "cover-$name-$author"),
) {
val items by viewModel.dataFlow.collectAsStateWithLifecycle(initialValue = emptyList())
val isSearching by viewModel.isSearching.collectAsStateWithLifecycle()
LaunchedEffect(name, author) {
viewModel.initData(name, author)
}
DisposableEffect(show) {
onDispose {
viewModel.stopSearch()
}
}
AppModalBottomSheet(
show = show,
onDismissRequest = onDismissRequest,
title = stringResource(R.string.change_cover_source),
endAction = {
IconButton(onClick = { viewModel.startOrStopSearch() }) {
Icon(if (isSearching) Icons.Default.MoreVert else Icons.Default.Refresh, null)
}
}
) {
if (isSearching) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
Spacer(modifier = Modifier.height(12.dp))
}
LazyVerticalGrid(columns = GridCells.Fixed(3), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
items(items, key = { it.bookUrl + it.originName }) { item ->
GlassCard(onClick = { onSelect(item.coverUrl.orEmpty()) }) {
Column(modifier = Modifier.fillMaxWidth().padding(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
BookCover(name = item.name, author = item.author, path = item.coverUrl, sourceOrigin = item.origin, modifier = Modifier.fillMaxWidth())
AppText(text = item.originName, style = LegadoTheme.typography.bodySmall, maxLines = 2)
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
@Composable
fun ChangeSourceSheet(
show: Boolean,
oldBook: Book,
onDismissRequest: () -> Unit,
onReplace: (BookSource, Book, List<BookChapter>) -> Unit,
onAddAsNew: (Book, List<BookChapter>) -> Unit,
viewModel: ChangeBookSourceComposeViewModel = koinViewModel(key = "source-${oldBook.bookUrl}"),
) {
val context = LocalContext.current
val items by viewModel.searchDataFlow.collectAsStateWithLifecycle(initialValue = emptyList())
val isSearching by viewModel.isSearching.collectAsStateWithLifecycle()
val progress by viewModel.changeSourceProgress.collectAsStateWithLifecycle()
val groups by appDb.bookSourceDao.flowEnabledGroups().collectAsStateWithLifecycle(initialValue = emptyList())
var searchQuery by rememberSaveable { mutableStateOf("") }
val selectedGroup = viewModel.searchGroup
val checkAuthor = viewModel.checkAuthor
val loadInfo = viewModel.loadInfo
val loadToc = viewModel.loadToc
val loadWordCount = viewModel.loadWordCount
var actionBook by remember { mutableStateOf<SearchBook?>(null) }
var mismatchBook by remember { mutableStateOf<SearchBook?>(null) }
var loadingAction by remember { mutableStateOf(false) }
var showOptionsMenu by rememberSaveable { mutableStateOf(false) }
var showFilterMenu by rememberSaveable { mutableStateOf(false) }
val editSourceResult = rememberLauncherForActivityResult(StartActivityContract(BookSourceEditActivity::class.java)) {
val origin = it.data?.getStringExtra("origin") ?: return@rememberLauncherForActivityResult
viewModel.startSearch(origin)
}
LaunchedEffect(oldBook.bookUrl) {
viewModel.initData(oldBook.name, oldBook.author, oldBook, false)
}
DisposableEffect(oldBook.bookUrl) {
onDispose {
viewModel.stopSearch()
}
}
AppModalBottomSheet(
show = show,
onDismissRequest = onDismissRequest,
title = stringResource(R.string.book_source),
startAction = {
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Box {
MediumIconButton(
onClick = { showOptionsMenu = true },
imageVector = Icons.Default.MoreVert
)
RoundDropdownMenu(
expanded = showOptionsMenu,
onDismissRequest = { showOptionsMenu = false }
) { dismiss ->
RoundDropdownMenuItem(
text = "校验作者",
isSelected = checkAuthor,
onClick = {
viewModel.onCheckAuthorChange(!checkAuthor)
dismiss()
}
)
RoundDropdownMenuItem(
text = "加载详情",
isSelected = loadInfo,
onClick = {
viewModel.onLoadInfoChange(!loadInfo)
dismiss()
}
)
RoundDropdownMenuItem(
text = "加载目录",
isSelected = loadToc,
onClick = {
viewModel.onLoadTocChange(!loadToc)
dismiss()
}
)
RoundDropdownMenuItem(
text = "字数对比",
isSelected = loadWordCount,
onClick = {
viewModel.onLoadWordCountChange(!loadWordCount)
dismiss()
}
)
}
}
MediumIconButton(
onClick = { context.startActivity<BookSourceActivity>() },
imageVector = Icons.Outlined.Settings
)
}
},
endAction = {
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
MediumIconButton(
onClick = { viewModel.startOrStopSearch() },
imageVector = if (isSearching) Icons.Default.PauseCircleOutline else Icons.Default.Refresh,
)
Box {
MediumIconButton(
onClick = { showFilterMenu = true },
imageVector = Icons.Default.FilterList
)
RoundDropdownMenu(
expanded = showFilterMenu,
onDismissRequest = { showFilterMenu = false }
) { dismiss ->
RoundDropdownMenuItem(
text = stringResource(R.string.all_source),
isSelected = selectedGroup.isBlank(),
onClick = {
viewModel.onSearchGroupSelected("")
dismiss()
}
)
groups.forEach { group ->
RoundDropdownMenuItem(
text = group,
isSelected = selectedGroup == group,
onClick = {
viewModel.onSearchGroupSelected(group)
dismiss()
}
)
}
}
}
}
}
) {
AppTextField(
value = searchQuery,
backgroundColor = LegadoTheme.colorScheme.surface,
onValueChange = {
searchQuery = it
viewModel.screen(it)
},
label = stringResource(R.string.screen),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
if (isSearching) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
Spacer(modifier = Modifier.height(8.dp))
AppText(
text = "${progress.first} / ${viewModel.totalSourceCount} · ${items.size}",
style = LegadoTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(12.dp))
}
LazyColumn(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
items(items, key = { it.bookUrl + it.origin }) { item ->
val bookScore by remember(item.origin, item.name, item.author) {
viewModel.bookScoreFlow(item)
}.collectAsStateWithLifecycle()
SelectionItemCard(
title = item.originName,
containerColor = LegadoTheme.colorScheme.surfaceContainer,
selectedContainerColor = LegadoTheme.colorScheme.primaryContainer.copy(alpha = 0.2f),
leadingContent = {
MediumIconButton(
onClick = {
viewModel.onBookScoreClick(item)
},
imageVector = Icons.Default.PushPin,
tint = if (bookScore > 0) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.outline,
contentDescription = null
)
},
supportingContent = {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
AppText(
text = item.author,
style = LegadoTheme.typography.labelLargeEmphasized
)
AppText(
text = item.getDisplayLastChapterTitle(),
style = LegadoTheme.typography.labelMediumEmphasized
)
item.chapterWordCountText?.takeIf { loadWordCount }?.let {
AppText(
text = it,
style = LegadoTheme.typography.labelSmallEmphasized,
color = LegadoTheme.colorScheme.primary
)
}
}
},
isSelected = item.bookUrl == oldBook.bookUrl,
onToggleSelection = {
if (item.bookUrl != oldBook.bookUrl) {
if (!item.sameBookTypeLocal(oldBook.type)) mismatchBook = item else actionBook = item
}
},
dropdownContent = { onDismiss: () -> Unit ->
RoundDropdownMenuItem(
text = stringResource(R.string.to_top),
onClick = {
viewModel.topSource(item)
onDismiss()
}
)
RoundDropdownMenuItem(
text = "置底",
onClick = {
viewModel.bottomSource(item)
onDismiss()
}
)
RoundDropdownMenuItem(
text = stringResource(R.string.edit),
onClick = {
onDismiss()
editSourceResult.launch { putExtra("sourceUrl", item.origin) }
}
)
RoundDropdownMenuItem(
text = "禁用",
onClick = {
viewModel.disableSource(item)
onDismiss()
}
)
RoundDropdownMenuItem(
text = stringResource(R.string.delete),
color = LegadoTheme.colorScheme.error,
onClick = {
viewModel.del(item)
if (oldBook.bookUrl == item.bookUrl) {
viewModel.autoChangeSource(oldBook.type) { book, toc, source ->
onReplace(source, book, toc)
}
}
onDismiss()
}
)
}
)
}
}
Spacer(modifier = Modifier.height(16.dp))
}
val performAction: (SearchBook, Boolean) -> Unit = { searchBook, replace ->
loadingAction = true
val book = viewModel.bookMap[searchBook.primaryStr()] ?: searchBook.toBook()
viewModel.getToc(book, { toc, source ->
loadingAction = false
if (replace) {
onReplace(source, book, toc)
onDismissRequest()
} else {
onAddAsNew(book, toc)
context.toastOnUi(context.getString(R.string.book_added_to_shelf))
}
actionBook = null
}, {
loadingAction = false
context.toastOnUi(if (replace) "换源失败" else "添加书籍失败")
})
}
if (mismatchBook != null) {
AppAlertDialog(show = true, onDismissRequest = { mismatchBook = null }, title = stringResource(R.string.book_type_different), text = stringResource(R.string.soure_change_source), confirmText = stringResource(android.R.string.ok), onConfirm = { actionBook = mismatchBook; mismatchBook = null }, dismissText = stringResource(android.R.string.cancel), onDismiss = { mismatchBook = null })
}
actionBook?.let { searchBook ->
AppAlertDialog(show = true, onDismissRequest = { actionBook = null }, title = stringResource(R.string.change_source_option_title), dismissText = stringResource(R.string.add_as_new_book), onDismiss = { performAction(searchBook, false) }, confirmText = stringResource(R.string.replace_current_book), onConfirm = { performAction(searchBook, true) })
}
if (loadingAction) {
AppAlertDialog(show = true, onDismissRequest = {}, content = {
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { CircularProgressIndicator() }
})
}
}
File diff suppressed because it is too large Load Diff
@@ -5,9 +5,11 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
@@ -89,8 +91,10 @@ fun BookInfoEditScreen(
uiState.book?.let {
BookInfoEditContent(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(paddingValues)
.consumeWindowInsets(paddingValues)
.imePadding()
.verticalScroll(rememberScrollState()),
uiState = uiState,
viewModel = viewModel
@@ -57,18 +57,14 @@ fun LauncherIconPickerSheet(
AppModalBottomSheet(
show = show,
onDismissRequest = onDismissRequest
onDismissRequest = onDismissRequest,
title = stringResource(R.string.change_icon)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp)
) {
AppText(
text = stringResource(R.string.change_icon),
style = LegadoTheme.typography.titleMedium,
modifier = Modifier.padding(16.dp)
)
LazyVerticalGrid(
columns = GridCells.Fixed(3),
@@ -89,15 +85,15 @@ fun LauncherIconPickerSheet(
.clip(MaterialTheme.shapes.large)
.background(
if (isSelected)
MaterialTheme.colorScheme.secondaryContainer
LegadoTheme.colorScheme.secondaryContainer
else
MaterialTheme.colorScheme.surfaceContainer
LegadoTheme.colorScheme.surfaceContainer
)
.then(
if (isSelected) {
Modifier.border(
width = 2.dp,
color = MaterialTheme.colorScheme.primary,
color = LegadoTheme.colorScheme.primary,
shape = MaterialTheme.shapes.large
)
} else {
@@ -24,6 +24,10 @@ object ThemeConfig {
//m3 or miuix
var composeEngine by prefDelegate(PreferKey.composeEngine, "material")
var useMiuixMonet by prefDelegate(PreferKey.useMiuixMonet, false) {
postEvent(EventBus.RECREATE, "")
}
var materialVersion by prefDelegate(PreferKey.materialVersion, "material3")
var appTheme by prefDelegate(PreferKey.appTheme, "0")
@@ -75,7 +75,7 @@ import io.legado.app.help.config.OldThemeConfig
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeManager
import io.legado.app.ui.theme.ThemeEngine
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.widget.components.AppScaffold
@@ -112,6 +112,7 @@ fun ThemeConfigScreen(
var selectedThemeMode by remember { mutableStateOf(ThemeConfig.themeMode) }
var selectedTheme by remember { mutableStateOf(ThemeConfig.appTheme) }
var useMiuixMonet by remember { mutableStateOf(ThemeConfig.useMiuixMonet) }
var showRestartDialog by remember { mutableStateOf(false) }
var showColorPicker by remember { mutableStateOf(false) }
var showLauncherIconPicker by remember { mutableStateOf(false) }
@@ -202,6 +203,37 @@ fun ThemeConfigScreen(
OldThemeConfig.applyDayNight(context)
}
)
SwitchSettingItem(
title = stringResource(R.string.miuix_monet),
description = stringResource(R.string.miuix_monet_summary),
checked = useMiuixMonet,
onCheckedChange = {
useMiuixMonet = it
ThemeConfig.useMiuixMonet = it
if (it && selectedTheme != "0" && selectedTheme != "12") {
selectedTheme = "0"
ThemeConfig.appTheme = "0"
}
}
)
if (useMiuixMonet) {
SwitchSettingItem(
title = stringResource(R.string.dynamic_colors),
description = stringResource(R.string.dynamic_colors_summary),
checked = selectedTheme == "0",
onCheckedChange = { checked ->
val newTheme = if (checked) "0" else "12"
val oldTheme = selectedTheme
selectedTheme = newTheme
ThemeConfig.appTheme = newTheme
if (oldTheme != newTheme) {
showRestartDialog = true
}
}
)
}
} else {
ThemeModeSelector(
selectedMode = selectedThemeMode,
@@ -249,10 +281,14 @@ fun ThemeConfigScreen(
}
SplicedColumnGroup {
SwitchSettingItem(
title = stringResource(R.string.pure_black),
checked = ThemeConfig.isPureBlack,
onCheckedChange = { ThemeConfig.isPureBlack = it }
DropdownListSettingItem(
title = stringResource(R.string.compose_engine),
selectedValue = ThemeConfig.composeEngine,
displayEntries = stringArrayResource(R.array.composeEngine),
entryValues = stringArrayResource(R.array.composeEngine_value),
onValueChange = {
ThemeConfig.composeEngine = it
}
)
ClickableSettingItem(
title = stringResource(R.string.change_icon),
@@ -268,15 +304,6 @@ fun ThemeConfigScreen(
context.toastOnUi(R.string.restart_to_apply)
}
)
DropdownListSettingItem(
title = stringResource(R.string.compose_engine),
selectedValue = ThemeConfig.composeEngine,
displayEntries = stringArrayResource(R.array.composeEngine),
entryValues = stringArrayResource(R.array.composeEngine_value),
onValueChange = {
ThemeConfig.composeEngine = it
}
)
SliderSettingItem(
title = stringResource(R.string.font_scale),
description = stringResource(
@@ -294,7 +321,8 @@ fun ThemeConfigScreen(
)
}
if (selectedTheme == "12") {
val showCustomThemeOptions = selectedTheme == "12" && (!isMiuixEngine || useMiuixMonet)
if (showCustomThemeOptions) {
SplicedColumnGroup(title = stringResource(R.string.custom_theme)) {
ClickableSettingItem(
title = stringResource(R.string.seed_color),
@@ -426,11 +454,18 @@ fun ThemeConfigScreen(
}
SplicedColumnGroup(title = stringResource(R.string.compose_related)) {
SwitchSettingItem(
title = stringResource(R.string.use_flexible_top_bar),
checked = ThemeConfig.useFlexibleTopAppBar,
onCheckedChange = { ThemeConfig.useFlexibleTopAppBar = it }
)
if (!isMiuixEngine) {
SwitchSettingItem(
title = stringResource(R.string.pure_black),
checked = ThemeConfig.isPureBlack,
onCheckedChange = { ThemeConfig.isPureBlack = it }
)
SwitchSettingItem(
title = stringResource(R.string.use_flexible_top_bar),
checked = ThemeConfig.useFlexibleTopAppBar,
onCheckedChange = { ThemeConfig.useFlexibleTopAppBar = it }
)
}
SwitchSettingItem(
title = stringResource(R.string.is_blur_enable),
checked = ThemeConfig.enableBlur,
@@ -446,7 +481,7 @@ fun ThemeConfigScreen(
onCheckedChange = { ThemeConfig.enableProgressiveBlur = it }
)
}
AnimatedVisibility(visible = !ThemeConfig.enableBlur) {
AnimatedVisibility(visible = !isMiuixEngine && !ThemeConfig.enableBlur) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
@@ -476,18 +511,20 @@ fun ThemeConfigScreen(
)
}
}
SliderSettingItem(
title = stringResource(R.string.container_opacity),
description = stringResource(
R.string.container_opacity_summary,
ThemeConfig.containerOpacity
),
value = ThemeConfig.containerOpacity.toFloat(),
defaultValue = 100f,
valueRange = 0f..100f,
steps = 99,
onValueChange = { ThemeConfig.containerOpacity = it.toInt() }
)
if (!isMiuixEngine) {
SliderSettingItem(
title = stringResource(R.string.container_opacity),
description = stringResource(
R.string.container_opacity_summary,
ThemeConfig.containerOpacity
),
value = ThemeConfig.containerOpacity.toFloat(),
defaultValue = 100f,
valueRange = 0f..100f,
steps = 99,
onValueChange = { ThemeConfig.containerOpacity = it.toInt() }
)
}
}
SplicedColumnGroup(title = stringResource(R.string.day)) {
@@ -738,7 +775,7 @@ fun ThemeColorButton(
shape = RoundedCornerShape(16.dp),
border = if (isSelected) BorderStroke(
borderWidth,
MaterialTheme.colorScheme.primary
LegadoTheme.colorScheme.primary
) else null,
colors = CardDefaults.cardColors(containerColor = colors.surfaceContainer)
) {
@@ -801,7 +838,7 @@ fun ThemeColorButton(
AppText(
text = label,
style = LegadoTheme.typography.labelSmall,
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
color = if (isSelected) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.onSurface
)
}
}
@@ -915,7 +952,7 @@ private fun getThemeColorPalette(
materialVersion: String? = null
): ThemeColorPalette {
val appThemeMode = ThemeResolver.resolveThemeMode(value)
val colorScheme = ThemeManager.getColorScheme(
val colorScheme = ThemeEngine.getColorScheme(
context = context,
mode = appThemeMode,
darkTheme = isDark,
@@ -942,7 +979,7 @@ private fun getThemeColors(
materialVersion: String? = null
): ThemeColors {
val appThemeMode = ThemeResolver.resolveThemeMode(value)
val colorScheme = ThemeManager.getColorScheme(
val colorScheme = ThemeEngine.getColorScheme(
context = context,
mode = appThemeMode,
darkTheme = isDark,
@@ -38,6 +38,7 @@ import io.legado.app.help.config.OldThemeConfig
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.MediumIconButton
import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText
@@ -59,41 +60,27 @@ fun ThemeListDialog(
AppModalBottomSheet(
show = show,
onDismissRequest = onDismissRequest
onDismissRequest = onDismissRequest,
title = stringResource(R.string.theme_list),
endAction = {
MediumIconButton(
onClick = {
val clipText = context.getClipText()
if (clipText != null && OldThemeConfig.addConfig(clipText)) {
listVersion++
} else {
context.toastOnUi("Import failed")
}
},
imageVector = Icons.Default.FileDownload
)
}
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 8.dp, top = 8.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
AppText(
text = stringResource(R.string.theme_list),
style = LegadoTheme.typography.titleMedium,
modifier = Modifier.weight(1f)
)
IconButton(
onClick = {
val clipText = context.getClipText()
if (clipText != null && OldThemeConfig.addConfig(clipText)) {
listVersion++
} else {
context.toastOnUi("Import failed")
}
}
) {
Icon(
imageVector = Icons.Default.FileDownload,
contentDescription = stringResource(R.string.import_theme)
)
}
}
LazyColumn(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp)
@@ -104,8 +91,7 @@ fun ThemeListDialog(
OldThemeConfig.applyConfig(context, item)
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
containerColor = if (item.primaryColor.toColorInt() == context.primaryColor) {
MaterialTheme.colorScheme.secondaryContainer
@@ -0,0 +1,19 @@
package io.legado.app.ui.theme
import androidx.compose.material3.LocalContentColor as MaterialLocalContentColor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.graphics.Color
import top.yukonga.miuix.kmp.theme.LocalContentColor as MiuixLocalContentColor
@Composable
fun ProvideAppContentColor(
contentColor: Color,
content: @Composable () -> Unit
) {
CompositionLocalProvider(
MaterialLocalContentColor provides contentColor,
MiuixLocalContentColor provides contentColor,
content = content
)
}
@@ -1,5 +1,6 @@
package io.legado.app.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialExpressiveTheme
@@ -11,6 +12,8 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import io.legado.app.ui.config.themeConfig.ThemeConfig
import top.yukonga.miuix.kmp.theme.MiuixTheme
@@ -24,33 +27,56 @@ fun AppTheme(
) {
val context = LocalContext.current
val appThemeMode = ThemeResolver.resolveThemeMode(ThemeConfig.appTheme)
val themeModeValue = ThemeConfig.themeMode
val isPureBlack = ThemeConfig.isPureBlack
val paletteStyleStr = ThemeConfig.paletteStyle
val paletteStyleValue = ThemeConfig.paletteStyle
val materialVersion = ThemeConfig.materialVersion
val composeEngine = ThemeConfig.composeEngine
val colorSchemeMode = ThemeResolver.resolveColorSchemeMode(ThemeConfig.themeMode)
val paletteStyle =
remember(paletteStyleStr) { ThemeResolver.resolvePaletteStyle(paletteStyleStr) }
val seedColor = remember(ThemeConfig.cPrimary) {
if (ThemeConfig.cPrimary != 0) Color(ThemeConfig.cPrimary) else Color(0xFF3482FF)
val useMiuixMonet = ThemeConfig.useMiuixMonet
val customPrimary = ThemeConfig.cPrimary
val colorSchemeMode = ThemeResolver.resolveColorSchemeMode(themeModeValue)
val miuixColorSchemeMode = remember(themeModeValue, useMiuixMonet) {
ThemeResolver.resolveMiuixColorSchemeMode(themeModeValue, useMiuixMonet)
}
val paletteStyle =
remember(paletteStyleValue) { ThemeResolver.resolvePaletteStyle(paletteStyleValue) }
val colorScheme =
remember(context, appThemeMode, darkTheme, isPureBlack, paletteStyleStr, materialVersion) {
ThemeManager.getColorScheme(
remember(
context,
appThemeMode,
darkTheme,
isPureBlack,
paletteStyleValue,
materialVersion
) {
ThemeEngine.getColorScheme(
context = context,
mode = appThemeMode,
darkTheme = darkTheme,
isAmoled = isPureBlack,
paletteStyle = paletteStyleStr,
paletteStyle = paletteStyleValue,
materialVersion = materialVersion
)
}
val customSeedColor = remember(customPrimary, colorScheme.primary) {
if (customPrimary != 0) Color(customPrimary) else colorScheme.primary
}
val themeSeedColor = remember(appThemeMode, customSeedColor, colorScheme.primary) {
if (appThemeMode == AppThemeMode.Custom) customSeedColor else colorScheme.primary
}
val miuixPaletteStyle = remember(paletteStyleValue) {
ThemeResolver.resolveMiuixPaletteStyle(paletteStyleValue)
}
val miuixColorSpec = remember(materialVersion, paletteStyleValue) {
ThemeResolver.resolveMiuixColorSpec(materialVersion, paletteStyleValue)
}
val themeColors = remember(
colorScheme,
darkTheme,
seedColor,
themeSeedColor,
paletteStyle,
colorSchemeMode,
composeEngine
@@ -58,7 +84,7 @@ fun AppTheme(
LegadoThemeMode(
colorScheme = colorScheme,
isDark = darkTheme,
seedColor = seedColor,
seedColor = themeSeedColor,
paletteStyle = paletteStyle,
themeMode = colorSchemeMode,
useDynamicColor = appThemeMode == AppThemeMode.Dynamic,
@@ -70,8 +96,37 @@ fun AppTheme(
LocalLegadoThemeColors provides themeColors
) {
if (ThemeResolver.isMiuixEngine(themeColors.composeEngine)) {
val controller = remember(colorSchemeMode, darkTheme) {
ThemeController(colorSchemeMode = colorSchemeMode, isDark = darkTheme)
val keyColor = if (useMiuixMonet &&
themeColors.useDynamicColor &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
) {
colorResource(id = android.R.color.system_accent1_500)
} else {
themeSeedColor
}
val controller = remember(
miuixColorSchemeMode,
useMiuixMonet,
keyColor,
miuixPaletteStyle,
miuixColorSpec,
darkTheme
) {
if (useMiuixMonet) {
ThemeController(
colorSchemeMode = miuixColorSchemeMode,
keyColor = keyColor,
paletteStyle = miuixPaletteStyle,
colorSpec = miuixColorSpec,
isDark = darkTheme
)
} else {
ThemeController(
colorSchemeMode = miuixColorSchemeMode,
isDark = darkTheme
)
}
}
MiuixTheme(controller = controller) {
@@ -79,34 +134,34 @@ fun AppTheme(
val legadoTypography = remember(miuixStyles) {
LegadoTypography(
headlineLarge = miuixStyles.title1,
headlineLargeEmphasized = miuixStyles.title1.copy(fontWeight = FontWeight.Medium),
headlineLargeEmphasized = miuixStyles.title1.emphasized(),
headlineMedium = miuixStyles.title2,
headlineMediumEmphasized = miuixStyles.title2.copy(fontWeight = FontWeight.Medium),
headlineMediumEmphasized = miuixStyles.title2.emphasized(),
headlineSmall = miuixStyles.title3,
headlineSmallEmphasized = miuixStyles.title3.copy(fontWeight = FontWeight.Medium),
headlineSmallEmphasized = miuixStyles.title3.emphasized(),
titleLarge = miuixStyles.headline1,
titleLargeEmphasized = miuixStyles.headline1.copy(fontWeight = FontWeight.Medium),
titleLargeEmphasized = miuixStyles.headline1.emphasized(),
titleMedium = miuixStyles.headline2,
titleMediumEmphasized = miuixStyles.headline2.copy(fontWeight = FontWeight.Medium),
titleMediumEmphasized = miuixStyles.headline2.emphasized(),
titleSmall = miuixStyles.subtitle,
titleSmallEmphasized = miuixStyles.subtitle.copy(fontWeight = FontWeight.Medium),
titleSmallEmphasized = miuixStyles.subtitle.emphasized(),
bodyLarge = miuixStyles.paragraph,
bodyLargeEmphasized = miuixStyles.paragraph.copy(fontWeight = FontWeight.Medium),
bodyLargeEmphasized = miuixStyles.paragraph.emphasized(),
bodyMedium = miuixStyles.body1,
bodyMediumEmphasized = miuixStyles.body1.copy(fontWeight = FontWeight.Medium),
bodyMediumEmphasized = miuixStyles.body1.emphasized(),
bodySmall = miuixStyles.body2,
bodySmallEmphasized = miuixStyles.body2.copy(fontWeight = FontWeight.Medium),
bodySmallEmphasized = miuixStyles.body2.emphasized(),
labelLarge = miuixStyles.button,
labelLargeEmphasized = miuixStyles.button.copy(fontWeight = FontWeight.Medium),
labelLargeEmphasized = miuixStyles.button.emphasized(),
labelMedium = miuixStyles.footnote1,
labelMediumEmphasized = miuixStyles.footnote1.copy(fontWeight = FontWeight.Medium),
labelMediumEmphasized = miuixStyles.footnote1.emphasized(),
labelSmall = miuixStyles.footnote2,
labelSmallEmphasized = miuixStyles.footnote2.copy(fontWeight = FontWeight.Medium)
labelSmallEmphasized = miuixStyles.footnote2.emphasized()
)
}
@@ -130,39 +185,28 @@ fun AppTheme(
tertiaryContainer = miuixColorScheme.primaryContainer,
onTertiaryContainer = miuixColorScheme.primaryVariant,
// ================= 4. 背景与表面 (Background & Surface) =================
background = miuixColorScheme.background,
onBackground = miuixColorScheme.onBackground,
surface = miuixColorScheme.surface,
onSurface = miuixColorScheme.onSurface,
surfaceVariant = miuixColorScheme.surfaceVariant,
// M3 的 onSurfaceVariant 通常是次级文字色。Miuix 的 onSurfaceSecondary 完美契合这个语义
onSurfaceVariant = miuixColorScheme.onSurfaceSecondary,
// M3 中用于给 Surface 叠加一层极淡主题色的属性,通常直接取 primary
surfaceTint = miuixColorScheme.primary,
// Inverse 系列通常用于深色模式下的反色提示(如 Snackbar)。
// 简单映射法:直接用现有的 onSurface 和 surface 交叉互换。
inverseSurface = miuixColorScheme.onSurface,
inverseOnSurface = miuixColorScheme.surface,
// ================= 5. 错误状态 (Error) =================
error = miuixColorScheme.error,
onError = miuixColorScheme.onError,
errorContainer = miuixColorScheme.errorContainer,
onErrorContainer = miuixColorScheme.onErrorContainer,
// ================= 6. 边框、分割线与遮罩 (Outline & Scrim) =================
outline = miuixColorScheme.outline,
// outlineVariant 在 M3 中常用于分割线。Miuix 刚好有 dividerLine
outlineVariant = miuixColorScheme.dividerLine,
// scrim 是 M3 的遮罩层(如弹窗背后的阴影)。Miuix 刚好有 windowDimming
scrim = miuixColorScheme.windowDimming,
surfaceBright = miuixColorScheme.surface, // Miuix 缺省,用 surface 兜底
surfaceDim = miuixColorScheme.background, // Miuix 缺省,用 background 兜底
surfaceBright = miuixColorScheme.surface,
surfaceDim = miuixColorScheme.background,
surfaceContainer = miuixColorScheme.surfaceContainer,
surfaceContainerHigh = miuixColorScheme.surfaceContainerHigh,
surfaceContainerHighest = miuixColorScheme.surfaceContainerHighest,
@@ -182,7 +226,7 @@ fun AppTheme(
onTertiaryFixed = miuixColorScheme.onTertiaryContainer,
onTertiaryFixedVariant = miuixColorScheme.onTertiaryContainer,
cardContainer = miuixColorScheme.tertiaryContainer,
cardContainer = miuixColorScheme.disabledPrimary,
onCardContainer = miuixColorScheme.primary
)
}
@@ -195,102 +239,17 @@ fun AppTheme(
}
}
} else {
val Typography = Typography()
val materialTypography = remember { Typography() }
MaterialExpressiveTheme(
colorScheme = colorScheme,
typography = Typography,
typography = materialTypography,
motionScheme = MotionScheme.expressive(),
shapes = Shapes()
) {
val legadoTypography = remember(Typography) {
LegadoTypography(
headlineLarge = Typography.headlineLarge,
headlineLargeEmphasized = Typography.headlineLargeEmphasized,
headlineMedium = Typography.headlineMedium,
headlineMediumEmphasized = Typography.headlineMediumEmphasized,
headlineSmall = Typography.headlineSmall,
headlineSmallEmphasized = Typography.headlineSmallEmphasized,
titleLarge = Typography.titleLarge,
titleLargeEmphasized = Typography.titleLargeEmphasized,
titleMedium = Typography.titleMedium,
titleMediumEmphasized = Typography.titleMediumEmphasized,
titleSmall = Typography.titleSmall,
titleSmallEmphasized = Typography.titleSmallEmphasized,
bodyLarge = Typography.bodyLarge,
bodyLargeEmphasized = Typography.bodyLargeEmphasized,
bodyMedium = Typography.bodyMedium,
bodyMediumEmphasized = Typography.bodyMediumEmphasized,
bodySmall = Typography.bodySmall,
bodySmallEmphasized = Typography.bodySmallEmphasized,
labelLarge = Typography.labelLarge,
labelLargeEmphasized = Typography.labelLargeEmphasized,
labelMedium = Typography.labelMedium,
labelMediumEmphasized = Typography.labelMediumEmphasized,
labelSmall = Typography.labelSmall,
labelSmallEmphasized = Typography.labelSmallEmphasized
)
}
val semanticColors = remember(colorScheme) {
LegadoColorScheme(
primary = colorScheme.primary,
onPrimary = colorScheme.onPrimary,
primaryContainer = colorScheme.primaryContainer,
onPrimaryContainer = colorScheme.onPrimaryContainer,
inversePrimary = colorScheme.inversePrimary,
secondary = colorScheme.secondary,
onSecondary = colorScheme.onSecondary,
secondaryContainer = colorScheme.secondaryContainer,
onSecondaryContainer = colorScheme.onSecondaryContainer,
tertiary = colorScheme.tertiary,
onTertiary = colorScheme.onTertiary,
tertiaryContainer = colorScheme.tertiaryContainer,
onTertiaryContainer = colorScheme.onTertiaryContainer,
background = colorScheme.background,
onBackground = colorScheme.onBackground,
surface = colorScheme.surface,
onSurface = colorScheme.onSurface,
surfaceVariant = colorScheme.surfaceVariant,
onSurfaceVariant = colorScheme.onSurfaceVariant,
surfaceTint = colorScheme.surfaceTint,
inverseSurface = colorScheme.inverseSurface,
inverseOnSurface = colorScheme.inverseOnSurface,
error = colorScheme.error,
onError = colorScheme.onError,
errorContainer = colorScheme.errorContainer,
onErrorContainer = colorScheme.onErrorContainer,
outline = colorScheme.outline,
outlineVariant = colorScheme.outlineVariant,
scrim = colorScheme.scrim,
surfaceBright = colorScheme.surfaceBright,
surfaceDim = colorScheme.surfaceDim,
surfaceContainer = colorScheme.surfaceContainer,
surfaceContainerHigh = colorScheme.surfaceContainerHigh,
surfaceContainerHighest = colorScheme.surfaceContainerHighest,
surfaceContainerLow = colorScheme.surfaceContainerLow,
surfaceContainerLowest = colorScheme.surfaceContainerLowest,
primaryFixed = colorScheme.primaryFixed,
primaryFixedDim = colorScheme.primaryFixedDim,
onPrimaryFixed = colorScheme.onPrimaryFixed,
onPrimaryFixedVariant = colorScheme.onPrimaryFixedVariant,
secondaryFixed = colorScheme.secondaryFixed,
secondaryFixedDim = colorScheme.secondaryFixedDim,
onSecondaryFixed = colorScheme.onSecondaryFixed,
onSecondaryFixedVariant = colorScheme.onSecondaryFixedVariant,
tertiaryFixed = colorScheme.tertiaryFixed,
tertiaryFixedDim = colorScheme.tertiaryFixedDim,
onTertiaryFixed = colorScheme.onTertiaryFixed,
onTertiaryFixedVariant = colorScheme.onTertiaryFixedVariant,
cardContainer = colorScheme.primaryContainer.copy(alpha = 0.5f),
onCardContainer = colorScheme.primary
)
val legadoTypography = remember(materialTypography) {
materialTypography.toLegadoTypography()
}
val semanticColors = remember(colorScheme) { colorScheme.toLegadoColorScheme() }
CompositionLocalProvider(
LocalLegadoTypography provides legadoTypography,
@@ -302,3 +261,36 @@ fun AppTheme(
}
}
}
private fun Typography.toLegadoTypography(): LegadoTypography {
return LegadoTypography(
headlineLarge = headlineLarge,
headlineLargeEmphasized = headlineLargeEmphasized,
headlineMedium = headlineMedium,
headlineMediumEmphasized = headlineMediumEmphasized,
headlineSmall = headlineSmall,
headlineSmallEmphasized = headlineSmallEmphasized,
titleLarge = titleLarge,
titleLargeEmphasized = titleLargeEmphasized,
titleMedium = titleMedium,
titleMediumEmphasized = titleMediumEmphasized,
titleSmall = titleSmall,
titleSmallEmphasized = titleSmallEmphasized,
bodyLarge = bodyLarge,
bodyLargeEmphasized = bodyLargeEmphasized,
bodyMedium = bodyMedium,
bodyMediumEmphasized = bodyMediumEmphasized,
bodySmall = bodySmall,
bodySmallEmphasized = bodySmallEmphasized,
labelLarge = labelLarge,
labelLargeEmphasized = labelLargeEmphasized,
labelMedium = labelMedium,
labelMediumEmphasized = labelMediumEmphasized,
labelSmall = labelSmall,
labelSmallEmphasized = labelSmallEmphasized
)
}
private fun TextStyle.emphasized(): TextStyle {
return copy(fontWeight = FontWeight.Medium)
}
@@ -13,7 +13,7 @@ enum class AppThemeMode {
Yuuka,
Phoebe,
Mujika,
CUSTOM,
Custom,
Transparent
}
@@ -0,0 +1,137 @@
package io.legado.app.ui.theme
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.core.graphics.drawable.toBitmap
import androidx.core.graphics.scale
import coil.ImageLoader
import coil.request.ImageRequest
import coil.request.SuccessResult
import com.materialkolor.quantize.QuantizerCelebi
import com.materialkolor.score.Score
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
private const val IMAGE_COLOR_EXTRACT_SIZE_PX = 256
private const val IMAGE_QUANTIZE_BITMAP_MAX_SIZE = 128
private const val IMAGE_MAX_QUANTIZE_COLORS = 128
private const val IMAGE_FALLBACK_SEED_COLOR = 0xFF4285F4.toInt()
suspend fun ImageLoader.extractSeedColor(
context: Context,
data: Any,
configureRequest: ImageRequest.Builder.() -> Unit = {},
): Color? {
val request = ImageRequest.Builder(context)
.data(data)
.allowHardware(false)
.apply(configureRequest)
.build()
val result = withContext(Dispatchers.IO) {
execute(request)
} as? SuccessResult ?: return null
return withContext(Dispatchers.Default) {
val bitmap = result.drawable.toSafeBitmap(IMAGE_COLOR_EXTRACT_SIZE_PX)
Color(bitmap.extractSeedColor())
}
}
@Composable
fun rememberImageSeedColor(
imageLoader: ImageLoader,
data: Any?,
requestKey: Any? = data,
configureRequest: ImageRequest.Builder.() -> Unit = {},
): Color? {
val context = LocalContext.current
val seedColor by produceState<Color?>(initialValue = null, imageLoader, requestKey) {
value = if (data == null) {
null
} else {
imageLoader.extractSeedColor(
context = context,
data = data,
configureRequest = configureRequest
)
}
}
return seedColor
}
internal fun Bitmap.extractSeedColor(
maxColors: Int = IMAGE_MAX_QUANTIZE_COLORS,
fallbackColorArgb: Int = IMAGE_FALLBACK_SEED_COLOR,
): Int {
val needsScaling =
width > IMAGE_QUANTIZE_BITMAP_MAX_SIZE || height > IMAGE_QUANTIZE_BITMAP_MAX_SIZE
val scaledBitmap = if (needsScaling) {
val scale = minOf(
IMAGE_QUANTIZE_BITMAP_MAX_SIZE.toFloat() / width,
IMAGE_QUANTIZE_BITMAP_MAX_SIZE.toFloat() / height
)
scale(
(width * scale).toInt().coerceAtLeast(1),
(height * scale).toInt().coerceAtLeast(1)
)
} else {
this
}
return try {
val pixels = IntArray(scaledBitmap.width * scaledBitmap.height)
scaledBitmap.getPixels(
pixels,
0,
scaledBitmap.width,
0,
0,
scaledBitmap.width,
scaledBitmap.height
)
val quantized = QuantizerCelebi.quantize(pixels, maxColors)
Score.score(quantized, 1, fallbackColorArgb, true).first()
} finally {
if (scaledBitmap !== this) {
scaledBitmap.recycle()
}
}
}
internal fun Drawable.toSafeBitmap(maxSizePx: Int): Bitmap {
if (this is BitmapDrawable && bitmap != null) {
val rawBitmap = bitmap
if (rawBitmap.width <= maxSizePx && rawBitmap.height <= maxSizePx) {
return rawBitmap
}
val scale = minOf(
1f,
maxSizePx.toFloat() / rawBitmap.width,
maxSizePx.toFloat() / rawBitmap.height
)
return rawBitmap.scale(
(rawBitmap.width * scale).toInt().coerceAtLeast(1),
(rawBitmap.height * scale).toInt().coerceAtLeast(1)
)
}
val rawWidth = intrinsicWidth.takeIf { it > 0 } ?: maxSizePx
val rawHeight = intrinsicHeight.takeIf { it > 0 } ?: maxSizePx
val scale = minOf(1f, maxSizePx.toFloat() / rawWidth, maxSizePx.toFloat() / rawHeight)
return toBitmap(
width = (rawWidth * scale).toInt().coerceAtLeast(1),
height = (rawHeight * scale).toInt().coerceAtLeast(1)
)
}
@@ -6,7 +6,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import io.legado.app.ui.config.themeConfig.ThemeConfig
import io.legado.app.ui.theme.ThemeManager.getColorScheme
import io.legado.app.ui.theme.ThemeEngine.getColorScheme
@Composable
fun rememberOpaqueColorScheme(): ColorScheme {
@@ -27,4 +27,4 @@ fun rememberOpaqueColorScheme(): ColorScheme {
forceOpaque = true
)
}
}
}
@@ -0,0 +1,242 @@
package io.legado.app.ui.theme
import androidx.compose.animation.animateColor
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import io.legado.app.ui.config.themeConfig.ThemeConfig
import top.yukonga.miuix.kmp.theme.ColorSchemeMode
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.theme.ThemeController
fun ColorScheme.toLegadoColorScheme(): LegadoColorScheme {
return LegadoColorScheme(
primary = primary,
onPrimary = onPrimary,
primaryContainer = primaryContainer,
onPrimaryContainer = onPrimaryContainer,
inversePrimary = inversePrimary,
secondary = secondary,
onSecondary = onSecondary,
secondaryContainer = secondaryContainer,
onSecondaryContainer = onSecondaryContainer,
tertiary = tertiary,
onTertiary = onTertiary,
tertiaryContainer = tertiaryContainer,
onTertiaryContainer = onTertiaryContainer,
background = background,
onBackground = onBackground,
surface = surface,
onSurface = onSurface,
surfaceVariant = surfaceVariant,
onSurfaceVariant = onSurfaceVariant,
surfaceTint = surfaceTint,
inverseSurface = inverseSurface,
inverseOnSurface = inverseOnSurface,
error = error,
onError = onError,
errorContainer = errorContainer,
onErrorContainer = onErrorContainer,
outline = outline,
outlineVariant = outlineVariant,
scrim = scrim,
surfaceBright = surfaceBright,
surfaceDim = surfaceDim,
surfaceContainer = surfaceContainer,
surfaceContainerHigh = surfaceContainerHigh,
surfaceContainerHighest = surfaceContainerHighest,
surfaceContainerLow = surfaceContainerLow,
surfaceContainerLowest = surfaceContainerLowest,
primaryFixed = primaryFixed,
primaryFixedDim = primaryFixedDim,
onPrimaryFixed = onPrimaryFixed,
onPrimaryFixedVariant = onPrimaryFixedVariant,
secondaryFixed = secondaryFixed,
secondaryFixedDim = secondaryFixedDim,
onSecondaryFixed = onSecondaryFixed,
onSecondaryFixedVariant = onSecondaryFixedVariant,
tertiaryFixed = tertiaryFixed,
tertiaryFixedDim = tertiaryFixedDim,
onTertiaryFixed = onTertiaryFixed,
onTertiaryFixedVariant = onTertiaryFixedVariant,
cardContainer = primaryContainer.copy(alpha = 0.5f),
onCardContainer = primary
)
}
@Composable
fun ProvideColorSchemeOverride(
colorScheme: ColorScheme,
seedColor: Color = colorScheme.primary,
content: @Composable () -> Unit,
) {
val themeAnimationSpec = tween<Color>(
durationMillis = 700,
easing = FastOutSlowInEasing
)
val baseThemeMode = LocalLegadoThemeColors.current
val animatedColorScheme = colorScheme.animateColorSchemeAsState(themeAnimationSpec)
val animatedSeedColor = animateColorAsState(
targetValue = seedColor,
animationSpec = themeAnimationSpec,
label = "theme_seed_animation"
).value
val legadoColorScheme = remember(animatedColorScheme) { animatedColorScheme.toLegadoColorScheme() }
val overrideThemeMode = remember(baseThemeMode, animatedColorScheme, animatedSeedColor) {
baseThemeMode.copy(
colorScheme = animatedColorScheme,
seedColor = animatedSeedColor,
)
}
val materialTypography = MaterialTheme.typography
val materialShapes = MaterialTheme.shapes
val isMiuixEngine = ThemeResolver.isMiuixEngine(overrideThemeMode.composeEngine)
val miuixColorSchemeMode = remember(overrideThemeMode.themeMode) {
overrideThemeMode.themeMode.toMiuixMonetMode()
}
val miuixPaletteStyle = remember(ThemeConfig.paletteStyle) {
ThemeResolver.resolveMiuixPaletteStyle(ThemeConfig.paletteStyle)
}
val miuixColorSpec = remember(ThemeConfig.materialVersion, ThemeConfig.paletteStyle) {
ThemeResolver.resolveMiuixColorSpec(ThemeConfig.materialVersion, ThemeConfig.paletteStyle)
}
val miuixController = remember(
isMiuixEngine,
miuixColorSchemeMode,
overrideThemeMode.isDark,
animatedSeedColor,
miuixPaletteStyle,
miuixColorSpec
) {
if (!isMiuixEngine) {
null
} else {
ThemeController(
colorSchemeMode = miuixColorSchemeMode,
keyColor = animatedSeedColor,
paletteStyle = miuixPaletteStyle,
colorSpec = miuixColorSpec,
isDark = overrideThemeMode.isDark
)
}
}
CompositionLocalProvider(
LocalLegadoThemeColors provides overrideThemeMode,
LocalLegadoColorScheme provides legadoColorScheme
) {
if (miuixController != null) {
MiuixTheme(controller = miuixController) {
MaterialTheme(
colorScheme = animatedColorScheme,
typography = materialTypography,
shapes = materialShapes
) {
content()
}
}
} else {
MaterialTheme(
colorScheme = animatedColorScheme,
typography = materialTypography,
shapes = materialShapes
) {
content()
}
}
}
}
private fun ColorSchemeMode.toMiuixMonetMode(): ColorSchemeMode {
return when (this) {
ColorSchemeMode.Light,
ColorSchemeMode.MonetLight -> ColorSchemeMode.MonetLight
ColorSchemeMode.Dark,
ColorSchemeMode.MonetDark -> ColorSchemeMode.MonetDark
else -> ColorSchemeMode.MonetSystem
}
}
@Composable
fun ColorScheme.animateColorSchemeAsState(
animationSpec: FiniteAnimationSpec<Color> = tween(
durationMillis = 700,
easing = FastOutSlowInEasing
)
): ColorScheme {
val transition = updateTransition(
targetState = this,
label = "theme_color_scheme_transition"
)
@Composable
fun animateColor(label: String, color: ColorScheme.() -> Color): Color {
return transition.animateColor(
transitionSpec = { animationSpec },
label = label
) { scheme ->
scheme.color()
}.value
}
return ColorScheme(
primary = animateColor("scheme-primary") { primary },
onPrimary = animateColor("scheme-onPrimary") { onPrimary },
primaryContainer = animateColor("scheme-primaryContainer") { primaryContainer },
onPrimaryContainer = animateColor("scheme-onPrimaryContainer") { onPrimaryContainer },
inversePrimary = animateColor("scheme-inversePrimary") { inversePrimary },
secondary = animateColor("scheme-secondary") { secondary },
onSecondary = animateColor("scheme-onSecondary") { onSecondary },
secondaryContainer = animateColor("scheme-secondaryContainer") { secondaryContainer },
onSecondaryContainer = animateColor("scheme-onSecondaryContainer") { onSecondaryContainer },
tertiary = animateColor("scheme-tertiary") { tertiary },
onTertiary = animateColor("scheme-onTertiary") { onTertiary },
tertiaryContainer = animateColor("scheme-tertiaryContainer") { tertiaryContainer },
onTertiaryContainer = animateColor("scheme-onTertiaryContainer") { onTertiaryContainer },
background = animateColor("scheme-background") { background },
onBackground = animateColor("scheme-onBackground") { onBackground },
surface = animateColor("scheme-surface") { surface },
onSurface = animateColor("scheme-onSurface") { onSurface },
surfaceVariant = animateColor("scheme-surfaceVariant") { surfaceVariant },
onSurfaceVariant = animateColor("scheme-onSurfaceVariant") { onSurfaceVariant },
surfaceTint = animateColor("scheme-surfaceTint") { surfaceTint },
inverseSurface = animateColor("scheme-inverseSurface") { inverseSurface },
inverseOnSurface = animateColor("scheme-inverseOnSurface") { inverseOnSurface },
error = animateColor("scheme-error") { error },
onError = animateColor("scheme-onError") { onError },
errorContainer = animateColor("scheme-errorContainer") { errorContainer },
onErrorContainer = animateColor("scheme-onErrorContainer") { onErrorContainer },
outline = animateColor("scheme-outline") { outline },
outlineVariant = animateColor("scheme-outlineVariant") { outlineVariant },
scrim = animateColor("scheme-scrim") { scrim },
surfaceBright = animateColor("scheme-surfaceBright") { surfaceBright },
surfaceDim = animateColor("scheme-surfaceDim") { surfaceDim },
surfaceContainer = animateColor("scheme-surfaceContainer") { surfaceContainer },
surfaceContainerHigh = animateColor("scheme-surfaceContainerHigh") { surfaceContainerHigh },
surfaceContainerHighest = animateColor("scheme-surfaceContainerHighest") { surfaceContainerHighest },
surfaceContainerLow = animateColor("scheme-surfaceContainerLow") { surfaceContainerLow },
surfaceContainerLowest = animateColor("scheme-surfaceContainerLowest") { surfaceContainerLowest },
primaryFixed = animateColor("scheme-primaryFixed") { primaryFixed },
primaryFixedDim = animateColor("scheme-primaryFixedDim") { primaryFixedDim },
onPrimaryFixed = animateColor("scheme-onPrimaryFixed") { onPrimaryFixed },
onPrimaryFixedVariant = animateColor("scheme-onPrimaryFixedVariant") { onPrimaryFixedVariant },
secondaryFixed = animateColor("scheme-secondaryFixed") { secondaryFixed },
secondaryFixedDim = animateColor("scheme-secondaryFixedDim") { secondaryFixedDim },
onSecondaryFixed = animateColor("scheme-onSecondaryFixed") { onSecondaryFixed },
onSecondaryFixedVariant = animateColor("scheme-onSecondaryFixedVariant") { onSecondaryFixedVariant },
tertiaryFixed = animateColor("scheme-tertiaryFixed") { tertiaryFixed },
tertiaryFixedDim = animateColor("scheme-tertiaryFixedDim") { tertiaryFixedDim },
onTertiaryFixed = animateColor("scheme-onTertiaryFixed") { onTertiaryFixed },
onTertiaryFixedVariant = animateColor("scheme-onTertiaryFixedVariant") { onTertiaryFixedVariant }
)
}
@@ -0,0 +1,142 @@
package io.legado.app.ui.theme
import android.content.Context
import android.os.Build
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.ui.graphics.Color
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.theme.ThemeResolver.resolvePaletteStyle
import io.legado.app.ui.theme.colorScheme.AugustColorScheme
import io.legado.app.ui.theme.colorScheme.CarlottaColorScheme
import io.legado.app.ui.theme.colorScheme.ElinkColorScheme
import io.legado.app.ui.theme.colorScheme.GRColorScheme
import io.legado.app.ui.theme.colorScheme.KoharuColorScheme
import io.legado.app.ui.theme.colorScheme.LemonColorScheme
import io.legado.app.ui.theme.colorScheme.MujikaColorScheme
import io.legado.app.ui.theme.colorScheme.PhoebeColorScheme
import io.legado.app.ui.theme.colorScheme.SoraColorScheme
import io.legado.app.ui.theme.colorScheme.TransparentColorScheme
import io.legado.app.ui.theme.colorScheme.WHColorScheme
import io.legado.app.ui.theme.colorScheme.YuukaColorScheme
object ThemeEngine {
private val predefinedColorSchemes: Map<AppThemeMode, BaseColorScheme> = mapOf(
AppThemeMode.GR to GRColorScheme,
AppThemeMode.Lemon to LemonColorScheme,
AppThemeMode.WH to WHColorScheme,
AppThemeMode.Elink to ElinkColorScheme,
AppThemeMode.Sora to SoraColorScheme,
AppThemeMode.August to AugustColorScheme,
AppThemeMode.Carlotta to CarlottaColorScheme,
AppThemeMode.Koharu to KoharuColorScheme,
AppThemeMode.Yuuka to YuukaColorScheme,
AppThemeMode.Phoebe to PhoebeColorScheme,
AppThemeMode.Mujika to MujikaColorScheme,
AppThemeMode.Transparent to TransparentColorScheme,
)
fun getColorScheme(
context: Context,
mode: AppThemeMode,
darkTheme: Boolean,
isAmoled: Boolean,
paletteStyle: String?,
materialVersion: String? = null,
forceOpaque: Boolean = false
): ColorScheme {
val resolvedMode = resolveMode(mode = mode, forceOpaque = forceOpaque)
val baseColorScheme = resolveBaseColorScheme(
context = context,
mode = resolvedMode,
darkTheme = darkTheme,
paletteStyle = paletteStyle,
materialVersion = materialVersion
)
return baseColorScheme
.applyAmoledIfNeeded(darkTheme = darkTheme, isAmoled = isAmoled)
.applyTransparentIfNeeded(mode = resolvedMode, forceOpaque = forceOpaque)
}
private fun resolveMode(
mode: AppThemeMode,
forceOpaque: Boolean
): AppThemeMode {
return if (forceOpaque && mode == AppThemeMode.Transparent) {
AppThemeMode.WH
} else {
mode
}
}
private fun resolveBaseColorScheme(
context: Context,
mode: AppThemeMode,
darkTheme: Boolean,
paletteStyle: String?,
materialVersion: String?
): ColorScheme {
if (mode == AppThemeMode.Dynamic) {
return resolveDynamicColorScheme(context = context, darkTheme = darkTheme)
}
if (mode == AppThemeMode.Custom) {
return resolveCustomColorScheme(
seedColor = context.primaryColor,
darkTheme = darkTheme,
paletteStyle = paletteStyle,
materialVersion = materialVersion
)
}
return (predefinedColorSchemes[mode] ?: GRColorScheme).getColorScheme(darkTheme)
}
private fun resolveDynamicColorScheme(
context: Context,
darkTheme: Boolean
): ColorScheme {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return GRColorScheme.getColorScheme(darkTheme)
}
return if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
private fun resolveCustomColorScheme(
seedColor: Int,
darkTheme: Boolean,
paletteStyle: String?,
materialVersion: String?
): ColorScheme {
val style = resolvePaletteStyle(paletteStyle)
val colorSpec = ThemeResolver.resolveColorSpecFromMaterialVersion(materialVersion)
return CustomColorScheme(seedColor, style, colorSpec).getColorScheme(darkTheme)
}
private fun ColorScheme.applyAmoledIfNeeded(
darkTheme: Boolean,
isAmoled: Boolean
): ColorScheme {
if (!darkTheme || !isAmoled) return this
return copy(
surface = Color.Black,
background = Color.Black,
surfaceContainerLow = Color(0xFF0A0A0A),
surfaceContainer = Color(0xFF121212)
)
}
private fun ColorScheme.applyTransparentIfNeeded(
mode: AppThemeMode,
forceOpaque: Boolean
): ColorScheme {
if (forceOpaque || mode != AppThemeMode.Transparent) return this
return copy(
surface = Color.Transparent,
background = Color.Transparent,
surfaceContainerLow = Color.Transparent,
surfaceContainer = Color.Transparent,
)
}
}
@@ -1,97 +0,0 @@
package io.legado.app.ui.theme
import android.content.Context
import android.os.Build
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.ui.graphics.Color
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.theme.ThemeResolver.resolvePaletteStyle
import io.legado.app.ui.theme.colorScheme.AugustColorScheme
import io.legado.app.ui.theme.colorScheme.CarlottaColorScheme
import io.legado.app.ui.theme.colorScheme.ElinkColorScheme
import io.legado.app.ui.theme.colorScheme.GRColorScheme
import io.legado.app.ui.theme.colorScheme.KoharuColorScheme
import io.legado.app.ui.theme.colorScheme.LemonColorScheme
import io.legado.app.ui.theme.colorScheme.MujikaColorScheme
import io.legado.app.ui.theme.colorScheme.PhoebeColorScheme
import io.legado.app.ui.theme.colorScheme.SoraColorScheme
import io.legado.app.ui.theme.colorScheme.TransparentColorScheme
import io.legado.app.ui.theme.colorScheme.WHColorScheme
import io.legado.app.ui.theme.colorScheme.YuukaColorScheme
object ThemeManager {
val colorSchemes: Map<AppThemeMode, BaseColorScheme> = mapOf(
AppThemeMode.GR to GRColorScheme,
AppThemeMode.Lemon to LemonColorScheme,
AppThemeMode.WH to WHColorScheme,
AppThemeMode.Elink to ElinkColorScheme,
AppThemeMode.Sora to SoraColorScheme,
AppThemeMode.August to AugustColorScheme,
AppThemeMode.Carlotta to CarlottaColorScheme,
AppThemeMode.Koharu to KoharuColorScheme,
AppThemeMode.Yuuka to YuukaColorScheme,
AppThemeMode.Phoebe to PhoebeColorScheme,
AppThemeMode.Mujika to MujikaColorScheme,
AppThemeMode.Transparent to TransparentColorScheme,
)
fun getColorScheme(
context: Context,
mode: AppThemeMode,
darkTheme: Boolean,
isAmoled: Boolean,
paletteStyle: String?,
materialVersion: String? = null,
forceOpaque: Boolean = false
): ColorScheme {
val style = resolvePaletteStyle(paletteStyle)
val actualMode = if (forceOpaque && mode == AppThemeMode.Transparent) {
AppThemeMode.WH
} else {
mode
}
var scheme = when (actualMode) {
AppThemeMode.Dynamic -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(
context
)
} else GRColorScheme.getColorScheme(darkTheme)
}
AppThemeMode.CUSTOM -> {
val colorSpec = ThemeResolver.resolveColorSpecFromMaterialVersion(materialVersion)
CustomColorScheme(context.primaryColor, style, colorSpec)
.getColorScheme(darkTheme)
}
else -> (colorSchemes[actualMode] ?: GRColorScheme).getColorScheme(darkTheme)
}
if (darkTheme && isAmoled) {
scheme = scheme.copy(
surface = Color.Black,
background = Color.Black,
surfaceContainerLow = Color(0xFF0A0A0A),
surfaceContainer = Color(0xFF121212)
)
}
if (!forceOpaque && actualMode == AppThemeMode.Transparent) {
return scheme.copy(
surface = Color.Transparent,
background = Color.Transparent,
surfaceContainerLow = Color.Transparent,
surfaceContainer = Color.Transparent,
)
}
return scheme
}
}
@@ -0,0 +1,100 @@
package io.legado.app.ui.theme
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.graphics.Color
import com.materialkolor.PaletteStyle
import com.materialkolor.dynamicColorScheme
import io.legado.app.ui.config.themeConfig.ThemeConfig
data class ThemeOverrideState(
val seedColor: Color,
val colorScheme: ColorScheme,
)
fun buildThemeOverrideState(
seedColor: Color,
isDark: Boolean,
paletteStyle: PaletteStyle,
colorSpec: ThemeColorSpec,
usePureBlack: Boolean,
): ThemeOverrideState {
var colorScheme = dynamicColorScheme(
seedColor = seedColor,
isDark = isDark,
isAmoled = false,
style = paletteStyle,
contrastLevel = ThemeResolver.resolveContrastLevel(),
specVersion = ThemeResolver.resolveColorSpecVersion(colorSpec)
)
if (isDark && usePureBlack) {
colorScheme = colorScheme.copy(
surface = Color.Black,
background = Color.Black,
surfaceContainerLow = Color(0xFF0A0A0A),
surfaceContainer = Color(0xFF121212)
)
}
return ThemeOverrideState(
seedColor = seedColor,
colorScheme = colorScheme
)
}
@Composable
fun ProvideThemeOverride(
theme: ThemeOverrideState?,
content: @Composable () -> Unit,
) {
var appliedTheme by remember { mutableStateOf<ThemeOverrideState?>(null) }
LaunchedEffect(theme) {
if (theme == null) {
appliedTheme = null
} else {
withFrameNanos { }
appliedTheme = theme
}
}
val currentTheme = appliedTheme
if (currentTheme == null) {
content()
} else {
ProvideColorSchemeOverride(
colorScheme = currentTheme.colorScheme,
seedColor = currentTheme.seedColor,
content = content
)
}
}
@Composable
fun rememberThemeOverride(
seedColor: Color?,
): ThemeOverrideState? {
val isDark = LegadoTheme.isDark
val paletteStyle = LegadoTheme.paletteStyle
val colorSpec = ThemeResolver.resolveColorSpecFromMaterialVersion(ThemeConfig.materialVersion)
val usePureBlack = ThemeConfig.isPureBlack
return remember(seedColor, isDark, paletteStyle, colorSpec, usePureBlack) {
seedColor?.let { color ->
buildThemeOverrideState(
seedColor = color,
isDark = isDark,
paletteStyle = paletteStyle,
colorSpec = colorSpec,
usePureBlack = usePureBlack
)
}
}
}
@@ -5,58 +5,100 @@ import com.materialkolor.PaletteStyle
import com.materialkolor.dynamiccolor.ColorSpec
import io.legado.app.ui.config.themeConfig.ThemeConfig
import top.yukonga.miuix.kmp.theme.ColorSchemeMode
import top.yukonga.miuix.kmp.theme.ThemeColorSpec as MiuixThemeColorSpec
import top.yukonga.miuix.kmp.theme.ThemePaletteStyle as MiuixPaletteStyle
object ThemeResolver {
fun resolveThemeMode(value: String): AppThemeMode = when (value) {
"0" -> AppThemeMode.Dynamic
"1" -> AppThemeMode.GR
"2" -> AppThemeMode.Lemon
"3" -> AppThemeMode.WH
"4" -> AppThemeMode.Elink
"5" -> AppThemeMode.Sora
"6" -> AppThemeMode.August
"7" -> AppThemeMode.Carlotta
"8" -> AppThemeMode.Koharu
"9" -> AppThemeMode.Yuuka
"10" -> AppThemeMode.Phoebe
"11" -> AppThemeMode.Mujika
"12" -> AppThemeMode.CUSTOM
"13" -> AppThemeMode.Transparent
else -> AppThemeMode.Dynamic
private const val COMPOSE_ENGINE_MIUIX = "miuix"
private const val MATERIAL_VERSION_EXPRESSIVE = "material3Expressive"
private val appThemeModes = mapOf(
"0" to AppThemeMode.Dynamic,
"1" to AppThemeMode.GR,
"2" to AppThemeMode.Lemon,
"3" to AppThemeMode.WH,
"4" to AppThemeMode.Elink,
"5" to AppThemeMode.Sora,
"6" to AppThemeMode.August,
"7" to AppThemeMode.Carlotta,
"8" to AppThemeMode.Koharu,
"9" to AppThemeMode.Yuuka,
"10" to AppThemeMode.Phoebe,
"11" to AppThemeMode.Mujika,
"12" to AppThemeMode.Custom,
"13" to AppThemeMode.Transparent,
)
private val materialPaletteStyles = mapOf(
"tonalSpot" to PaletteStyle.TonalSpot,
"neutral" to PaletteStyle.Neutral,
"vibrant" to PaletteStyle.Vibrant,
"expressive" to PaletteStyle.Expressive,
"rainbow" to PaletteStyle.Rainbow,
"fruitSalad" to PaletteStyle.FruitSalad,
"monochrome" to PaletteStyle.Monochrome,
"fidelity" to PaletteStyle.Fidelity,
"content" to PaletteStyle.Content,
)
private val miuixPaletteStyles = mapOf(
"tonalSpot" to MiuixPaletteStyle.TonalSpot,
"neutral" to MiuixPaletteStyle.Neutral,
"vibrant" to MiuixPaletteStyle.Vibrant,
"expressive" to MiuixPaletteStyle.Expressive,
"rainbow" to MiuixPaletteStyle.Rainbow,
"fruitSalad" to MiuixPaletteStyle.FruitSalad,
"monochrome" to MiuixPaletteStyle.Monochrome,
"fidelity" to MiuixPaletteStyle.Fidelity,
"content" to MiuixPaletteStyle.Content,
)
private val supportedSpec2025PaletteStyles = setOf(
"tonalSpot",
"neutral",
"vibrant",
"expressive"
)
fun resolveThemeMode(value: String): AppThemeMode {
return appThemeModes[value] ?: AppThemeMode.Dynamic
}
fun resolvePaletteStyle(value: String?): PaletteStyle {
return when (value) {
"tonalSpot" -> PaletteStyle.TonalSpot
"neutral" -> PaletteStyle.Neutral
"vibrant" -> PaletteStyle.Vibrant
"expressive" -> PaletteStyle.Expressive
"rainbow" -> PaletteStyle.Rainbow
"fruitSalad" -> PaletteStyle.FruitSalad
"monochrome" -> PaletteStyle.Monochrome
"fidelity" -> PaletteStyle.Fidelity
"content" -> PaletteStyle.Content
else -> PaletteStyle.TonalSpot
}
return materialPaletteStyles[value] ?: PaletteStyle.TonalSpot
}
fun resolveContrastLevel(): Double {
return try {
Contrast.valueOf(ThemeConfig.customContrast).value
} catch (e: Exception) {
Contrast.Default.value
return runCatching { Contrast.valueOf(ThemeConfig.customContrast).value }
.getOrDefault(Contrast.Default.value)
}
fun resolveColorSchemeMode(value: String): ColorSchemeMode {
return when (value) {
"1" -> ColorSchemeMode.Light
"2" -> ColorSchemeMode.Dark
else -> ColorSchemeMode.System
}
}
fun resolveColorSchemeMode(value: String): ColorSchemeMode = when (value) {
"0" -> ColorSchemeMode.System
"1" -> ColorSchemeMode.Light
"2" -> ColorSchemeMode.Dark
else -> ColorSchemeMode.System
fun resolveMiuixColorSchemeMode(
value: String,
useMonet: Boolean
): ColorSchemeMode {
val baseMode = resolveColorSchemeMode(value)
if (!useMonet) return baseMode
return when (baseMode) {
ColorSchemeMode.Light -> ColorSchemeMode.MonetLight
ColorSchemeMode.Dark -> ColorSchemeMode.MonetDark
else -> ColorSchemeMode.MonetSystem
}
}
fun isMiuixEngine(composeEngine: String): Boolean = composeEngine == "miuix"
fun isMiuixEngine(composeEngine: String): Boolean {
return composeEngine.equals(COMPOSE_ENGINE_MIUIX, ignoreCase = true)
}
fun resolveColorSpecVersion(colorSpec: ThemeColorSpec): ColorSpec.SpecVersion {
return when (colorSpec) {
@@ -66,11 +108,28 @@ object ThemeResolver {
}
fun resolveColorSpecFromMaterialVersion(value: String?): ThemeColorSpec {
return when (value) {
"material3Expressive" -> ThemeColorSpec.SPEC_2025
"material3" -> ThemeColorSpec.SPEC_2021
else -> ThemeColorSpec.SPEC_2021
return if (value == MATERIAL_VERSION_EXPRESSIVE) {
ThemeColorSpec.SPEC_2025
} else {
ThemeColorSpec.SPEC_2021
}
}
fun resolveMiuixPaletteStyle(value: String?): MiuixPaletteStyle {
return miuixPaletteStyles[value] ?: MiuixPaletteStyle.TonalSpot
}
fun resolveMiuixColorSpec(
materialVersion: String?,
paletteStyle: String?
): MiuixThemeColorSpec {
val useSpec2025 = resolveColorSpecFromMaterialVersion(materialVersion) == ThemeColorSpec.SPEC_2025
val supportsSpec2025 = paletteStyle in supportedSpec2025PaletteStyles
return if (useSpec2025 && supportsSpec2025) {
MiuixThemeColorSpec.Spec2025
} else {
MiuixThemeColorSpec.Spec2021
}
}
}
@@ -9,6 +9,7 @@ import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.text.AppText
@@ -20,6 +21,8 @@ fun AppFloatingActionButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
tooltipText: String? = null,
containerColor: Color = LegadoTheme.colorScheme.primaryContainer,
contentColor: Color = LegadoTheme.colorScheme.onPrimaryContainer,
content: @Composable () -> Unit
) {
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
@@ -42,6 +45,8 @@ fun AppFloatingActionButton(
FloatingActionButton(
onClick = onClick,
modifier = modifier,
containerColor = containerColor,
contentColor = contentColor,
content = content
)
}
@@ -49,8 +54,10 @@ fun AppFloatingActionButton(
FloatingActionButton(
onClick = onClick,
modifier = modifier,
containerColor = containerColor,
contentColor = contentColor,
content = content
)
}
}
}
}
@@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
@@ -18,6 +20,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.button.MiuixPrimaryButton
@@ -91,6 +94,11 @@ fun AppAlertDialog(
AlertDialog(
onDismissRequest = onDismissRequest,
modifier = modifier,
containerColor = LegadoTheme.colorScheme.surfaceContainerHigh,
iconContentColor = LegadoTheme.colorScheme.primary,
titleContentColor = LegadoTheme.colorScheme.onSurface,
textContentColor = LegadoTheme.colorScheme.onSurfaceVariant,
tonalElevation = AlertDialogDefaults.TonalElevation,
title = title?.let { { Text(text = it) } },
text = {
Column {
@@ -108,7 +116,10 @@ fun AppAlertDialog(
confirmButton = {
if (onConfirm != null) {
OutlinedButton(
onClick = onConfirm
onClick = onConfirm,
colors = ButtonDefaults.outlinedButtonColors(
contentColor = LegadoTheme.colorScheme.primary
)
) {
Text(text = confirmText)
}
@@ -120,7 +131,10 @@ fun AppAlertDialog(
onClick = {
onDismiss()
onDismissRequest()
}
},
colors = ButtonDefaults.textButtonColors(
contentColor = LegadoTheme.colorScheme.primary
)
) {
Text(text = dismissText)
}
@@ -167,4 +181,4 @@ fun <T> AppAlertDialog(
content = content?.let { { it(currentData) } }
)
}
}
}
@@ -1,10 +1,16 @@
package io.legado.app.ui.widget.components.button
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.basic.Button as MiuixButton
@@ -71,4 +77,47 @@ fun SecondaryButton(
)
}
}
}
}
@Composable
fun ConfirmDismissButtonsRow(
onDismiss: () -> Unit,
onConfirm: () -> Unit,
modifier: Modifier = Modifier,
dismissText: String,
confirmText: String,
dismissEnabled: Boolean = true,
confirmEnabled: Boolean = true,
) {
val isMiuix = ThemeResolver.isMiuixEngine(composeEngine)
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = if (isMiuix) {
Arrangement.spacedBy(12.dp)
} else {
Arrangement.spacedBy(12.dp, Alignment.End)
}
) {
SecondaryButton(
onClick = onDismiss,
modifier = if (isMiuix) {
Modifier.weight(1f)
} else {
Modifier.widthIn(min = 88.dp)
},
enabled = dismissEnabled,
text = dismissText
)
PrimaryButton(
onClick = onConfirm,
modifier = if (isMiuix) {
Modifier.weight(1f)
} else {
Modifier.widthIn(min = 88.dp)
},
enabled = confirmEnabled,
text = confirmText
)
}
}
@@ -26,6 +26,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
@@ -42,6 +43,7 @@ import top.yukonga.miuix.kmp.basic.Text as MiuixText
fun MediumIconButton(
onClick: () -> Unit,
imageVector: ImageVector,
tint: Color = LegadoTheme.colorScheme.onSurface,
modifier: Modifier = Modifier,
contentDescription: String? = null
) {
@@ -52,7 +54,8 @@ fun MediumIconButton(
) {
MiuixIcon(
imageVector = imageVector,
contentDescription = contentDescription
contentDescription = contentDescription,
tint = tint
)
}
} else {
@@ -62,7 +65,8 @@ fun MediumIconButton(
) {
Icon(
imageVector = imageVector,
contentDescription = contentDescription
contentDescription = contentDescription,
tint = tint
)
}
}
@@ -22,7 +22,6 @@ import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedIconButton
import androidx.compose.material3.Text
import androidx.compose.material3.ToggleButton
@@ -72,7 +71,7 @@ fun TopBarButton(
modifier = commonModifier,
colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = GlassTopAppBarDefaults.controlContainerColor(),
contentColor = MaterialTheme.colorScheme.onSurface
contentColor = LegadoTheme.colorScheme.onSurface
)
) {
AnimatedIcon(
@@ -162,7 +161,7 @@ fun TopbarNavigationButton(
fun TopBarActionButton(
onClick: () -> Unit,
imageVector: ImageVector,
contentDescription: String?,
contentDescription: String? = "",
modifier: Modifier = Modifier
) {
val enableProgressive = ThemeConfig.enableProgressiveBlur
@@ -6,7 +6,6 @@ import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CardElevation
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -37,8 +36,8 @@ private fun BaseCard(
) {
if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
val colors = MiuixCardDefaults.defaultColors(
color = (containerColor ?: MiuixTheme.colorScheme.secondaryContainer.copy(alpha)),
contentColor = contentColor ?: MiuixTheme.colorScheme.onSurface
color = (containerColor ?: LegadoTheme.colorScheme.secondaryContainer.copy(alpha)),
contentColor = contentColor ?: LegadoTheme.colorScheme.onSurface
)
MiuixCard(
modifier = modifier,
@@ -51,12 +50,12 @@ private fun BaseCard(
)
} else {
val colors = CardDefaults.cardColors(
containerColor = (containerColor ?: MaterialTheme.colorScheme.secondaryContainer).copy(
containerColor = (containerColor ?: LegadoTheme.colorScheme.secondaryContainer).copy(
alpha = alpha
),
contentColor = contentColor ?: MaterialTheme.colorScheme.onSecondaryContainer,
disabledContainerColor = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = alpha * 0.38f),
disabledContentColor = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = alpha * 0.38f)
contentColor = contentColor ?: LegadoTheme.colorScheme.onSecondaryContainer,
disabledContainerColor = LegadoTheme.colorScheme.onSecondaryContainer.copy(alpha = alpha * 0.38f),
disabledContentColor = LegadoTheme.colorScheme.onSecondaryContainer.copy(alpha = alpha * 0.38f)
)
if (onClick != null) {
Card(
@@ -56,6 +56,7 @@ fun SelectionItemCard(
title: String,
modifier: Modifier = Modifier,
subtitle: String? = null,
supportingContent: @Composable (() -> Unit)? = null,
isEnabled: Boolean = true,
isSelected: Boolean = false,
inSelectionMode: Boolean = false,
@@ -75,10 +76,10 @@ fun SelectionItemCard(
val animatedContainerColor by animateColorAsState(
targetValue = if (isSelected)
selectedContainerColor
?: if (composeEngine) MiuixTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.secondaryContainer
?: if (composeEngine) LegadoTheme.colorScheme.secondaryContainer else LegadoTheme.colorScheme.secondaryContainer
else
containerColor
?: if (composeEngine) MiuixTheme.colorScheme.surfaceContainer else MaterialTheme.colorScheme.surfaceContainerLow,
?: if (composeEngine) LegadoTheme.colorScheme.surfaceContainer else LegadoTheme.colorScheme.surfaceContainerLow,
animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing),
label = "CardColor"
)
@@ -130,13 +131,16 @@ fun SelectionItemCard(
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (!subtitle.isNullOrBlank()) {
AppText(
text = subtitle,
style = LegadoTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
when {
supportingContent != null -> supportingContent()
!subtitle.isNullOrBlank() -> {
AppText(
text = subtitle,
style = LegadoTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
} else {
@@ -150,16 +154,20 @@ fun SelectionItemCard(
overflow = TextOverflow.Ellipsis
)
},
supportingContent = if (!subtitle.isNullOrBlank()) {
{
AppText(
text = subtitle,
style = LegadoTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
supportingContent = when {
supportingContent != null -> supportingContent
!subtitle.isNullOrBlank() -> {
{
AppText(
text = subtitle,
style = LegadoTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
} else null,
else -> null
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent)
)
}
@@ -218,6 +226,7 @@ fun LazyItemScope.ReorderableSelectionItem(
title: String,
modifier: Modifier = Modifier,
subtitle: String? = null,
supportingContent: @Composable (() -> Unit)? = null,
isEnabled: Boolean = true,
isSelected: Boolean = false,
inSelectionMode: Boolean = false,
@@ -242,6 +251,7 @@ fun LazyItemScope.ReorderableSelectionItem(
SelectionItemCard(
title = title,
subtitle = subtitle,
supportingContent = supportingContent,
isEnabled = isEnabled,
isSelected = isSelected,
inSelectionMode = inSelectionMode,
@@ -31,7 +31,7 @@ fun TextCard(
contentColor: Color? = null,
cornerRadius: Dp = 8.dp,
horizontalPadding: Dp = 8.dp,
verticalPadding: Dp = 2.dp,
verticalPadding: Dp = 4.dp,
iconSize: Dp = 14.dp,
spacing: Dp = 4.dp,
textStyle: TextStyle = LegadoTheme.typography.labelSmallEmphasized
@@ -2,7 +2,6 @@ package io.legado.app.ui.widget.components.dialog
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -10,20 +9,17 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -34,15 +30,20 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.github.skydoves.colorpicker.compose.AlphaSlider
import com.github.skydoves.colorpicker.compose.BrightnessSlider
import com.github.skydoves.colorpicker.compose.HsvColorPicker
import com.github.skydoves.colorpicker.compose.rememberColorPickerController
import io.legado.app.R
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.isHex
import top.yukonga.miuix.kmp.basic.ColorPalette
import top.yukonga.miuix.kmp.basic.ColorPicker
import top.yukonga.miuix.kmp.basic.ColorSpace
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -52,111 +53,50 @@ fun ColorPickerSheet(
onDismissRequest: () -> Unit,
onColorSelected: (Int) -> Unit
) {
val controller = rememberColorPickerController()
var currentColor by remember { mutableStateOf(Color(initialColor)) }
var hexInput by remember { mutableStateOf(initialColor.asHexColorString()) }
var isHexInputError by remember { mutableStateOf(false) }
val presets = remember {
listOf(
Color(0xFFF44336),
Color(0xFFE91E63),
Color(0xFF9C27B0),
Color(0xFF673AB7),
Color(0xFF3F51B5),
Color(0xFF2196F3),
Color(0xFF03A9F4),
Color(0xFF00BCD4),
Color(0xFF009688),
Color(0xFF4CAF50),
Color(0xFF8BC34A),
Color(0xFFCDDC39),
Color(0xFFFFEB3B),
Color(0xFFFFC107),
Color(0xFFFF9800),
Color(0xFFFF5722),
Color(0xFF795548),
Color(0xFF9E9E9E),
Color(0xFF607D8B),
Color(0xFF000000),
Color(0xFFFFFFFF)
)
LaunchedEffect(show, initialColor) {
if (show) {
currentColor = Color(initialColor)
hexInput = initialColor.asHexColorString()
isHexInputError = false
}
}
AppModalBottomSheet(show = show, onDismissRequest = onDismissRequest) {
val parsedHexColor = parseHexColor(hexInput)
AppModalBottomSheet(
show = show,
onDismissRequest = onDismissRequest,
title = "选择颜色",
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
.padding(bottom = 16.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
AppText(
text = "颜色选择",
style = LegadoTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 16.dp)
ColorPalette(
color = currentColor,
onColorChanged = { color ->
currentColor = color
hexInput = color.toArgb().asHexColorString()
isHexInputError = false
},
rows = 8,
hueColumns = 12,
modifier = Modifier.fillMaxWidth(),
showPreview = false
)
HsvColorPicker(
modifier = Modifier
.fillMaxWidth()
.height(240.dp)
.padding(10.dp),
controller = controller,
initialColor = Color(initialColor),
onColorChanged = { envelope ->
currentColor = envelope.color
}
)
Spacer(modifier = Modifier.height(16.dp))
AlphaSlider(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
.height(35.dp),
controller = controller,
)
BrightnessSlider(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
.height(35.dp),
controller = controller,
)
LazyVerticalGrid(
columns = GridCells.Adaptive(36.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.heightIn(max = 80.dp)
.padding(horizontal = 10.dp)
) {
items(presets) { color ->
Box(
modifier = Modifier
.size(36.dp)
.clip(CircleShape)
.background(color)
.border(
if (color.toArgb() == currentColor.toArgb()) 2.dp else 0.dp,
MaterialTheme.colorScheme.primary,
CircleShape
)
.clickable {
controller.selectByColor(color, true)
}
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(32.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp)
modifier = Modifier.fillMaxWidth()
) {
Box(
modifier = Modifier
@@ -165,32 +105,68 @@ fun ColorPickerSheet(
.background(currentColor)
.border(
1.dp,
MaterialTheme.colorScheme.outlineVariant,
LegadoTheme.colorScheme.outlineVariant,
RoundedCornerShape(8.dp)
)
)
Spacer(modifier = Modifier.width(12.dp))
AppText(
text = "#${Integer.toHexString(currentColor.toArgb()).uppercase()}",
style = LegadoTheme.typography.bodyLarge
)
Row(
AppTextField(
value = hexInput,
onValueChange = { value ->
hexInput = normalizeHexInput(value)
val parsedColor = parseHexColor(hexInput)
if (parsedColor != null) {
currentColor = Color(parsedColor)
isHexInputError = false
} else {
isHexInputError = hexInput.isNotBlank()
}
},
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = onDismissRequest) {
AppText(stringResource(R.string.cancel))
}
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = {
onColorSelected(currentColor.toArgb())
onDismissRequest()
}) {
AppText(stringResource(R.string.ok))
}
}
label = "色值",
singleLine = true,
isError = isHexInputError,
backgroundColor = LegadoTheme.colorScheme.surface,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Characters,
keyboardType = KeyboardType.Ascii,
imeAction = ImeAction.Done
)
)
}
Spacer(modifier = Modifier.height(16.dp))
ConfirmDismissButtonsRow(
modifier = Modifier.fillMaxWidth(),
onDismiss = onDismissRequest,
onConfirm = {
onColorSelected(currentColor.toArgb())
onDismissRequest()
},
dismissText = stringResource(R.string.cancel),
confirmText = stringResource(R.string.ok),
confirmEnabled = parsedHexColor != null && !isHexInputError
)
}
}
}
private fun normalizeHexInput(input: String): String {
val trimmed = input.trim().uppercase()
return if (trimmed.startsWith("#")) {
"#${trimmed.removePrefix("#")}"
} else {
trimmed
}
}
private fun parseHexColor(input: String): Int? {
val hex = input.trim().removePrefix("#")
if (hex.length !in setOf(6, 8) || !hex.isHex()) return null
val argb = if (hex.length == 6) "FF$hex" else hex
return argb.toLong(16).toInt()
}
private fun Int.asHexColorString(): String =
"#${Integer.toHexString(this).uppercase().padStart(8, '0')}"
@@ -1,6 +1,7 @@
package io.legado.app.ui.widget.components.menuItem
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
@@ -13,15 +14,20 @@ import androidx.compose.material3.MotionScheme
import androidx.compose.material3.Shapes
import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
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.ProvideAppContentColor
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.theme.rememberOpaqueColorScheme
import top.yukonga.miuix.kmp.basic.ListPopupColumn
import top.yukonga.miuix.kmp.overlay.OverlayListPopup
import top.yukonga.miuix.kmp.window.WindowListPopup
val LocalUseMiuixWindowPopup = staticCompositionLocalOf { false }
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@@ -29,25 +35,28 @@ fun RoundDropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
// M3 专属视觉参数,Miuix 模式下会优雅降级(忽略)
shape: Shape = MaterialTheme.shapes.medium,
shadowElevation: Dp = 4.dp,
verticalSpacing: Dp = 8.dp,
content: @Composable ColumnScope.(dismiss: () -> Unit) -> Unit
) {
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
val popupContainerColor = LegadoTheme.colorScheme.surfaceContainer
if (isMiuix) {
val popupContentColor = LegadoTheme.colorScheme.onSurface
OverlayListPopup(
show = expanded,
onDismissRequest = onDismissRequest,
popupModifier = modifier
) {
ListPopupColumn {
Column() {
Spacer(Modifier.height(12.dp))
content(onDismissRequest)
Spacer(Modifier.height(12.dp))
ProvideAppContentColor(popupContentColor) {
ListPopupColumn {
Column(modifier = Modifier.background(popupContainerColor)) {
Spacer(Modifier.height(12.dp))
content(onDismissRequest)
Spacer(Modifier.height(12.dp))
}
}
}
}
@@ -1,5 +1,6 @@
package io.legado.app.ui.widget.components.menuItem
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
@@ -13,10 +14,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -26,6 +29,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -33,13 +37,16 @@ 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
import top.yukonga.miuix.kmp.icon.basic.Check
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.basic.Text as MiuixText
@Composable
fun RoundDropdownMenuItem(
text: String,
color: Color = LegadoTheme.colorScheme.surface,
color: Color = Color.Unspecified,
onClick: () -> Unit,
modifier: Modifier = Modifier,
isSelected: Boolean = false,
@@ -51,20 +58,21 @@ fun RoundDropdownMenuItem(
) {
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
val interaction = interactionSource ?: remember { MutableInteractionSource() }
val hasCustomContentColor = color != Color.Unspecified
if (isMiuix) {
val backgroundColor = if (isSelected) {
LegadoTheme.colorScheme.primaryContainer
val dropdownColors = DropdownDefaults.dropdownColors()
val (textColor, backgroundColor) = if (isSelected) {
dropdownColors.selectedContentColor to dropdownColors.selectedContainerColor
} else {
dropdownColors.contentColor to dropdownColors.containerColor
}
val checkColor = if (isSelected) {
dropdownColors.selectedContentColor
} else {
Color.Transparent
}
val contentColor = if (isSelected) {
LegadoTheme.colorScheme.onPrimaryContainer
} else {
LegadoTheme.colorScheme.onSurface
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
@@ -73,16 +81,13 @@ fun RoundDropdownMenuItem(
.drawBehind { drawRect(backgroundColor) }
.clickable(
interactionSource = interaction,
indication = androidx.compose.foundation.LocalIndication.current,
indication = LocalIndication.current,
enabled = enabled,
onClick = onClick
)
.padding(horizontal = 20.dp, vertical = 12.dp)
) {
CompositionLocalProvider(
LocalContentColor provides contentColor,
LocalTextStyle provides MiuixTheme.textStyles.body1
) {
CompositionLocalProvider(LocalContentColor provides textColor) {
if (leadingIcon != null) {
leadingIcon()
Spacer(Modifier.width(12.dp))
@@ -93,24 +98,46 @@ fun RoundDropdownMenuItem(
contentAlignment = Alignment.CenterStart
) {
MiuixText(
modifier = Modifier.widthIn(max = 200.dp),
text = text,
fontWeight = FontWeight.Medium
fontSize = MiuixTheme.textStyles.body1.fontSize,
fontWeight = FontWeight.Medium,
color = textColor,
)
}
if (trailingIcon != null) {
Spacer(Modifier.width(12.dp))
trailingIcon()
} else {
Spacer(Modifier.width(12.dp))
AppIcon(
imageVector = MiuixIcons.Basic.Check,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = checkColor
)
}
}
}
} else {
val colorScheme = rememberOpaqueColorScheme()
val selectedContentColor = colorScheme.onPrimaryContainer
val defaultContentColor = colorScheme.onSurface
val contentColor = if (enabled) {
MaterialTheme.colorScheme.onSurface
when {
hasCustomContentColor -> color
isSelected -> selectedContentColor
else -> defaultContentColor
}
} else {
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
when {
hasCustomContentColor -> color.copy(alpha = 0.38f)
isSelected -> selectedContentColor.copy(alpha = 0.38f)
else -> defaultContentColor.copy(alpha = 0.38f)
}
}
val containerColor = if (isSelected) colorScheme.primaryContainer else colorScheme.surface
Surface(
onClick = onClick,
@@ -119,7 +146,8 @@ fun RoundDropdownMenuItem(
.fillMaxWidth(),
enabled = enabled,
shape = MaterialTheme.shapes.small,
color = colorScheme.surface,
color = containerColor,
contentColor = contentColor,
interactionSource = interaction
) {
Row(
@@ -141,13 +169,22 @@ fun RoundDropdownMenuItem(
Text(
modifier = Modifier.widthIn(max = 200.dp),
text = text,
style = LegadoTheme.typography.bodyMediumEmphasized
style = LegadoTheme.typography.bodyMediumEmphasized,
color = contentColor
)
}
if (trailingIcon != null) {
Spacer(Modifier.width(8.dp))
trailingIcon()
} else {
Spacer(Modifier.width(8.dp))
AppIcon(
imageVector = Icons.Default.Check,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = if (isSelected) contentColor else Color.Transparent
)
}
}
}
@@ -18,6 +18,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.Typography
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -28,8 +29,10 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LocalLegadoThemeColors
import io.legado.app.ui.theme.ProvideAppContentColor
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.theme.rememberOpaqueColorScheme
import io.legado.app.ui.widget.components.menuItem.LocalUseMiuixWindowPopup
import top.yukonga.miuix.kmp.window.WindowBottomSheet
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@@ -43,30 +46,56 @@ fun AppModalBottomSheet(
endAction: @Composable (() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit
) {
val colorScheme = LocalLegadoThemeColors.current.colorScheme
val sheetContainerColor = LegadoTheme.colorScheme.surfaceContainer
val sheetContentColor = LegadoTheme.colorScheme.onSurface
val sheetDragHandleColor = LegadoTheme.colorScheme.onSurfaceVariant
if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
WindowBottomSheet(
show = show,
modifier = modifier,
title = title,
startAction = startAction,
endAction = endAction,
startAction = startAction?.let { action ->
{
ProvideAppContentColor(sheetContentColor) {
CompositionLocalProvider(LocalUseMiuixWindowPopup provides true) {
action()
}
}
}
},
endAction = endAction?.let { action ->
{
ProvideAppContentColor(sheetContentColor) {
CompositionLocalProvider(LocalUseMiuixWindowPopup provides true) {
action()
}
}
}
},
insideMargin = DpSize(16.dp, 12.dp),
backgroundColor = sheetContainerColor,
dragHandleColor = sheetDragHandleColor,
onDismissRequest = onDismissRequest,
onDismissFinished = onDismissRequest,
enableWindowDim = true,
allowDismiss = true
) {
Column(
modifier = Modifier
.fillMaxWidth()
.animateContentSize(),
content = content
)
ProvideAppContentColor(sheetContentColor) {
CompositionLocalProvider(LocalUseMiuixWindowPopup provides true) {
Column(
modifier = Modifier
.fillMaxWidth()
.animateContentSize(),
content = content
)
}
}
}
} else {
if (show) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val colorScheme = rememberOpaqueColorScheme()
val density = LocalDensity.current
val maxHeight = with(density) {
LocalWindowInfo.current.containerSize.height.toDp() * 0.8f
@@ -75,8 +104,9 @@ fun AppModalBottomSheet(
ModalBottomSheet(
onDismissRequest = onDismissRequest,
sheetState = sheetState,
contentColor = colorScheme.onSurface,
dragHandle = { BottomSheetDefaults.DragHandle() }
containerColor = sheetContainerColor,
contentColor = sheetContentColor,
dragHandle = { BottomSheetDefaults.DragHandle(color = sheetDragHandleColor) }
) {
MaterialExpressiveTheme(
colorScheme = colorScheme,
@@ -112,7 +142,7 @@ fun AppModalBottomSheet(
Text(
text = title,
style = LegadoTheme.typography.titleMediumEmphasized,
color = colorScheme.onSurface,
color = sheetContentColor,
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@@ -134,4 +164,4 @@ fun AppModalBottomSheet(
}
}
}
}
}
@@ -1,17 +1,12 @@
package io.legado.app.ui.widget.components.settingItem
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Replay
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -25,7 +20,7 @@ import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.button.SmallTextButton
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
import io.legado.app.ui.widget.components.text.AppText
import top.yukonga.miuix.kmp.basic.BasicComponent
import top.yukonga.miuix.kmp.basic.TextField as MiuixTextField
@@ -76,28 +71,18 @@ fun InputSettingItem(
}
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
horizontalArrangement = Arrangement.End
) {
SmallTextButton(
text = "默认",
imageVector = Icons.Default.Replay,
onClick = {
state.edit { replace(0, length, defaultValue.toString()) }
}
)
SmallTextButton(
text = "确认",
imageVector = Icons.Default.Check,
onClick = {
onConfirm(state.text.toString())
expanded = false
}
)
}
ConfirmDismissButtonsRow(
modifier = Modifier.padding(top = 16.dp),
onDismiss = {
state.edit { replace(0, length, defaultValue.toString()) }
},
onConfirm = {
onConfirm(state.text.toString())
expanded = false
},
dismissText = stringResource(R.string.text_default),
confirmText = stringResource(R.string.confirm)
)
}
}
}
@@ -127,26 +112,18 @@ fun InputSettingItem(
}
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
SmallTextButton(
text = "默认",
imageVector = Icons.Default.Replay,
onClick = {
state.edit { replace(0, length, defaultValue.toString()) }
}
)
SmallTextButton(
text = "确认",
imageVector = Icons.Default.Check,
onClick = {
onConfirm(state.text.toString())
expanded = false
}
)
}
ConfirmDismissButtonsRow(
modifier = Modifier.padding(top = 16.dp),
onDismiss = {
state.edit { replace(0, length, defaultValue.toString()) }
},
onConfirm = {
onConfirm(state.text.toString())
expanded = false
},
dismissText = stringResource(R.string.text_default),
confirmText = stringResource(R.string.confirm)
)
}
)
}
@@ -2,15 +2,12 @@
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.rememberTextFieldState
@@ -29,11 +26,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.button.SmallTextButton
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
import io.legado.app.ui.widget.components.text.AppText
import top.yukonga.miuix.kmp.basic.BasicComponent
import top.yukonga.miuix.kmp.basic.Slider as MiuixSlider
@@ -78,7 +77,11 @@ fun SliderSettingItem(
MiuixTextField(
state = textFieldState,
lineLimits = TextFieldLineLimits.SingleLine,
label = "杈撳叆鏁板€?(${valueRange.start.toInt()}-${valueRange.endInclusive.toInt()})",
label = stringResource(
R.string.input_value_range,
valueRange.start.toInt(),
valueRange.endInclusive.toInt()
),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth(),
inputTransformation = {
@@ -113,35 +116,26 @@ fun SliderSettingItem(
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
horizontalArrangement = Arrangement.End
) {
SmallTextButton(
text = if (isInputMode) "婊戝潡" else "杈撳叆",
imageVector = if (isInputMode) Icons.Default.LinearScale else Icons.Default.Edit,
onClick = { isInputMode = !isInputMode }
)
Spacer(Modifier.width(8.dp))
SmallTextButton(
text = "榛樿",
imageVector = Icons.Default.RestartAlt,
onClick = {
onValueChange(defaultValue)
textFieldState.edit {
replace(
0,
length,
defaultValue.toInt().toString()
)
}
ConfirmDismissButtonsRow(
modifier = Modifier.padding(top = 16.dp),
onDismiss = { isInputMode = !isInputMode },
onConfirm = {
onValueChange(defaultValue)
textFieldState.edit {
replace(
0,
length,
defaultValue.toInt().toString()
)
}
)
}
},
dismissText = if (isInputMode) {
stringResource(R.string.slider)
} else {
stringResource(R.string.edit)
},
confirmText = stringResource(R.string.text_default)
)
}
}
}
@@ -163,7 +157,15 @@ fun SliderSettingItem(
TextField(
state = textFieldState,
lineLimits = TextFieldLineLimits.SingleLine,
label = { AppText("杈撳叆鏁板€?(${valueRange.start.toInt()}-${valueRange.endInclusive.toInt()})") },
label = {
AppText(
stringResource(
R.string.input_value_range,
valueRange.start.toInt(),
valueRange.endInclusive.toInt()
)
)
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier
.fillMaxWidth()
@@ -207,33 +209,26 @@ fun SliderSettingItem(
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
SmallTextButton(
text = if (isInputMode) "婊戝潡" else "杈撳叆",
imageVector = if (isInputMode) Icons.Default.LinearScale else Icons.Default.Edit,
onClick = { isInputMode = !isInputMode }
)
Spacer(Modifier.width(8.dp))
SmallTextButton(
text = "榛樿",
imageVector = Icons.Default.RestartAlt,
onClick = {
onValueChange(defaultValue)
textFieldState.edit {
replace(
0,
length,
defaultValue.toInt().toString()
)
}
ConfirmDismissButtonsRow(
modifier = Modifier.padding(top = 16.dp),
onDismiss = { isInputMode = !isInputMode },
onConfirm = {
onValueChange(defaultValue)
textFieldState.edit {
replace(
0,
length,
defaultValue.toInt().toString()
)
}
)
}
},
dismissText = if (isInputMode) {
stringResource(R.string.slider)
} else {
stringResource(R.string.edit)
},
confirmText = stringResource(R.string.text_default)
)
}
)
}
@@ -33,4 +33,4 @@ class MiuixGlassScrollBehavior(
override val collapsedFraction: Float
get() = miuixBehavior.state.collapsedFraction
}
}
@@ -33,7 +33,7 @@
<io.legado.app.ui.widget.text.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="80dp"
android:layout_marginBottom="240dp"
app:layout_constraintTop_toBottomOf="@+id/tool_bar">
<com.google.android.material.textfield.TextInputEditText
@@ -394,6 +394,7 @@
<string name="imm_navigation_bar">沉浸式导航栏</string>
<string name="imm_navigation_bar_s">导航栏颜色透明</string>
<string name="add_to_bookshelf">放入书架</string>
<string name="click_to_remove">点击删除</string>
<string name="continue_read">继续阅读</string>
<string name="cover_path">封面地址</string>
<string name="page_anim_cover">覆盖</string>
@@ -1237,6 +1238,8 @@
<string name="refresh_explore">刷新发现</string>
<string name="ungroup">未分组</string>
<string name="dynamic_colors">动态取色</string>
<string name="dynamic_colors_summary">使用基于壁纸的主题颜色</string>
<string name="input_value_range">输入数值(%1$d-%2$d</string>
<string name="change_icon_success">图标已修改</string>
<string name="replace_rule_title_setting">配置替换规则</string>
<string name="more_setting">更多设置</string>
@@ -1289,6 +1292,8 @@
<string name="light_mode">浅色</string>
<string name="dark_mode">深色</string>
<string name="pure_black">纯黑深色模式</string>
<string name="miuix_monet">自定义主题色</string>
<string name="miuix_monet_summary">为 Miuix 使用自定义主题配色</string>
<string name="transparent_theme_alarm">透明主题请于设置背景图片后设置</string>
<string name="seed_color_summary">使用种子颜色生成主题颜色</string>
<string name="seed_color">种子色</string>
@@ -370,6 +370,7 @@
<string name="imm_navigation_bar">沉浸式导航栏</string>
<string name="imm_navigation_bar_s">导航栏颜色透明</string>
<string name="add_to_bookshelf">放入書架</string>
<string name="click_to_remove">點擊刪除</string>
<string name="continue_read">繼續閲讀</string>
<string name="cover_path">封面地址</string>
<string name="page_anim_cover">覆蓋</string>
@@ -1223,4 +1224,8 @@
<string name="bottom_bar_opacity_summary">%d</string>
<string name="web_file">網頁文件</string>
<string name="show_book_count">顯示分組書籍數量</string>
<string name="dynamic_colors_summary">使用基於桌布的主題顏色</string>
<string name="input_value_range">輸入數值(%1$d-%2$d</string>
<string name="miuix_monet">Miuix Monet</string>
<string name="miuix_monet_summary">為 Miuix 使用 Monet 演算法生成配色。目前主題為動態主題且系統為 Android 12 或以上時,會優先使用系統取色;否則使用下方種子色。</string>
</resources>
@@ -372,6 +372,7 @@
<string name="imm_navigation_bar">沉浸式導航欄</string>
<string name="imm_navigation_bar_s">導航欄顏色透明</string>
<string name="add_to_bookshelf">放入書架</string>
<string name="click_to_remove">點擊刪除</string>
<string name="continue_read">繼續閱讀</string>
<string name="cover_path">封面地址</string>
<string name="page_anim_cover">覆蓋</string>
@@ -1225,4 +1226,8 @@
<string name="bottom_bar_opacity_summary">%d</string>
<string name="web_file">網頁文件</string>
<string name="show_book_count">顯示分組書籍數量</string>
<string name="dynamic_colors_summary">使用基於桌布的主題顏色</string>
<string name="input_value_range">輸入數值(%1$d-%2$d</string>
<string name="miuix_monet">Miuix Monet</string>
<string name="miuix_monet_summary">為 Miuix 使用 Monet 演算法產生配色。目前主題為動態主題且系統為 Android 12 以上時,會優先使用系統取色;否則使用下方種子色。</string>
</resources>
+5
View File
@@ -397,6 +397,7 @@
<string name="imm_navigation_bar">immersion navigation bar</string>
<string name="imm_navigation_bar_s">The navigation bar becomes transparent</string>
<string name="add_to_bookshelf">Add to Bookshelf</string>
<string name="click_to_remove">Tap to remove</string>
<string name="continue_read">Continue reading</string>
<string name="cover_path">Cover path</string>
<string name="page_anim_cover">Cover</string>
@@ -1240,6 +1241,8 @@
<string name="refresh_explore">Refresh Explore</string>
<string name="ungroup">Ungrouped</string>
<string name="dynamic_colors">Dynamic Colors</string>
<string name="dynamic_colors_summary">Use wallpaper-based theme colors</string>
<string name="input_value_range">Enter value (%1$d-%2$d)</string>
<string name="change_icon_success">Icon Updated</string>
<string name="replace_rule_title_setting">Configure Replace Rules</string>
<string name="more_setting">More Settings</string>
@@ -1292,6 +1295,8 @@
<string name="light_mode">Light</string>
<string name="dark_mode">Dark</string>
<string name="pure_black">True Black Mode</string>
<string name="miuix_monet">Miuix Monet</string>
<string name="miuix_monet_summary">Use Monet to generate the Miuix palette. If the current theme is Dynamic on Android 12+, the system accent is used; otherwise the seed color below is used.</string>
<string name="transparent_theme_alarm">Please set the transparent theme after setting a background image.</string>
<string name="seed_color_summary">Create a theme from a seed color</string>
<string name="seed_color">Seed Color</string>