[优化] 重写书籍详情页、本地书籍页和优化一些界面,为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 <activity
android:name=".ui.book.info.edit.BookInfoEditActivity" android:name=".ui.book.info.edit.BookInfoEditActivity"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
android:launchMode="singleTask" /> android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize|stateHidden" />
<!-- 音频播放界面 --> <!-- 音频播放界面 -->
<activity <activity
android:name="io.legado.app.ui.book.audio.AudioPlayActivity" android:name="io.legado.app.ui.book.audio.AudioPlayActivity"
@@ -185,6 +185,7 @@ object PreferKey {
const val paletteStyle = "paletteStyle" const val paletteStyle = "paletteStyle"
const val materialVersion = "materialVersion" const val materialVersion = "materialVersion"
const val composeEngine = "composeEngine" const val composeEngine = "composeEngine"
const val useMiuixMonet = "useMiuixMonet"
const val containerOpacity = "containerOpacity" const val containerOpacity = "containerOpacity"
const val topBarOpacity = "topBarOpacity" const val topBarOpacity = "topBarOpacity"
const val bottomBarOpacity = "bottomBarOpacity" 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.okHttpClient
import io.legado.app.help.http.okHttpClientManga import io.legado.app.help.http.okHttpClientManga
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel 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.explore.ExploreShowViewModel
import io.legado.app.ui.book.group.GroupViewModel import io.legado.app.ui.book.group.GroupViewModel
import io.legado.app.ui.book.import.local.ImportBookViewModel import io.legado.app.ui.book.import.local.ImportBookViewModel
@@ -101,6 +104,9 @@ val appModule = module {
viewModelOf(::ImportBookViewModel) viewModelOf(::ImportBookViewModel)
viewModelOf(::RemoteBookViewModel) viewModelOf(::RemoteBookViewModel)
viewModelOf(::BookInfoViewModel) viewModelOf(::BookInfoViewModel)
viewModelOf(::ChangeCoverViewModel)
viewModelOf(::ChangeBookSourceComposeViewModel)
viewModelOf(::ChangeBookSourceViewModel)
viewModelOf(::ExploreViewModel) viewModelOf(::ExploreViewModel)
viewModelOf(::RssViewModel) viewModelOf(::RssViewModel)
@@ -25,8 +25,10 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import java.util.Collections import java.util.Collections
@@ -51,6 +53,8 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
} }
private var task: Job? = null private var task: Job? = null
val searchStateData = MutableLiveData<Boolean>() val searchStateData = MutableLiveData<Boolean>()
private val _isSearching = MutableStateFlow(false)
val isSearching = _isSearching.asStateFlow()
var name: String = "" var name: String = ""
var author: String = "" var author: String = ""
val searchBooks: MutableList<SearchBook> = Collections.synchronizedList(arrayListOf()) 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() { private fun initSearchPool() {
searchPool = Executors searchPool = Executors
.newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher() .newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher()
@@ -121,12 +130,14 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
} }
}.onStart { }.onStart {
searchStateData.postValue(true) searchStateData.postValue(true)
_isSearching.value = true
}.mapParallelSafe(threadCount) { }.mapParallelSafe(threadCount) {
withTimeout(60000L) { withTimeout(60000L) {
search(it) search(it)
} }
}.onCompletion { }.onCompletion {
searchStateData.postValue(false) searchStateData.postValue(false)
_isSearching.value = false
}.catch { }.catch {
AppLog.put("封面换源搜索出错\n${it.localizedMessage}", it) AppLog.put("封面换源搜索出错\n${it.localizedMessage}", it)
}.collect() }.collect()
@@ -156,10 +167,11 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
} }
} }
private fun stopSearch() { fun stopSearch() {
task?.cancel() task?.cancel()
searchPool?.close() searchPool?.close()
searchStateData.postValue(false) searchStateData.postValue(false)
_isSearching.value = false
} }
override fun onCleared() { override fun onCleared() {
@@ -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 val threadCount = AppConfig.threadCount
private var searchPool: ExecutorCoroutineDispatcher? = null private var searchPool: ExecutorCoroutineDispatcher? = null
val searchStateData = MutableLiveData<Boolean>() val searchStateData = MutableLiveData<Boolean>()
private val _isSearching = MutableStateFlow(false)
val isSearching = _isSearching.asStateFlow()
var searchFinishCallback: ((isEmpty: Boolean) -> Unit)? = null var searchFinishCallback: ((isEmpty: Boolean) -> Unit)? = null
var name: String = "" var name: String = ""
var author: 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() { private fun initSearchPool() {
searchPool = Executors searchPool = Executors
.newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher() .newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher()
@@ -234,6 +249,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
} }
}.onStart { }.onStart {
searchStateData.postValue(true) searchStateData.postValue(true)
_isSearching.value = true
}.mapParallel(threadCount) { }.mapParallel(threadCount) {
try { try {
withTimeout(60000L) { withTimeout(60000L) {
@@ -250,6 +266,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
}.onCompletion { }.onCompletion {
ensureActive() ensureActive()
searchStateData.postValue(false) searchStateData.postValue(false)
_isSearching.value = false
searchFinishCallback?.invoke(searchBooks.isEmpty()) searchFinishCallback?.invoke(searchBooks.isEmpty())
}.catch { }.catch {
AppLog.put("换源搜索出错\n${it.localizedMessage}", it) AppLog.put("换源搜索出错\n${it.localizedMessage}", it)
@@ -383,6 +400,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
} }
}.onStart { }.onStart {
searchStateData.postValue(true) searchStateData.postValue(true)
_isSearching.value = true
}.mapParallelSafe(threadCount) { }.mapParallelSafe(threadCount) {
val source = appDb.bookSourceDao.getBookSource(it.origin)!! val source = appDb.bookSourceDao.getBookSource(it.origin)!!
withTimeout(60000L) { withTimeout(60000L) {
@@ -390,6 +408,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
} }
}.onCompletion { }.onCompletion {
searchStateData.postValue(false) searchStateData.postValue(false)
_isSearching.value = false
}.catch { }.catch {
AppLog.put("换源刷新列表出错\n${it.localizedMessage}", it) AppLog.put("换源刷新列表出错\n${it.localizedMessage}", it)
}.collect() }.collect()
@@ -446,6 +465,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
task?.cancel() task?.cancel()
searchPool?.close() searchPool?.close()
searchStateData.postValue(false) searchStateData.postValue(false)
_isSearching.value = false
} }
fun getToc( fun getToc(
@@ -554,10 +574,8 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
} }
fun setBookScore(searchBook: SearchBook, score: Int) { fun setBookScore(searchBook: SearchBook, score: Int) {
execute { SourceConfig.setBookScore(searchBook.origin, searchBook.name, searchBook.author, score)
SourceConfig.setBookScore(searchBook.origin, searchBook.name, searchBook.author, score) searchCallback?.upAdapter()
searchCallback?.upAdapter()
}
} }
fun getBookScore(searchBook: SearchBook): Int { fun getBookScore(searchBook: SearchBook): Int {
@@ -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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
@@ -89,8 +91,10 @@ fun BookInfoEditScreen(
uiState.book?.let { uiState.book?.let {
BookInfoEditContent( BookInfoEditContent(
modifier = Modifier modifier = Modifier
.padding(paddingValues)
.fillMaxSize() .fillMaxSize()
.padding(paddingValues)
.consumeWindowInsets(paddingValues)
.imePadding()
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
uiState = uiState, uiState = uiState,
viewModel = viewModel viewModel = viewModel
@@ -57,18 +57,14 @@ fun LauncherIconPickerSheet(
AppModalBottomSheet( AppModalBottomSheet(
show = show, show = show,
onDismissRequest = onDismissRequest onDismissRequest = onDismissRequest,
title = stringResource(R.string.change_icon)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 24.dp) .padding(bottom = 24.dp)
) { ) {
AppText(
text = stringResource(R.string.change_icon),
style = LegadoTheme.typography.titleMedium,
modifier = Modifier.padding(16.dp)
)
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Fixed(3), columns = GridCells.Fixed(3),
@@ -89,15 +85,15 @@ fun LauncherIconPickerSheet(
.clip(MaterialTheme.shapes.large) .clip(MaterialTheme.shapes.large)
.background( .background(
if (isSelected) if (isSelected)
MaterialTheme.colorScheme.secondaryContainer LegadoTheme.colorScheme.secondaryContainer
else else
MaterialTheme.colorScheme.surfaceContainer LegadoTheme.colorScheme.surfaceContainer
) )
.then( .then(
if (isSelected) { if (isSelected) {
Modifier.border( Modifier.border(
width = 2.dp, width = 2.dp,
color = MaterialTheme.colorScheme.primary, color = LegadoTheme.colorScheme.primary,
shape = MaterialTheme.shapes.large shape = MaterialTheme.shapes.large
) )
} else { } else {
@@ -24,6 +24,10 @@ object ThemeConfig {
//m3 or miuix //m3 or miuix
var composeEngine by prefDelegate(PreferKey.composeEngine, "material") 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 materialVersion by prefDelegate(PreferKey.materialVersion, "material3")
var appTheme by prefDelegate(PreferKey.appTheme, "0") 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.ThemeStore
import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.theme.LegadoTheme 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.ThemeResolver
import io.legado.app.ui.theme.adaptiveContentPadding import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
@@ -112,6 +112,7 @@ fun ThemeConfigScreen(
var selectedThemeMode by remember { mutableStateOf(ThemeConfig.themeMode) } var selectedThemeMode by remember { mutableStateOf(ThemeConfig.themeMode) }
var selectedTheme by remember { mutableStateOf(ThemeConfig.appTheme) } var selectedTheme by remember { mutableStateOf(ThemeConfig.appTheme) }
var useMiuixMonet by remember { mutableStateOf(ThemeConfig.useMiuixMonet) }
var showRestartDialog by remember { mutableStateOf(false) } var showRestartDialog by remember { mutableStateOf(false) }
var showColorPicker by remember { mutableStateOf(false) } var showColorPicker by remember { mutableStateOf(false) }
var showLauncherIconPicker by remember { mutableStateOf(false) } var showLauncherIconPicker by remember { mutableStateOf(false) }
@@ -202,6 +203,37 @@ fun ThemeConfigScreen(
OldThemeConfig.applyDayNight(context) 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 { } else {
ThemeModeSelector( ThemeModeSelector(
selectedMode = selectedThemeMode, selectedMode = selectedThemeMode,
@@ -249,10 +281,14 @@ fun ThemeConfigScreen(
} }
SplicedColumnGroup { SplicedColumnGroup {
SwitchSettingItem( DropdownListSettingItem(
title = stringResource(R.string.pure_black), title = stringResource(R.string.compose_engine),
checked = ThemeConfig.isPureBlack, selectedValue = ThemeConfig.composeEngine,
onCheckedChange = { ThemeConfig.isPureBlack = it } displayEntries = stringArrayResource(R.array.composeEngine),
entryValues = stringArrayResource(R.array.composeEngine_value),
onValueChange = {
ThemeConfig.composeEngine = it
}
) )
ClickableSettingItem( ClickableSettingItem(
title = stringResource(R.string.change_icon), title = stringResource(R.string.change_icon),
@@ -268,15 +304,6 @@ fun ThemeConfigScreen(
context.toastOnUi(R.string.restart_to_apply) 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( SliderSettingItem(
title = stringResource(R.string.font_scale), title = stringResource(R.string.font_scale),
description = stringResource( 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)) { SplicedColumnGroup(title = stringResource(R.string.custom_theme)) {
ClickableSettingItem( ClickableSettingItem(
title = stringResource(R.string.seed_color), title = stringResource(R.string.seed_color),
@@ -426,11 +454,18 @@ fun ThemeConfigScreen(
} }
SplicedColumnGroup(title = stringResource(R.string.compose_related)) { SplicedColumnGroup(title = stringResource(R.string.compose_related)) {
SwitchSettingItem( if (!isMiuixEngine) {
title = stringResource(R.string.use_flexible_top_bar), SwitchSettingItem(
checked = ThemeConfig.useFlexibleTopAppBar, title = stringResource(R.string.pure_black),
onCheckedChange = { ThemeConfig.useFlexibleTopAppBar = it } checked = ThemeConfig.isPureBlack,
) onCheckedChange = { ThemeConfig.isPureBlack = it }
)
SwitchSettingItem(
title = stringResource(R.string.use_flexible_top_bar),
checked = ThemeConfig.useFlexibleTopAppBar,
onCheckedChange = { ThemeConfig.useFlexibleTopAppBar = it }
)
}
SwitchSettingItem( SwitchSettingItem(
title = stringResource(R.string.is_blur_enable), title = stringResource(R.string.is_blur_enable),
checked = ThemeConfig.enableBlur, checked = ThemeConfig.enableBlur,
@@ -446,7 +481,7 @@ fun ThemeConfigScreen(
onCheckedChange = { ThemeConfig.enableProgressiveBlur = it } onCheckedChange = { ThemeConfig.enableProgressiveBlur = it }
) )
} }
AnimatedVisibility(visible = !ThemeConfig.enableBlur) { AnimatedVisibility(visible = !isMiuixEngine && !ThemeConfig.enableBlur) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(2.dp) verticalArrangement = Arrangement.spacedBy(2.dp)
) { ) {
@@ -476,18 +511,20 @@ fun ThemeConfigScreen(
) )
} }
} }
SliderSettingItem( if (!isMiuixEngine) {
title = stringResource(R.string.container_opacity), SliderSettingItem(
description = stringResource( title = stringResource(R.string.container_opacity),
R.string.container_opacity_summary, description = stringResource(
ThemeConfig.containerOpacity R.string.container_opacity_summary,
), ThemeConfig.containerOpacity
value = ThemeConfig.containerOpacity.toFloat(), ),
defaultValue = 100f, value = ThemeConfig.containerOpacity.toFloat(),
valueRange = 0f..100f, defaultValue = 100f,
steps = 99, valueRange = 0f..100f,
onValueChange = { ThemeConfig.containerOpacity = it.toInt() } steps = 99,
) onValueChange = { ThemeConfig.containerOpacity = it.toInt() }
)
}
} }
SplicedColumnGroup(title = stringResource(R.string.day)) { SplicedColumnGroup(title = stringResource(R.string.day)) {
@@ -738,7 +775,7 @@ fun ThemeColorButton(
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
border = if (isSelected) BorderStroke( border = if (isSelected) BorderStroke(
borderWidth, borderWidth,
MaterialTheme.colorScheme.primary LegadoTheme.colorScheme.primary
) else null, ) else null,
colors = CardDefaults.cardColors(containerColor = colors.surfaceContainer) colors = CardDefaults.cardColors(containerColor = colors.surfaceContainer)
) { ) {
@@ -801,7 +838,7 @@ fun ThemeColorButton(
AppText( AppText(
text = label, text = label,
style = LegadoTheme.typography.labelSmall, 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 materialVersion: String? = null
): ThemeColorPalette { ): ThemeColorPalette {
val appThemeMode = ThemeResolver.resolveThemeMode(value) val appThemeMode = ThemeResolver.resolveThemeMode(value)
val colorScheme = ThemeManager.getColorScheme( val colorScheme = ThemeEngine.getColorScheme(
context = context, context = context,
mode = appThemeMode, mode = appThemeMode,
darkTheme = isDark, darkTheme = isDark,
@@ -942,7 +979,7 @@ private fun getThemeColors(
materialVersion: String? = null materialVersion: String? = null
): ThemeColors { ): ThemeColors {
val appThemeMode = ThemeResolver.resolveThemeMode(value) val appThemeMode = ThemeResolver.resolveThemeMode(value)
val colorScheme = ThemeManager.getColorScheme( val colorScheme = ThemeEngine.getColorScheme(
context = context, context = context,
mode = appThemeMode, mode = appThemeMode,
darkTheme = isDark, darkTheme = isDark,
@@ -38,6 +38,7 @@ import io.legado.app.help.config.OldThemeConfig
import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.alert.AppAlertDialog 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.card.GlassCard
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
@@ -59,41 +60,27 @@ fun ThemeListDialog(
AppModalBottomSheet( AppModalBottomSheet(
show = show, 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( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 24.dp) .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( LazyColumn(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
@@ -104,8 +91,7 @@ fun ThemeListDialog(
OldThemeConfig.applyConfig(context, item) OldThemeConfig.applyConfig(context, item)
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth(),
.padding(horizontal = 16.dp),
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(20.dp),
containerColor = if (item.primaryColor.toColorInt() == context.primaryColor) { containerColor = if (item.primaryColor.toColorInt() == context.primaryColor) {
MaterialTheme.colorScheme.secondaryContainer 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 package io.legado.app.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MaterialExpressiveTheme
@@ -11,6 +12,8 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext 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 androidx.compose.ui.text.font.FontWeight
import io.legado.app.ui.config.themeConfig.ThemeConfig import io.legado.app.ui.config.themeConfig.ThemeConfig
import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.MiuixTheme
@@ -24,33 +27,56 @@ fun AppTheme(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val appThemeMode = ThemeResolver.resolveThemeMode(ThemeConfig.appTheme) val appThemeMode = ThemeResolver.resolveThemeMode(ThemeConfig.appTheme)
val themeModeValue = ThemeConfig.themeMode
val isPureBlack = ThemeConfig.isPureBlack val isPureBlack = ThemeConfig.isPureBlack
val paletteStyleStr = ThemeConfig.paletteStyle val paletteStyleValue = ThemeConfig.paletteStyle
val materialVersion = ThemeConfig.materialVersion val materialVersion = ThemeConfig.materialVersion
val composeEngine = ThemeConfig.composeEngine val composeEngine = ThemeConfig.composeEngine
val colorSchemeMode = ThemeResolver.resolveColorSchemeMode(ThemeConfig.themeMode) val useMiuixMonet = ThemeConfig.useMiuixMonet
val paletteStyle = val customPrimary = ThemeConfig.cPrimary
remember(paletteStyleStr) { ThemeResolver.resolvePaletteStyle(paletteStyleStr) } val colorSchemeMode = ThemeResolver.resolveColorSchemeMode(themeModeValue)
val seedColor = remember(ThemeConfig.cPrimary) { val miuixColorSchemeMode = remember(themeModeValue, useMiuixMonet) {
if (ThemeConfig.cPrimary != 0) Color(ThemeConfig.cPrimary) else Color(0xFF3482FF) ThemeResolver.resolveMiuixColorSchemeMode(themeModeValue, useMiuixMonet)
} }
val paletteStyle =
remember(paletteStyleValue) { ThemeResolver.resolvePaletteStyle(paletteStyleValue) }
val colorScheme = val colorScheme =
remember(context, appThemeMode, darkTheme, isPureBlack, paletteStyleStr, materialVersion) { remember(
ThemeManager.getColorScheme( context,
appThemeMode,
darkTheme,
isPureBlack,
paletteStyleValue,
materialVersion
) {
ThemeEngine.getColorScheme(
context = context, context = context,
mode = appThemeMode, mode = appThemeMode,
darkTheme = darkTheme, darkTheme = darkTheme,
isAmoled = isPureBlack, isAmoled = isPureBlack,
paletteStyle = paletteStyleStr, paletteStyle = paletteStyleValue,
materialVersion = materialVersion 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( val themeColors = remember(
colorScheme, colorScheme,
darkTheme, darkTheme,
seedColor, themeSeedColor,
paletteStyle, paletteStyle,
colorSchemeMode, colorSchemeMode,
composeEngine composeEngine
@@ -58,7 +84,7 @@ fun AppTheme(
LegadoThemeMode( LegadoThemeMode(
colorScheme = colorScheme, colorScheme = colorScheme,
isDark = darkTheme, isDark = darkTheme,
seedColor = seedColor, seedColor = themeSeedColor,
paletteStyle = paletteStyle, paletteStyle = paletteStyle,
themeMode = colorSchemeMode, themeMode = colorSchemeMode,
useDynamicColor = appThemeMode == AppThemeMode.Dynamic, useDynamicColor = appThemeMode == AppThemeMode.Dynamic,
@@ -70,8 +96,37 @@ fun AppTheme(
LocalLegadoThemeColors provides themeColors LocalLegadoThemeColors provides themeColors
) { ) {
if (ThemeResolver.isMiuixEngine(themeColors.composeEngine)) { if (ThemeResolver.isMiuixEngine(themeColors.composeEngine)) {
val controller = remember(colorSchemeMode, darkTheme) { val keyColor = if (useMiuixMonet &&
ThemeController(colorSchemeMode = colorSchemeMode, isDark = darkTheme) 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) { MiuixTheme(controller = controller) {
@@ -79,34 +134,34 @@ fun AppTheme(
val legadoTypography = remember(miuixStyles) { val legadoTypography = remember(miuixStyles) {
LegadoTypography( LegadoTypography(
headlineLarge = miuixStyles.title1, headlineLarge = miuixStyles.title1,
headlineLargeEmphasized = miuixStyles.title1.copy(fontWeight = FontWeight.Medium), headlineLargeEmphasized = miuixStyles.title1.emphasized(),
headlineMedium = miuixStyles.title2, headlineMedium = miuixStyles.title2,
headlineMediumEmphasized = miuixStyles.title2.copy(fontWeight = FontWeight.Medium), headlineMediumEmphasized = miuixStyles.title2.emphasized(),
headlineSmall = miuixStyles.title3, headlineSmall = miuixStyles.title3,
headlineSmallEmphasized = miuixStyles.title3.copy(fontWeight = FontWeight.Medium), headlineSmallEmphasized = miuixStyles.title3.emphasized(),
titleLarge = miuixStyles.headline1, titleLarge = miuixStyles.headline1,
titleLargeEmphasized = miuixStyles.headline1.copy(fontWeight = FontWeight.Medium), titleLargeEmphasized = miuixStyles.headline1.emphasized(),
titleMedium = miuixStyles.headline2, titleMedium = miuixStyles.headline2,
titleMediumEmphasized = miuixStyles.headline2.copy(fontWeight = FontWeight.Medium), titleMediumEmphasized = miuixStyles.headline2.emphasized(),
titleSmall = miuixStyles.subtitle, titleSmall = miuixStyles.subtitle,
titleSmallEmphasized = miuixStyles.subtitle.copy(fontWeight = FontWeight.Medium), titleSmallEmphasized = miuixStyles.subtitle.emphasized(),
bodyLarge = miuixStyles.paragraph, bodyLarge = miuixStyles.paragraph,
bodyLargeEmphasized = miuixStyles.paragraph.copy(fontWeight = FontWeight.Medium), bodyLargeEmphasized = miuixStyles.paragraph.emphasized(),
bodyMedium = miuixStyles.body1, bodyMedium = miuixStyles.body1,
bodyMediumEmphasized = miuixStyles.body1.copy(fontWeight = FontWeight.Medium), bodyMediumEmphasized = miuixStyles.body1.emphasized(),
bodySmall = miuixStyles.body2, bodySmall = miuixStyles.body2,
bodySmallEmphasized = miuixStyles.body2.copy(fontWeight = FontWeight.Medium), bodySmallEmphasized = miuixStyles.body2.emphasized(),
labelLarge = miuixStyles.button, labelLarge = miuixStyles.button,
labelLargeEmphasized = miuixStyles.button.copy(fontWeight = FontWeight.Medium), labelLargeEmphasized = miuixStyles.button.emphasized(),
labelMedium = miuixStyles.footnote1, labelMedium = miuixStyles.footnote1,
labelMediumEmphasized = miuixStyles.footnote1.copy(fontWeight = FontWeight.Medium), labelMediumEmphasized = miuixStyles.footnote1.emphasized(),
labelSmall = miuixStyles.footnote2, labelSmall = miuixStyles.footnote2,
labelSmallEmphasized = miuixStyles.footnote2.copy(fontWeight = FontWeight.Medium) labelSmallEmphasized = miuixStyles.footnote2.emphasized()
) )
} }
@@ -130,39 +185,28 @@ fun AppTheme(
tertiaryContainer = miuixColorScheme.primaryContainer, tertiaryContainer = miuixColorScheme.primaryContainer,
onTertiaryContainer = miuixColorScheme.primaryVariant, onTertiaryContainer = miuixColorScheme.primaryVariant,
// ================= 4. 背景与表面 (Background & Surface) =================
background = miuixColorScheme.background, background = miuixColorScheme.background,
onBackground = miuixColorScheme.onBackground, onBackground = miuixColorScheme.onBackground,
surface = miuixColorScheme.surface, surface = miuixColorScheme.surface,
onSurface = miuixColorScheme.onSurface, onSurface = miuixColorScheme.onSurface,
surfaceVariant = miuixColorScheme.surfaceVariant, surfaceVariant = miuixColorScheme.surfaceVariant,
// M3 的 onSurfaceVariant 通常是次级文字色。Miuix 的 onSurfaceSecondary 完美契合这个语义
onSurfaceVariant = miuixColorScheme.onSurfaceSecondary, onSurfaceVariant = miuixColorScheme.onSurfaceSecondary,
// M3 中用于给 Surface 叠加一层极淡主题色的属性,通常直接取 primary
surfaceTint = miuixColorScheme.primary, surfaceTint = miuixColorScheme.primary,
// Inverse 系列通常用于深色模式下的反色提示(如 Snackbar)。
// 简单映射法:直接用现有的 onSurface 和 surface 交叉互换。
inverseSurface = miuixColorScheme.onSurface, inverseSurface = miuixColorScheme.onSurface,
inverseOnSurface = miuixColorScheme.surface, inverseOnSurface = miuixColorScheme.surface,
// ================= 5. 错误状态 (Error) =================
error = miuixColorScheme.error, error = miuixColorScheme.error,
onError = miuixColorScheme.onError, onError = miuixColorScheme.onError,
errorContainer = miuixColorScheme.errorContainer, errorContainer = miuixColorScheme.errorContainer,
onErrorContainer = miuixColorScheme.onErrorContainer, onErrorContainer = miuixColorScheme.onErrorContainer,
// ================= 6. 边框、分割线与遮罩 (Outline & Scrim) =================
outline = miuixColorScheme.outline, outline = miuixColorScheme.outline,
// outlineVariant 在 M3 中常用于分割线。Miuix 刚好有 dividerLine
outlineVariant = miuixColorScheme.dividerLine, outlineVariant = miuixColorScheme.dividerLine,
// scrim 是 M3 的遮罩层(如弹窗背后的阴影)。Miuix 刚好有 windowDimming
scrim = miuixColorScheme.windowDimming, scrim = miuixColorScheme.windowDimming,
surfaceBright = miuixColorScheme.surface, // Miuix 缺省,用 surface 兜底 surfaceBright = miuixColorScheme.surface,
surfaceDim = miuixColorScheme.background, // Miuix 缺省,用 background 兜底 surfaceDim = miuixColorScheme.background,
surfaceContainer = miuixColorScheme.surfaceContainer, surfaceContainer = miuixColorScheme.surfaceContainer,
surfaceContainerHigh = miuixColorScheme.surfaceContainerHigh, surfaceContainerHigh = miuixColorScheme.surfaceContainerHigh,
surfaceContainerHighest = miuixColorScheme.surfaceContainerHighest, surfaceContainerHighest = miuixColorScheme.surfaceContainerHighest,
@@ -182,7 +226,7 @@ fun AppTheme(
onTertiaryFixed = miuixColorScheme.onTertiaryContainer, onTertiaryFixed = miuixColorScheme.onTertiaryContainer,
onTertiaryFixedVariant = miuixColorScheme.onTertiaryContainer, onTertiaryFixedVariant = miuixColorScheme.onTertiaryContainer,
cardContainer = miuixColorScheme.tertiaryContainer, cardContainer = miuixColorScheme.disabledPrimary,
onCardContainer = miuixColorScheme.primary onCardContainer = miuixColorScheme.primary
) )
} }
@@ -195,102 +239,17 @@ fun AppTheme(
} }
} }
} else { } else {
val Typography = Typography() val materialTypography = remember { Typography() }
MaterialExpressiveTheme( MaterialExpressiveTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = Typography, typography = materialTypography,
motionScheme = MotionScheme.expressive(), motionScheme = MotionScheme.expressive(),
shapes = Shapes() shapes = Shapes()
) { ) {
val legadoTypography = remember(Typography) { val legadoTypography = remember(materialTypography) {
LegadoTypography( materialTypography.toLegadoTypography()
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 semanticColors = remember(colorScheme) { colorScheme.toLegadoColorScheme() }
CompositionLocalProvider( CompositionLocalProvider(
LocalLegadoTypography provides legadoTypography, 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, Yuuka,
Phoebe, Phoebe,
Mujika, Mujika,
CUSTOM, Custom,
Transparent 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.runtime.remember
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import io.legado.app.ui.config.themeConfig.ThemeConfig 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 @Composable
fun rememberOpaqueColorScheme(): ColorScheme { fun rememberOpaqueColorScheme(): ColorScheme {
@@ -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 com.materialkolor.dynamiccolor.ColorSpec
import io.legado.app.ui.config.themeConfig.ThemeConfig import io.legado.app.ui.config.themeConfig.ThemeConfig
import top.yukonga.miuix.kmp.theme.ColorSchemeMode 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 { object ThemeResolver {
fun resolveThemeMode(value: String): AppThemeMode = when (value) { private const val COMPOSE_ENGINE_MIUIX = "miuix"
"0" -> AppThemeMode.Dynamic private const val MATERIAL_VERSION_EXPRESSIVE = "material3Expressive"
"1" -> AppThemeMode.GR
"2" -> AppThemeMode.Lemon private val appThemeModes = mapOf(
"3" -> AppThemeMode.WH "0" to AppThemeMode.Dynamic,
"4" -> AppThemeMode.Elink "1" to AppThemeMode.GR,
"5" -> AppThemeMode.Sora "2" to AppThemeMode.Lemon,
"6" -> AppThemeMode.August "3" to AppThemeMode.WH,
"7" -> AppThemeMode.Carlotta "4" to AppThemeMode.Elink,
"8" -> AppThemeMode.Koharu "5" to AppThemeMode.Sora,
"9" -> AppThemeMode.Yuuka "6" to AppThemeMode.August,
"10" -> AppThemeMode.Phoebe "7" to AppThemeMode.Carlotta,
"11" -> AppThemeMode.Mujika "8" to AppThemeMode.Koharu,
"12" -> AppThemeMode.CUSTOM "9" to AppThemeMode.Yuuka,
"13" -> AppThemeMode.Transparent "10" to AppThemeMode.Phoebe,
else -> AppThemeMode.Dynamic "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 { fun resolvePaletteStyle(value: String?): PaletteStyle {
return when (value) { return materialPaletteStyles[value] ?: PaletteStyle.TonalSpot
"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
}
} }
fun resolveContrastLevel(): Double { fun resolveContrastLevel(): Double {
return try { return runCatching { Contrast.valueOf(ThemeConfig.customContrast).value }
Contrast.valueOf(ThemeConfig.customContrast).value .getOrDefault(Contrast.Default.value)
} catch (e: Exception) { }
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) { fun resolveMiuixColorSchemeMode(
"0" -> ColorSchemeMode.System value: String,
"1" -> ColorSchemeMode.Light useMonet: Boolean
"2" -> ColorSchemeMode.Dark ): ColorSchemeMode {
else -> ColorSchemeMode.System 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 { fun resolveColorSpecVersion(colorSpec: ThemeColorSpec): ColorSpec.SpecVersion {
return when (colorSpec) { return when (colorSpec) {
@@ -66,11 +108,28 @@ object ThemeResolver {
} }
fun resolveColorSpecFromMaterialVersion(value: String?): ThemeColorSpec { fun resolveColorSpecFromMaterialVersion(value: String?): ThemeColorSpec {
return when (value) { return if (value == MATERIAL_VERSION_EXPRESSIVE) {
"material3Expressive" -> ThemeColorSpec.SPEC_2025 ThemeColorSpec.SPEC_2025
"material3" -> ThemeColorSpec.SPEC_2021 } else {
else -> ThemeColorSpec.SPEC_2021 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.material3.rememberTooltipState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
@@ -20,6 +21,8 @@ fun AppFloatingActionButton(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
tooltipText: String? = null, tooltipText: String? = null,
containerColor: Color = LegadoTheme.colorScheme.primaryContainer,
contentColor: Color = LegadoTheme.colorScheme.onPrimaryContainer,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine) val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
@@ -42,6 +45,8 @@ fun AppFloatingActionButton(
FloatingActionButton( FloatingActionButton(
onClick = onClick, onClick = onClick,
modifier = modifier, modifier = modifier,
containerColor = containerColor,
contentColor = contentColor,
content = content content = content
) )
} }
@@ -49,6 +54,8 @@ fun AppFloatingActionButton(
FloatingActionButton( FloatingActionButton(
onClick = onClick, onClick = onClick,
modifier = modifier, modifier = modifier,
containerColor = containerColor,
contentColor = contentColor,
content = content content = content
) )
} }
@@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -18,6 +20,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.button.MiuixPrimaryButton import io.legado.app.ui.widget.components.button.MiuixPrimaryButton
@@ -91,6 +94,11 @@ fun AppAlertDialog(
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
modifier = modifier, 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) } }, title = title?.let { { Text(text = it) } },
text = { text = {
Column { Column {
@@ -108,7 +116,10 @@ fun AppAlertDialog(
confirmButton = { confirmButton = {
if (onConfirm != null) { if (onConfirm != null) {
OutlinedButton( OutlinedButton(
onClick = onConfirm onClick = onConfirm,
colors = ButtonDefaults.outlinedButtonColors(
contentColor = LegadoTheme.colorScheme.primary
)
) { ) {
Text(text = confirmText) Text(text = confirmText)
} }
@@ -120,7 +131,10 @@ fun AppAlertDialog(
onClick = { onClick = {
onDismiss() onDismiss()
onDismissRequest() onDismissRequest()
} },
colors = ButtonDefaults.textButtonColors(
contentColor = LegadoTheme.colorScheme.primary
)
) { ) {
Text(text = dismissText) Text(text = dismissText)
} }
@@ -1,10 +1,16 @@
package io.legado.app.ui.widget.components.button 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.Button
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.basic.Button as MiuixButton import top.yukonga.miuix.kmp.basic.Button as MiuixButton
@@ -72,3 +78,46 @@ 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.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
@@ -42,6 +43,7 @@ import top.yukonga.miuix.kmp.basic.Text as MiuixText
fun MediumIconButton( fun MediumIconButton(
onClick: () -> Unit, onClick: () -> Unit,
imageVector: ImageVector, imageVector: ImageVector,
tint: Color = LegadoTheme.colorScheme.onSurface,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
contentDescription: String? = null contentDescription: String? = null
) { ) {
@@ -52,7 +54,8 @@ fun MediumIconButton(
) { ) {
MiuixIcon( MiuixIcon(
imageVector = imageVector, imageVector = imageVector,
contentDescription = contentDescription contentDescription = contentDescription,
tint = tint
) )
} }
} else { } else {
@@ -62,7 +65,8 @@ fun MediumIconButton(
) { ) {
Icon( Icon(
imageVector = imageVector, 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.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedIconButton import androidx.compose.material3.OutlinedIconButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ToggleButton import androidx.compose.material3.ToggleButton
@@ -72,7 +71,7 @@ fun TopBarButton(
modifier = commonModifier, modifier = commonModifier,
colors = IconButtonDefaults.filledTonalIconButtonColors( colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = GlassTopAppBarDefaults.controlContainerColor(), containerColor = GlassTopAppBarDefaults.controlContainerColor(),
contentColor = MaterialTheme.colorScheme.onSurface contentColor = LegadoTheme.colorScheme.onSurface
) )
) { ) {
AnimatedIcon( AnimatedIcon(
@@ -162,7 +161,7 @@ fun TopbarNavigationButton(
fun TopBarActionButton( fun TopBarActionButton(
onClick: () -> Unit, onClick: () -> Unit,
imageVector: ImageVector, imageVector: ImageVector,
contentDescription: String?, contentDescription: String? = "",
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val enableProgressive = ThemeConfig.enableProgressiveBlur val enableProgressive = ThemeConfig.enableProgressiveBlur
@@ -6,7 +6,6 @@ import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CardElevation import androidx.compose.material3.CardElevation
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -37,8 +36,8 @@ private fun BaseCard(
) { ) {
if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) { if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
val colors = MiuixCardDefaults.defaultColors( val colors = MiuixCardDefaults.defaultColors(
color = (containerColor ?: MiuixTheme.colorScheme.secondaryContainer.copy(alpha)), color = (containerColor ?: LegadoTheme.colorScheme.secondaryContainer.copy(alpha)),
contentColor = contentColor ?: MiuixTheme.colorScheme.onSurface contentColor = contentColor ?: LegadoTheme.colorScheme.onSurface
) )
MiuixCard( MiuixCard(
modifier = modifier, modifier = modifier,
@@ -51,12 +50,12 @@ private fun BaseCard(
) )
} else { } else {
val colors = CardDefaults.cardColors( val colors = CardDefaults.cardColors(
containerColor = (containerColor ?: MaterialTheme.colorScheme.secondaryContainer).copy( containerColor = (containerColor ?: LegadoTheme.colorScheme.secondaryContainer).copy(
alpha = alpha alpha = alpha
), ),
contentColor = contentColor ?: MaterialTheme.colorScheme.onSecondaryContainer, contentColor = contentColor ?: LegadoTheme.colorScheme.onSecondaryContainer,
disabledContainerColor = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = alpha * 0.38f), disabledContainerColor = LegadoTheme.colorScheme.onSecondaryContainer.copy(alpha = alpha * 0.38f),
disabledContentColor = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = alpha * 0.38f) disabledContentColor = LegadoTheme.colorScheme.onSecondaryContainer.copy(alpha = alpha * 0.38f)
) )
if (onClick != null) { if (onClick != null) {
Card( Card(
@@ -56,6 +56,7 @@ fun SelectionItemCard(
title: String, title: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
subtitle: String? = null, subtitle: String? = null,
supportingContent: @Composable (() -> Unit)? = null,
isEnabled: Boolean = true, isEnabled: Boolean = true,
isSelected: Boolean = false, isSelected: Boolean = false,
inSelectionMode: Boolean = false, inSelectionMode: Boolean = false,
@@ -75,10 +76,10 @@ fun SelectionItemCard(
val animatedContainerColor by animateColorAsState( val animatedContainerColor by animateColorAsState(
targetValue = if (isSelected) targetValue = if (isSelected)
selectedContainerColor selectedContainerColor
?: if (composeEngine) MiuixTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.secondaryContainer ?: if (composeEngine) LegadoTheme.colorScheme.secondaryContainer else LegadoTheme.colorScheme.secondaryContainer
else else
containerColor 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), animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing),
label = "CardColor" label = "CardColor"
) )
@@ -130,13 +131,16 @@ fun SelectionItemCard(
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
if (!subtitle.isNullOrBlank()) { when {
AppText( supportingContent != null -> supportingContent()
text = subtitle, !subtitle.isNullOrBlank() -> {
style = LegadoTheme.typography.bodySmall, AppText(
maxLines = 1, text = subtitle,
overflow = TextOverflow.Ellipsis style = LegadoTheme.typography.bodySmall,
) maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
} }
} }
} else { } else {
@@ -150,16 +154,20 @@ fun SelectionItemCard(
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
}, },
supportingContent = if (!subtitle.isNullOrBlank()) { supportingContent = when {
{ supportingContent != null -> supportingContent
AppText( !subtitle.isNullOrBlank() -> {
text = subtitle, {
style = LegadoTheme.typography.bodySmall, AppText(
maxLines = 1, text = subtitle,
overflow = TextOverflow.Ellipsis style = LegadoTheme.typography.bodySmall,
) maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
} }
} else null, else -> null
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent) colors = ListItemDefaults.colors(containerColor = Color.Transparent)
) )
} }
@@ -218,6 +226,7 @@ fun LazyItemScope.ReorderableSelectionItem(
title: String, title: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
subtitle: String? = null, subtitle: String? = null,
supportingContent: @Composable (() -> Unit)? = null,
isEnabled: Boolean = true, isEnabled: Boolean = true,
isSelected: Boolean = false, isSelected: Boolean = false,
inSelectionMode: Boolean = false, inSelectionMode: Boolean = false,
@@ -242,6 +251,7 @@ fun LazyItemScope.ReorderableSelectionItem(
SelectionItemCard( SelectionItemCard(
title = title, title = title,
subtitle = subtitle, subtitle = subtitle,
supportingContent = supportingContent,
isEnabled = isEnabled, isEnabled = isEnabled,
isSelected = isSelected, isSelected = isSelected,
inSelectionMode = inSelectionMode, inSelectionMode = inSelectionMode,
@@ -31,7 +31,7 @@ fun TextCard(
contentColor: Color? = null, contentColor: Color? = null,
cornerRadius: Dp = 8.dp, cornerRadius: Dp = 8.dp,
horizontalPadding: Dp = 8.dp, horizontalPadding: Dp = 8.dp,
verticalPadding: Dp = 2.dp, verticalPadding: Dp = 4.dp,
iconSize: Dp = 14.dp, iconSize: Dp = 14.dp,
spacing: Dp = 4.dp, spacing: Dp = 4.dp,
textStyle: TextStyle = LegadoTheme.typography.labelSmallEmphasized 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.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape 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.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.stringResource 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 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.R
import io.legado.app.ui.theme.LegadoTheme 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.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -52,111 +53,50 @@ fun ColorPickerSheet(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onColorSelected: (Int) -> Unit onColorSelected: (Int) -> Unit
) { ) {
val controller = rememberColorPickerController()
var currentColor by remember { mutableStateOf(Color(initialColor)) } var currentColor by remember { mutableStateOf(Color(initialColor)) }
var hexInput by remember { mutableStateOf(initialColor.asHexColorString()) }
var isHexInputError by remember { mutableStateOf(false) }
val presets = remember { LaunchedEffect(show, initialColor) {
listOf( if (show) {
Color(0xFFF44336), currentColor = Color(initialColor)
Color(0xFFE91E63), hexInput = initialColor.asHexColorString()
Color(0xFF9C27B0), isHexInputError = false
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)
)
} }
AppModalBottomSheet(show = show, onDismissRequest = onDismissRequest) { val parsedHexColor = parseHexColor(hexInput)
AppModalBottomSheet(
show = show,
onDismissRequest = onDismissRequest,
title = "选择颜色",
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(bottom = 16.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
AppText( ColorPalette(
text = "颜色选择", color = currentColor,
style = LegadoTheme.typography.titleLarge, onColorChanged = { color ->
modifier = Modifier.padding(bottom = 16.dp) currentColor = color
hexInput = color.toArgb().asHexColorString()
isHexInputError = false
},
rows = 8,
hueColumns = 12,
modifier = Modifier.fillMaxWidth(),
showPreview = false
) )
HsvColorPicker( Spacer(modifier = Modifier.height(32.dp))
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))
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier.fillMaxWidth()
.fillMaxWidth()
.padding(horizontal = 10.dp)
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -165,32 +105,68 @@ fun ColorPickerSheet(
.background(currentColor) .background(currentColor)
.border( .border(
1.dp, 1.dp,
MaterialTheme.colorScheme.outlineVariant, LegadoTheme.colorScheme.outlineVariant,
RoundedCornerShape(8.dp) RoundedCornerShape(8.dp)
) )
) )
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
AppText( AppTextField(
text = "#${Integer.toHexString(currentColor.toArgb()).uppercase()}", value = hexInput,
style = LegadoTheme.typography.bodyLarge onValueChange = { value ->
) hexInput = normalizeHexInput(value)
val parsedColor = parseHexColor(hexInput)
Row( if (parsedColor != null) {
currentColor = Color(parsedColor)
isHexInputError = false
} else {
isHexInputError = hexInput.isNotBlank()
}
},
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End label = "色值",
) { singleLine = true,
TextButton(onClick = onDismissRequest) { isError = isHexInputError,
AppText(stringResource(R.string.cancel)) backgroundColor = LegadoTheme.colorScheme.surface,
} keyboardOptions = KeyboardOptions(
Spacer(modifier = Modifier.width(8.dp)) capitalization = KeyboardCapitalization.Characters,
Button(onClick = { keyboardType = KeyboardType.Ascii,
onColorSelected(currentColor.toArgb()) imeAction = ImeAction.Done
onDismissRequest() )
}) { )
AppText(stringResource(R.string.ok))
}
}
} }
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 package io.legado.app.ui.widget.components.menuItem
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -13,15 +14,20 @@ import androidx.compose.material3.MotionScheme
import androidx.compose.material3.Shapes import androidx.compose.material3.Shapes
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme 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.ThemeResolver
import io.legado.app.ui.theme.rememberOpaqueColorScheme import io.legado.app.ui.theme.rememberOpaqueColorScheme
import top.yukonga.miuix.kmp.basic.ListPopupColumn import top.yukonga.miuix.kmp.basic.ListPopupColumn
import top.yukonga.miuix.kmp.overlay.OverlayListPopup import top.yukonga.miuix.kmp.overlay.OverlayListPopup
import top.yukonga.miuix.kmp.window.WindowListPopup
val LocalUseMiuixWindowPopup = staticCompositionLocalOf { false }
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
@@ -29,25 +35,28 @@ fun RoundDropdownMenu(
expanded: Boolean, expanded: Boolean,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
// M3 专属视觉参数,Miuix 模式下会优雅降级(忽略)
shape: Shape = MaterialTheme.shapes.medium, shape: Shape = MaterialTheme.shapes.medium,
shadowElevation: Dp = 4.dp, shadowElevation: Dp = 4.dp,
verticalSpacing: Dp = 8.dp, verticalSpacing: Dp = 8.dp,
content: @Composable ColumnScope.(dismiss: () -> Unit) -> Unit content: @Composable ColumnScope.(dismiss: () -> Unit) -> Unit
) { ) {
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine) val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
val popupContainerColor = LegadoTheme.colorScheme.surfaceContainer
if (isMiuix) { if (isMiuix) {
val popupContentColor = LegadoTheme.colorScheme.onSurface
OverlayListPopup( OverlayListPopup(
show = expanded, show = expanded,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
popupModifier = modifier popupModifier = modifier
) { ) {
ListPopupColumn { ProvideAppContentColor(popupContentColor) {
Column() { ListPopupColumn {
Spacer(Modifier.height(12.dp)) Column(modifier = Modifier.background(popupContainerColor)) {
content(onDismissRequest) Spacer(Modifier.height(12.dp))
Spacer(Modifier.height(12.dp)) content(onDismissRequest)
Spacer(Modifier.height(12.dp))
}
} }
} }
} }
@@ -1,5 +1,6 @@
package io.legado.app.ui.widget.components.menuItem package io.legado.app.ui.widget.components.menuItem
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement 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.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn 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.LocalContentColor
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -26,6 +29,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp 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.ThemeResolver
import io.legado.app.ui.theme.rememberOpaqueColorScheme import io.legado.app.ui.theme.rememberOpaqueColorScheme
import io.legado.app.ui.widget.components.icon.AppIcon import io.legado.app.ui.widget.components.icon.AppIcon
import top.yukonga.miuix.kmp.basic.DropdownDefaults
import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.basic.Check
import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.basic.Text as MiuixText import top.yukonga.miuix.kmp.basic.Text as MiuixText
@Composable @Composable
fun RoundDropdownMenuItem( fun RoundDropdownMenuItem(
text: String, text: String,
color: Color = LegadoTheme.colorScheme.surface, color: Color = Color.Unspecified,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isSelected: Boolean = false, isSelected: Boolean = false,
@@ -51,20 +58,21 @@ fun RoundDropdownMenuItem(
) { ) {
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine) val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
val interaction = interactionSource ?: remember { MutableInteractionSource() } val interaction = interactionSource ?: remember { MutableInteractionSource() }
val hasCustomContentColor = color != Color.Unspecified
if (isMiuix) { if (isMiuix) {
val backgroundColor = if (isSelected) { val dropdownColors = DropdownDefaults.dropdownColors()
LegadoTheme.colorScheme.primaryContainer val (textColor, backgroundColor) = if (isSelected) {
dropdownColors.selectedContentColor to dropdownColors.selectedContainerColor
} else {
dropdownColors.contentColor to dropdownColors.containerColor
}
val checkColor = if (isSelected) {
dropdownColors.selectedContentColor
} else { } else {
Color.Transparent Color.Transparent
} }
val contentColor = if (isSelected) {
LegadoTheme.colorScheme.onPrimaryContainer
} else {
LegadoTheme.colorScheme.onSurface
}
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
@@ -73,16 +81,13 @@ fun RoundDropdownMenuItem(
.drawBehind { drawRect(backgroundColor) } .drawBehind { drawRect(backgroundColor) }
.clickable( .clickable(
interactionSource = interaction, interactionSource = interaction,
indication = androidx.compose.foundation.LocalIndication.current, indication = LocalIndication.current,
enabled = enabled, enabled = enabled,
onClick = onClick onClick = onClick
) )
.padding(horizontal = 20.dp, vertical = 12.dp) .padding(horizontal = 20.dp, vertical = 12.dp)
) { ) {
CompositionLocalProvider( CompositionLocalProvider(LocalContentColor provides textColor) {
LocalContentColor provides contentColor,
LocalTextStyle provides MiuixTheme.textStyles.body1
) {
if (leadingIcon != null) { if (leadingIcon != null) {
leadingIcon() leadingIcon()
Spacer(Modifier.width(12.dp)) Spacer(Modifier.width(12.dp))
@@ -93,24 +98,46 @@ fun RoundDropdownMenuItem(
contentAlignment = Alignment.CenterStart contentAlignment = Alignment.CenterStart
) { ) {
MiuixText( MiuixText(
modifier = Modifier.widthIn(max = 200.dp),
text = text, text = text,
fontWeight = FontWeight.Medium fontSize = MiuixTheme.textStyles.body1.fontSize,
fontWeight = FontWeight.Medium,
color = textColor,
) )
} }
if (trailingIcon != null) { if (trailingIcon != null) {
Spacer(Modifier.width(12.dp)) Spacer(Modifier.width(12.dp))
trailingIcon() trailingIcon()
} else {
Spacer(Modifier.width(12.dp))
AppIcon(
imageVector = MiuixIcons.Basic.Check,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = checkColor
)
} }
} }
} }
} else { } else {
val colorScheme = rememberOpaqueColorScheme() val colorScheme = rememberOpaqueColorScheme()
val selectedContentColor = colorScheme.onPrimaryContainer
val defaultContentColor = colorScheme.onSurface
val contentColor = if (enabled) { val contentColor = if (enabled) {
MaterialTheme.colorScheme.onSurface when {
hasCustomContentColor -> color
isSelected -> selectedContentColor
else -> defaultContentColor
}
} else { } 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( Surface(
onClick = onClick, onClick = onClick,
@@ -119,7 +146,8 @@ fun RoundDropdownMenuItem(
.fillMaxWidth(), .fillMaxWidth(),
enabled = enabled, enabled = enabled,
shape = MaterialTheme.shapes.small, shape = MaterialTheme.shapes.small,
color = colorScheme.surface, color = containerColor,
contentColor = contentColor,
interactionSource = interaction interactionSource = interaction
) { ) {
Row( Row(
@@ -141,13 +169,22 @@ fun RoundDropdownMenuItem(
Text( Text(
modifier = Modifier.widthIn(max = 200.dp), modifier = Modifier.widthIn(max = 200.dp),
text = text, text = text,
style = LegadoTheme.typography.bodyMediumEmphasized style = LegadoTheme.typography.bodyMediumEmphasized,
color = contentColor
) )
} }
if (trailingIcon != null) { if (trailingIcon != null) {
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
trailingIcon() 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.Typography
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color 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.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LocalLegadoThemeColors
import io.legado.app.ui.theme.ProvideAppContentColor
import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.theme.rememberOpaqueColorScheme import io.legado.app.ui.widget.components.menuItem.LocalUseMiuixWindowPopup
import top.yukonga.miuix.kmp.window.WindowBottomSheet import top.yukonga.miuix.kmp.window.WindowBottomSheet
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@@ -43,30 +46,56 @@ fun AppModalBottomSheet(
endAction: @Composable (() -> Unit)? = null, endAction: @Composable (() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit 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)) { if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
WindowBottomSheet( WindowBottomSheet(
show = show, show = show,
modifier = modifier, modifier = modifier,
title = title, title = title,
startAction = startAction, startAction = startAction?.let { action ->
endAction = endAction, {
ProvideAppContentColor(sheetContentColor) {
CompositionLocalProvider(LocalUseMiuixWindowPopup provides true) {
action()
}
}
}
},
endAction = endAction?.let { action ->
{
ProvideAppContentColor(sheetContentColor) {
CompositionLocalProvider(LocalUseMiuixWindowPopup provides true) {
action()
}
}
}
},
insideMargin = DpSize(16.dp, 12.dp), insideMargin = DpSize(16.dp, 12.dp),
backgroundColor = sheetContainerColor,
dragHandleColor = sheetDragHandleColor,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onDismissFinished = onDismissRequest, onDismissFinished = onDismissRequest,
enableWindowDim = true, enableWindowDim = true,
allowDismiss = true allowDismiss = true
) { ) {
Column( ProvideAppContentColor(sheetContentColor) {
modifier = Modifier CompositionLocalProvider(LocalUseMiuixWindowPopup provides true) {
.fillMaxWidth() Column(
.animateContentSize(), modifier = Modifier
content = content .fillMaxWidth()
) .animateContentSize(),
content = content
)
}
}
} }
} else { } else {
if (show) { if (show) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val colorScheme = rememberOpaqueColorScheme()
val density = LocalDensity.current val density = LocalDensity.current
val maxHeight = with(density) { val maxHeight = with(density) {
LocalWindowInfo.current.containerSize.height.toDp() * 0.8f LocalWindowInfo.current.containerSize.height.toDp() * 0.8f
@@ -75,8 +104,9 @@ fun AppModalBottomSheet(
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
sheetState = sheetState, sheetState = sheetState,
contentColor = colorScheme.onSurface, containerColor = sheetContainerColor,
dragHandle = { BottomSheetDefaults.DragHandle() } contentColor = sheetContentColor,
dragHandle = { BottomSheetDefaults.DragHandle(color = sheetDragHandleColor) }
) { ) {
MaterialExpressiveTheme( MaterialExpressiveTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
@@ -112,7 +142,7 @@ fun AppModalBottomSheet(
Text( Text(
text = title, text = title,
style = LegadoTheme.typography.titleMediumEmphasized, style = LegadoTheme.typography.titleMediumEmphasized,
color = colorScheme.onSurface, color = sheetContentColor,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
@@ -1,17 +1,12 @@
package io.legado.app.ui.widget.components.settingItem package io.legado.app.ui.widget.components.settingItem
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.input.rememberTextFieldState 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.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -25,7 +20,7 @@ import androidx.compose.ui.unit.dp
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.button.SmallTextButton import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import top.yukonga.miuix.kmp.basic.BasicComponent import top.yukonga.miuix.kmp.basic.BasicComponent
import top.yukonga.miuix.kmp.basic.TextField as MiuixTextField import top.yukonga.miuix.kmp.basic.TextField as MiuixTextField
@@ -76,28 +71,18 @@ fun InputSettingItem(
} }
) )
Row( ConfirmDismissButtonsRow(
modifier = Modifier modifier = Modifier.padding(top = 16.dp),
.fillMaxWidth() onDismiss = {
.padding(top = 8.dp), state.edit { replace(0, length, defaultValue.toString()) }
horizontalArrangement = Arrangement.End },
) { onConfirm = {
SmallTextButton( onConfirm(state.text.toString())
text = "默认", expanded = false
imageVector = Icons.Default.Replay, },
onClick = { dismissText = stringResource(R.string.text_default),
state.edit { replace(0, length, defaultValue.toString()) } confirmText = stringResource(R.string.confirm)
} )
)
SmallTextButton(
text = "确认",
imageVector = Icons.Default.Check,
onClick = {
onConfirm(state.text.toString())
expanded = false
}
)
}
} }
} }
} }
@@ -127,26 +112,18 @@ fun InputSettingItem(
} }
) )
Row( ConfirmDismissButtonsRow(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.padding(top = 16.dp),
horizontalArrangement = Arrangement.End onDismiss = {
) { state.edit { replace(0, length, defaultValue.toString()) }
SmallTextButton( },
text = "默认", onConfirm = {
imageVector = Icons.Default.Replay, onConfirm(state.text.toString())
onClick = { expanded = false
state.edit { replace(0, length, defaultValue.toString()) } },
} dismissText = stringResource(R.string.text_default),
) confirmText = stringResource(R.string.confirm)
SmallTextButton( )
text = "确认",
imageVector = Icons.Default.Check,
onClick = {
onConfirm(state.text.toString())
expanded = false
}
)
}
} }
) )
} }
@@ -2,15 +2,12 @@
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.rememberTextFieldState 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp 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.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver 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 io.legado.app.ui.widget.components.text.AppText
import top.yukonga.miuix.kmp.basic.BasicComponent import top.yukonga.miuix.kmp.basic.BasicComponent
import top.yukonga.miuix.kmp.basic.Slider as MiuixSlider import top.yukonga.miuix.kmp.basic.Slider as MiuixSlider
@@ -78,7 +77,11 @@ fun SliderSettingItem(
MiuixTextField( MiuixTextField(
state = textFieldState, state = textFieldState,
lineLimits = TextFieldLineLimits.SingleLine, 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), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
inputTransformation = { inputTransformation = {
@@ -113,35 +116,26 @@ fun SliderSettingItem(
} }
} }
Row( ConfirmDismissButtonsRow(
modifier = Modifier modifier = Modifier.padding(top = 16.dp),
.fillMaxWidth() onDismiss = { isInputMode = !isInputMode },
.padding(top = 16.dp), onConfirm = {
horizontalArrangement = Arrangement.End onValueChange(defaultValue)
) { textFieldState.edit {
SmallTextButton( replace(
text = if (isInputMode) "婊戝潡" else "杈撳叆", 0,
imageVector = if (isInputMode) Icons.Default.LinearScale else Icons.Default.Edit, length,
onClick = { isInputMode = !isInputMode } defaultValue.toInt().toString()
) )
Spacer(Modifier.width(8.dp))
SmallTextButton(
text = "榛樿",
imageVector = Icons.Default.RestartAlt,
onClick = {
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( TextField(
state = textFieldState, state = textFieldState,
lineLimits = TextFieldLineLimits.SingleLine, 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), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -207,33 +209,26 @@ fun SliderSettingItem(
} }
} }
Row( ConfirmDismissButtonsRow(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.padding(top = 16.dp),
horizontalArrangement = Arrangement.End onDismiss = { isInputMode = !isInputMode },
) { onConfirm = {
SmallTextButton( onValueChange(defaultValue)
text = if (isInputMode) "婊戝潡" else "杈撳叆", textFieldState.edit {
imageVector = if (isInputMode) Icons.Default.LinearScale else Icons.Default.Edit, replace(
onClick = { isInputMode = !isInputMode } 0,
) length,
defaultValue.toInt().toString()
Spacer(Modifier.width(8.dp)) )
SmallTextButton(
text = "榛樿",
imageVector = Icons.Default.RestartAlt,
onClick = {
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,7 +33,7 @@
<io.legado.app.ui.widget.text.TextInputLayout <io.legado.app.ui.widget.text.TextInputLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="80dp" android:layout_marginBottom="240dp"
app:layout_constraintTop_toBottomOf="@+id/tool_bar"> app:layout_constraintTop_toBottomOf="@+id/tool_bar">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
@@ -394,6 +394,7 @@
<string name="imm_navigation_bar">沉浸式导航栏</string> <string name="imm_navigation_bar">沉浸式导航栏</string>
<string name="imm_navigation_bar_s">导航栏颜色透明</string> <string name="imm_navigation_bar_s">导航栏颜色透明</string>
<string name="add_to_bookshelf">放入书架</string> <string name="add_to_bookshelf">放入书架</string>
<string name="click_to_remove">点击删除</string>
<string name="continue_read">继续阅读</string> <string name="continue_read">继续阅读</string>
<string name="cover_path">封面地址</string> <string name="cover_path">封面地址</string>
<string name="page_anim_cover">覆盖</string> <string name="page_anim_cover">覆盖</string>
@@ -1237,6 +1238,8 @@
<string name="refresh_explore">刷新发现</string> <string name="refresh_explore">刷新发现</string>
<string name="ungroup">未分组</string> <string name="ungroup">未分组</string>
<string name="dynamic_colors">动态取色</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="change_icon_success">图标已修改</string>
<string name="replace_rule_title_setting">配置替换规则</string> <string name="replace_rule_title_setting">配置替换规则</string>
<string name="more_setting">更多设置</string> <string name="more_setting">更多设置</string>
@@ -1289,6 +1292,8 @@
<string name="light_mode">浅色</string> <string name="light_mode">浅色</string>
<string name="dark_mode">深色</string> <string name="dark_mode">深色</string>
<string name="pure_black">纯黑深色模式</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="transparent_theme_alarm">透明主题请于设置背景图片后设置</string>
<string name="seed_color_summary">使用种子颜色生成主题颜色</string> <string name="seed_color_summary">使用种子颜色生成主题颜色</string>
<string name="seed_color">种子色</string> <string name="seed_color">种子色</string>
@@ -370,6 +370,7 @@
<string name="imm_navigation_bar">沉浸式导航栏</string> <string name="imm_navigation_bar">沉浸式导航栏</string>
<string name="imm_navigation_bar_s">导航栏颜色透明</string> <string name="imm_navigation_bar_s">导航栏颜色透明</string>
<string name="add_to_bookshelf">放入書架</string> <string name="add_to_bookshelf">放入書架</string>
<string name="click_to_remove">點擊刪除</string>
<string name="continue_read">繼續閲讀</string> <string name="continue_read">繼續閲讀</string>
<string name="cover_path">封面地址</string> <string name="cover_path">封面地址</string>
<string name="page_anim_cover">覆蓋</string> <string name="page_anim_cover">覆蓋</string>
@@ -1223,4 +1224,8 @@
<string name="bottom_bar_opacity_summary">%d</string> <string name="bottom_bar_opacity_summary">%d</string>
<string name="web_file">網頁文件</string> <string name="web_file">網頁文件</string>
<string name="show_book_count">顯示分組書籍數量</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> </resources>
@@ -372,6 +372,7 @@
<string name="imm_navigation_bar">沉浸式導航欄</string> <string name="imm_navigation_bar">沉浸式導航欄</string>
<string name="imm_navigation_bar_s">導航欄顏色透明</string> <string name="imm_navigation_bar_s">導航欄顏色透明</string>
<string name="add_to_bookshelf">放入書架</string> <string name="add_to_bookshelf">放入書架</string>
<string name="click_to_remove">點擊刪除</string>
<string name="continue_read">繼續閱讀</string> <string name="continue_read">繼續閱讀</string>
<string name="cover_path">封面地址</string> <string name="cover_path">封面地址</string>
<string name="page_anim_cover">覆蓋</string> <string name="page_anim_cover">覆蓋</string>
@@ -1225,4 +1226,8 @@
<string name="bottom_bar_opacity_summary">%d</string> <string name="bottom_bar_opacity_summary">%d</string>
<string name="web_file">網頁文件</string> <string name="web_file">網頁文件</string>
<string name="show_book_count">顯示分組書籍數量</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> </resources>
+5
View File
@@ -397,6 +397,7 @@
<string name="imm_navigation_bar">immersion navigation bar</string> <string name="imm_navigation_bar">immersion navigation bar</string>
<string name="imm_navigation_bar_s">The navigation bar becomes transparent</string> <string name="imm_navigation_bar_s">The navigation bar becomes transparent</string>
<string name="add_to_bookshelf">Add to Bookshelf</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="continue_read">Continue reading</string>
<string name="cover_path">Cover path</string> <string name="cover_path">Cover path</string>
<string name="page_anim_cover">Cover</string> <string name="page_anim_cover">Cover</string>
@@ -1240,6 +1241,8 @@
<string name="refresh_explore">Refresh Explore</string> <string name="refresh_explore">Refresh Explore</string>
<string name="ungroup">Ungrouped</string> <string name="ungroup">Ungrouped</string>
<string name="dynamic_colors">Dynamic Colors</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="change_icon_success">Icon Updated</string>
<string name="replace_rule_title_setting">Configure Replace Rules</string> <string name="replace_rule_title_setting">Configure Replace Rules</string>
<string name="more_setting">More Settings</string> <string name="more_setting">More Settings</string>
@@ -1292,6 +1295,8 @@
<string name="light_mode">Light</string> <string name="light_mode">Light</string>
<string name="dark_mode">Dark</string> <string name="dark_mode">Dark</string>
<string name="pure_black">True Black Mode</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="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_summary">Create a theme from a seed color</string>
<string name="seed_color">Seed Color</string> <string name="seed_color">Seed Color</string>