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