From 1bd61c719a8aa8c702542291d52e6d519e0cd55f Mon Sep 17 00:00:00 2001
From: HapeLee <63206378+HapeLee@users.noreply.github.com>
Date: Mon, 13 Apr 2026 01:56:03 +0800
Subject: [PATCH] =?UTF-8?q?[=E4=BC=98=E5=8C=96]=20=E9=87=8D=E5=86=99?=
=?UTF-8?q?=E4=B9=A6=E7=B1=8D=E8=AF=A6=E6=83=85=E9=A1=B5=E3=80=81=E6=9C=AC?=
=?UTF-8?q?=E5=9C=B0=E4=B9=A6=E7=B1=8D=E9=A1=B5=E5=92=8C=E4=BC=98=E5=8C=96?=
=?UTF-8?q?=E4=B8=80=E4=BA=9B=E7=95=8C=E9=9D=A2=EF=BC=8C=E4=B8=BAMiuix?=
=?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=87=AA=E5=AE=9A=E4=B9=89=E5=8F=96=E8=89=B2?=
=?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96=E9=A2=9C=E8=89=B2?=
=?UTF-8?q?=E9=80=89=E6=8B=A9=E5=99=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/src/main/AndroidManifest.xml | 3 +-
.../java/io/legado/app/constant/PreferKey.kt | 1 +
.../main/java/io/legado/app/di/appModule.kt | 6 +
.../book/changecover/ChangeCoverViewModel.kt | 16 +-
.../ChangeBookSourceComposeViewModel.kt | 65 +
.../changesource/ChangeBookSourceViewModel.kt | 28 +-
.../book/changesource/ChangeSourceConfig.kt | 32 +
.../changesource/ObservableSourceConfig.kt | 71 +
.../app/ui/book/info/BookInfoActivity.kt | 1191 ++------------
.../app/ui/book/info/BookInfoContract.kt | 154 ++
.../legado/app/ui/book/info/BookInfoScreen.kt | 1013 ++++++++++++
.../legado/app/ui/book/info/BookInfoSheets.kt | 510 ++++++
.../app/ui/book/info/BookInfoViewModel.kt | 1387 ++++++++++++-----
.../ui/book/info/edit/BookInfoEditScreen.kt | 6 +-
.../themeConfig/LauncherIconPickerSheet.kt | 14 +-
.../app/ui/config/themeConfig/ThemeConfig.kt | 4 +
.../config/themeConfig/ThemeConfigScreen.kt | 111 +-
.../ui/config/themeConfig/ThemeListDialog.kt | 48 +-
.../io/legado/app/ui/theme/AppContentColor.kt | 19 +
.../java/io/legado/app/ui/theme/AppTheme.kt | 250 ++-
.../io/legado/app/ui/theme/AppThemeMode.kt | 2 +-
.../app/ui/theme/ImageSeedColorExtractor.kt | 137 ++
.../legado/app/ui/theme/OpaqueColorScheme.kt | 4 +-
.../app/ui/theme/ThemeColorSchemeOverride.kt | 242 +++
.../io/legado/app/ui/theme/ThemeEngine.kt | 142 ++
.../io/legado/app/ui/theme/ThemeManager.kt | 97 --
.../io/legado/app/ui/theme/ThemeOverride.kt | 100 ++
.../io/legado/app/ui/theme/ThemeResolver.kt | 143 +-
.../components/AppFloatingActionButton.kt | 9 +-
.../widget/components/alert/AppAlertDialog.kt | 20 +-
...pButton.kt => ConfirmDismissButtonsRow.kt} | 51 +-
.../components/button/MediumIconButton.kt | 8 +-
.../widget/components/button/TopBarButton.kt | 5 +-
.../ui/widget/components/card/GlassCard.kt | 13 +-
.../components/card/SelectionItemCard.kt | 46 +-
.../app/ui/widget/components/card/TextCard.kt | 2 +-
.../components/dialog/ColorPickerSheet.kt | 222 ++-
.../components/menuItem/RoundDropdownMenu.kt | 21 +-
.../menuItem/RoundDropdownMenuItem.kt | 77 +-
.../modalBottomSheet/AppModalBottomSheet.kt | 58 +-
.../settingItem/InputSettingItem.kt | 73 +-
.../settingItem/SliderSettingItem.kt | 115 +-
.../components/topbar/MiuixScrollBehavior.kt | 2 +-
.../main/res/layout/dialog_content_edit.xml | 2 +-
app/src/main/res/values-zh-rCN/strings.xml | 5 +
app/src/main/res/values-zh-rHK/strings.xml | 5 +
app/src/main/res/values-zh-rTW/strings.xml | 5 +
app/src/main/res/values/strings.xml | 5 +
48 files changed, 4375 insertions(+), 2165 deletions(-)
create mode 100644 app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceComposeViewModel.kt
create mode 100644 app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceConfig.kt
create mode 100644 app/src/main/java/io/legado/app/ui/book/changesource/ObservableSourceConfig.kt
create mode 100644 app/src/main/java/io/legado/app/ui/book/info/BookInfoContract.kt
create mode 100644 app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt
create mode 100644 app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt
create mode 100644 app/src/main/java/io/legado/app/ui/theme/AppContentColor.kt
create mode 100644 app/src/main/java/io/legado/app/ui/theme/ImageSeedColorExtractor.kt
create mode 100644 app/src/main/java/io/legado/app/ui/theme/ThemeColorSchemeOverride.kt
create mode 100644 app/src/main/java/io/legado/app/ui/theme/ThemeEngine.kt
delete mode 100644 app/src/main/java/io/legado/app/ui/theme/ThemeManager.kt
create mode 100644 app/src/main/java/io/legado/app/ui/theme/ThemeOverride.kt
rename app/src/main/java/io/legado/app/ui/widget/components/button/{AppButton.kt => ConfirmDismissButtonsRow.kt} (57%)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6d8a02238..aa45dc91b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -176,7 +176,8 @@
+ android:launchMode="singleTask"
+ android:windowSoftInputMode="adjustResize|stateHidden" />
()
+ private val _isSearching = MutableStateFlow(false)
+ val isSearching = _isSearching.asStateFlow()
var name: String = ""
var author: String = ""
val searchBooks: MutableList = 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()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceComposeViewModel.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceComposeViewModel.kt
new file mode 100644
index 000000000..21bf22a95
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceComposeViewModel.kt
@@ -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 {
+ return ObservableSourceConfig.bookScoreFlow(searchBook)
+ }
+
+ fun onBookScoreClick(searchBook: SearchBook) {
+ val currentScore = ObservableSourceConfig.getBookScore(searchBook)
+ setBookScore(searchBook, if (currentScore > 0) 0 else 1)
+ }
+}
diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt
index ed2f54e1c..2f8a37956 100644
--- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt
+++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt
@@ -62,6 +62,8 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
private val threadCount = AppConfig.threadCount
private var searchPool: ExecutorCoroutineDispatcher? = null
val searchStateData = MutableLiveData()
+ 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
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceConfig.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceConfig.kt
new file mode 100644
index 000000000..7058dbdcd
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceConfig.kt
@@ -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
+ )
+}
diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ObservableSourceConfig.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ObservableSourceConfig.kt
new file mode 100644
index 000000000..88f8ca706
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/book/changesource/ObservableSourceConfig.kt
@@ -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>()
+ private val sourceScoreFlows = ConcurrentHashMap>()
+
+ init {
+ sp.registerOnSharedPreferenceChangeListener(this)
+ }
+
+ fun bookScoreFlow(searchBook: SearchBook): StateFlow {
+ return bookScoreFlow(searchBook.origin, searchBook.name, searchBook.author)
+ }
+
+ fun bookScoreFlow(origin: String, name: String, author: String): StateFlow {
+ val key = bookScoreKey(origin, name, author)
+ return bookScoreFlows.getOrPut(key) {
+ MutableStateFlow(sp.getInt(key, 0))
+ }.asStateFlow()
+ }
+
+ fun sourceScoreFlow(origin: String): StateFlow {
+ 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}"
+ }
+}
diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
index 8410f6d5a..674b88e8f 100644
--- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
+++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
@@ -1,131 +1,43 @@
package io.legado.app.ui.book.info
-import android.animation.ValueAnimator
-import android.annotation.SuppressLint
-import android.content.Context
import android.content.Intent
-import android.content.res.ColorStateList
-import android.content.res.Configuration
-import android.graphics.Color
-import android.graphics.RenderEffect
-import android.graphics.Shader
-import android.graphics.drawable.BitmapDrawable
-import android.graphics.drawable.Drawable
-import android.graphics.drawable.GradientDrawable
-import android.graphics.drawable.TransitionDrawable
-import android.net.Uri
-import android.os.Build
import android.os.Bundle
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View
-import android.widget.CheckBox
-import android.widget.LinearLayout
-import androidx.activity.addCallback
-import androidx.core.app.ActivityCompat
-import androidx.core.app.ActivityOptionsCompat
-import androidx.core.content.ContextCompat
-import androidx.core.graphics.scale
-import androidx.core.view.doOnPreDraw
-import androidx.lifecycle.lifecycleScope
-import androidx.transition.TransitionManager
-import com.google.android.material.color.DynamicColors
-import com.google.android.material.color.DynamicColorsOptions
-import com.google.android.material.color.MaterialColors
-import com.google.android.material.transition.platform.MaterialContainerTransform
-import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
+import androidx.activity.compose.setContent
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.legado.app.R
-import io.legado.app.base.VMBaseActivity
-import io.legado.app.constant.BookType
-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.BookSource
-import io.legado.app.databinding.ActivityBookInfoBinding
-import io.legado.app.databinding.DialogEditTextBinding
-import io.legado.app.help.book.addType
+import io.legado.app.base.BaseComposeActivity
+import io.legado.app.help.config.AppConfig
import io.legado.app.help.book.isAudio
import io.legado.app.help.book.isImage
import io.legado.app.help.book.isLocal
-import io.legado.app.help.book.isLocalTxt
-import io.legado.app.help.book.isWebFile
-import io.legado.app.help.book.removeType
-import io.legado.app.help.config.AppConfig
-import io.legado.app.help.config.LocalConfig
-import io.legado.app.lib.dialogs.alert
-import io.legado.app.lib.dialogs.selector
-import io.legado.app.model.BookCover
import io.legado.app.model.SourceCallBack
-import io.legado.app.ui.about.AppLogDialog
import io.legado.app.ui.book.audio.AudioPlayActivity
-import io.legado.app.ui.book.changecover.ChangeCoverDialog
-import io.legado.app.ui.book.changesource.ChangeBookSourceDialog
-import io.legado.app.ui.book.group.GroupSelectDialog
import io.legado.app.ui.book.info.edit.BookInfoEditActivity
import io.legado.app.ui.book.manga.ReadMangaActivity
import io.legado.app.ui.book.read.ReadBookActivity
-import io.legado.app.ui.book.read.ReadBookActivity.Companion.RESULT_DELETED
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.book.toc.TocActivityResult
import io.legado.app.ui.file.HandleFileContract
import io.legado.app.ui.login.SourceLoginActivity
-import io.legado.app.ui.widget.dialog.PhotoDialog
import io.legado.app.ui.widget.dialog.VariableDialog
-import io.legado.app.ui.widget.dialog.WaitDialog
-import io.legado.app.utils.ConvertUtils
-import io.legado.app.utils.FileDoc
-import io.legado.app.utils.GSON
import io.legado.app.utils.StartActivityContract
-import io.legado.app.utils.ToolbarUtils.setAllIconsColor
-import io.legado.app.utils.applyNavigationBarMargin
-import io.legado.app.utils.dpToPx
-import io.legado.app.utils.gone
-import io.legado.app.utils.longToastOnUi
import io.legado.app.utils.openFileUri
import io.legado.app.utils.sendToClip
import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.startActivity
-import io.legado.app.utils.toastOnUi
-import io.legado.app.utils.viewbindingdelegate.viewBinding
-import io.legado.app.utils.visible
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Dispatchers.IO
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
+import kotlinx.coroutines.flow.collectLatest
import org.koin.androidx.viewmodel.ext.android.viewModel
-class BookInfoActivity :
- VMBaseActivity(),
- GroupSelectDialog.CallBack,
- ChangeBookSourceDialog.CallBack,
- ChangeCoverDialog.CallBack,
- VariableDialog.Callback {
+class BookInfoActivity : BaseComposeActivity(), VariableDialog.Callback {
- companion object {
- private const val READ_BOOK_REQUEST_CODE = 1001
- }
+ private val viewModel: BookInfoViewModel by viewModel()
private val tocActivityResult = registerForActivityResult(TocActivityResult()) {
- it?.let {
- viewModel.getBook(false)?.let { book ->
- lifecycleScope.launch {
- withContext(IO) {
- book.durChapterIndex = it.first
- book.durChapterPos = it.second
- chapterChanged = it.third
- appDb.bookDao.update(book)
- }
- startReadActivity(book, binding.btnRead)
- }
- }
- } ?: let {
- if (!viewModel.inBookshelf) {
- viewModel.delBook()
- }
- }
+ viewModel.onTocResult(it)
}
private val localBookTreeSelect = registerForActivityResult(HandleFileContract()) {
it.uri?.let { treeUri ->
@@ -136,1036 +48,145 @@ class BookInfoActivity :
StartActivityContract(BookInfoEditActivity::class.java)
) {
if (it.resultCode == RESULT_OK) {
- viewModel.upEditBook()
+ viewModel.onInfoEdited()
}
}
private val editSourceResult = registerForActivityResult(
StartActivityContract(BookSourceEditActivity::class.java)
) {
- if (it.resultCode == RESULT_CANCELED) {
- return@registerForActivityResult
- }
- book?.let { book ->
- viewModel.bookSource = appDb.bookSourceDao.getBookSource(book.origin)
- viewModel.refreshBook(book)
+ if (it.resultCode != RESULT_CANCELED) {
+ viewModel.onSourceEdited()
}
}
- private var surfaceFinalColor: Int = 0
- private var surfaceContainerFinalColor: Int = 0
- private var secondaryFinalColor: Int = 0
- private var onSurfaceFinalColor: Int = 0
- private var secondaryContainerFinalColor: Int = 0
- private var primaryFinalColor: Int = 0
- private var tertiaryFinalColor: Int = 0
- private var currentJob: Job? = null
- private var wrappedContext: Context? = null
- private var chapterChanged = false
- private val waitDialog by lazy { WaitDialog(this) }
- private var editMenuItem: MenuItem? = null
- private val book get() = viewModel.getBook(false)
+ private val readBookResult = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) {
+ viewModel.onReaderResult(it.resultCode)
+ }
- override val binding by viewBinding(ActivityBookInfoBinding::inflate)
- override val viewModel: BookInfoViewModel by viewModel()
-
- @SuppressLint("PrivateResource")
override fun onCreate(savedInstanceState: Bundle?) {
- setEnterSharedElementCallback(MaterialContainerTransformSharedElementCallback())
- setExitSharedElementCallback(MaterialContainerTransformSharedElementCallback())
- val transform = MaterialContainerTransform().apply {
- addTarget(binding.cdCov)
- scrimColor = Color.TRANSPARENT
- }
- window.sharedElementEnterTransition = transform
- window.sharedElementReturnTransition = transform
super.onCreate(savedInstanceState)
- setupBackCallback()
- surfaceFinalColor =
- MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurface, -1)
- secondaryFinalColor =
- MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondary, -1)
- onSurfaceFinalColor =
- MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurface, -1)
- surfaceContainerFinalColor = MaterialColors.getColor(
- this,
- com.google.android.material.R.attr.colorSurfaceContainer,
- -1
- )
- secondaryContainerFinalColor = MaterialColors.getColor(
- this,
- com.google.android.material.R.attr.colorSecondaryContainer,
- -1
- )
- primaryFinalColor = MaterialColors.getColor(
- this,
- androidx.appcompat.R.attr.colorPrimary,
- -1
- )
- tertiaryFinalColor = MaterialColors.getColor(
- this,
- com.google.android.material.R.attr.colorTertiary,
- -1
- )
- binding.cdCov.transitionName = intent.getStringExtra("transitionName")
- binding.cdCov.doOnPreDraw {
- startPostponedEnterTransition()
- }
- setSupportActionBar(binding.topBar)
-
- binding.scrollView.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
- if (scrollY > oldScrollY) binding.btnRead.shrink()
- else if (scrollY < oldScrollY) binding.btnRead.extend()
- }
-
- binding.btnRead.applyNavigationBarMargin(true)
- binding.btnShelf.text = getString(R.string.remove_from_bookshelf)
- binding.tvToc.text = getString(R.string.toc_s, getString(R.string.loading))
-
- binding.tvDetail.revealOnFocusHint = false
-
- viewModel.bookData.observe(this) { showBook(it) }
- viewModel.chapterListData.observe(this) { upLoading(false, it) }
- viewModel.waitDialogData.observe(this) { upWaitDialogStatus(it) }
viewModel.initData(intent)
- initViewEvent()
}
- override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.book_info, menu)
- editMenuItem = menu.findItem(R.id.menu_edit)
- return super.onCompatCreateOptionsMenu(menu)
- }
+ @Composable
+ override fun Content() {
+ val state = viewModel.uiState.collectAsStateWithLifecycle().value
- override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
- menu.findItem(R.id.menu_can_update)?.isChecked =
- viewModel.bookData.value?.canUpdate ?: true
- menu.findItem(R.id.menu_split_long_chapter)?.isChecked =
- viewModel.bookData.value?.getSplitLongChapter() ?: true
- menu.findItem(R.id.menu_login)?.isVisible =
- !viewModel.bookSource?.loginUrl.isNullOrBlank()
- menu.findItem(R.id.menu_set_source_variable)?.isVisible =
- viewModel.bookSource != null
- menu.findItem(R.id.menu_set_book_variable)?.isVisible =
- viewModel.bookSource != null
- menu.findItem(R.id.menu_can_update)?.isVisible =
- viewModel.bookSource != null
- menu.findItem(R.id.menu_split_long_chapter)?.isVisible =
- viewModel.bookData.value?.isLocalTxt ?: false
- menu.findItem(R.id.menu_upload)?.isVisible =
- viewModel.bookData.value?.isLocal ?: false
- menu.findItem(R.id.menu_delete_alert)?.isChecked =
- LocalConfig.bookInfoDeleteAlert
- return super.onMenuOpened(featureId, menu)
- }
-
- override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- R.id.menu_edit -> {
- viewModel.getBook()?.let {
- infoEditResult.launch {
- putExtra("bookUrl", it.bookUrl)
+ LaunchedEffect(Unit) {
+ viewModel.effects.collectLatest { effect ->
+ when (effect) {
+ is BookInfoEffect.Finish -> {
+ effect.resultCode?.let { setResult(it) }
+ if (effect.afterTransition) finishAfterTransition() else finish()
}
- }
- }
- R.id.menu_share_it -> {
- viewModel.getBook()?.let {
- SourceCallBack.callBackBtn(
- this,
- SourceCallBack.CLICK_SHARE_BOOK,
- viewModel.bookSource,
- it,
- null
- ) {
- val bookJson = GSON.toJson(it)
- val shareStr = "${it.bookUrl}#$bookJson"
- val intent = Intent(Intent.ACTION_SEND)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- intent.putExtra(Intent.EXTRA_TEXT, shareStr)
- intent.type = "text/plain"
- startActivity(Intent.createChooser(intent, it.name))
- }
- }
- }
-
- R.id.menu_refresh -> {
- refreshBook()
- }
-
- R.id.menu_sync_remote -> {
- viewModel.syncFromRemote()
- }
-
- R.id.menu_login -> viewModel.bookSource?.let {
- startActivity {
- putExtra("type", "bookSource")
- putExtra("key", it.bookSourceUrl)
- }
- }
-
- R.id.menu_top -> viewModel.topBook()
- R.id.menu_set_source_variable -> setSourceVariable()
- R.id.menu_set_book_variable -> setBookVariable()
- R.id.menu_copy_book_url -> viewModel.getBook()?.let {
- SourceCallBack.callBackBtn(
- this,
- SourceCallBack.CLICK_COPY_BOOK_URL,
- viewModel.bookSource,
- it,
- null
- ) {
- sendToClip(it.bookUrl)
- }
- }
-
- R.id.menu_copy_toc_url -> viewModel.getBook()?.let {
- SourceCallBack.callBackBtn(
- this,
- SourceCallBack.CLICK_COPY_TOC_URL,
- viewModel.bookSource,
- it,
- null
- ) {
- sendToClip(it.tocUrl)
- }
- }
-
- R.id.menu_can_update -> {
- viewModel.getBook()?.let {
- it.canUpdate = !it.canUpdate
- if (viewModel.inBookshelf) {
- if (!it.canUpdate) {
- it.removeType(BookType.updateError)
+ is BookInfoEffect.OpenBookInfoEdit -> {
+ infoEditResult.launch {
+ putExtra("bookUrl", effect.bookUrl)
}
- viewModel.saveBook(it)
}
- }
- }
- R.id.menu_clear_cache -> viewModel.getBook()?.let {
- SourceCallBack.callBackBtn(
- this,
- SourceCallBack.CLICK_CLEAR_CACHE,
- viewModel.bookSource,
- it,
- null
- ) {
- viewModel.clearCache(it)
- }
- }
-
- R.id.menu_log -> showDialogFragment()
- R.id.menu_split_long_chapter -> {
- upLoading(true)
- viewModel.getBook()?.let {
- it.setSplitLongChapter(!item.isChecked)
- viewModel.loadBookInfo(it, false)
- }
- item.isChecked = !item.isChecked
- if (!item.isChecked) longToastOnUi(R.string.need_more_time_load_content)
- }
-
- R.id.menu_delete_alert -> LocalConfig.bookInfoDeleteAlert = !item.isChecked
- R.id.menu_upload -> {
- viewModel.getBook()?.let { book ->
- viewModel.uploadBook(book) {
- toastOnUi("上传成功")
- }
- }
- }
- }
- return super.onCompatOptionsItemSelected(item)
- }
-
- override fun observeLiveBus() {
- viewModel.actionLive.observe(this) {
- when (it) {
- "selectBooksDir" -> localBookTreeSelect.launch {
- title = getString(R.string.select_book_folder)
- }
- }
- }
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
- if (requestCode == READ_BOOK_REQUEST_CODE) {
- when (resultCode) {
- RESULT_OK -> {
- viewModel.inBookshelf = true
- upTvBookshelf()
- }
-
- RESULT_DELETED -> {
- setResult(RESULT_OK)
- finish()
- }
- }
- }
- }
-
- private fun setupBackCallback() {
- onBackPressedDispatcher.addCallback(this) {
- if (!viewModel.inBookshelf && AppConfig.showAddToShelfAlert) {
- viewModel.getBook()?.let { book ->
- alert(title = getString(R.string.add_to_bookshelf)) {
- setMessage(getString(R.string.check_add_bookshelf, book.name))
- okButton {
- book.removeType(BookType.notShelf)
- book.save()
- viewModel.inBookshelf = true
- super.finishAfterTransition()
+ is BookInfoEffect.OpenReader -> startReadActivity(
+ book = effect.book,
+ inBookshelf = effect.inBookshelf,
+ chapterChanged = effect.chapterChanged,
+ )
+ is BookInfoEffect.OpenToc -> tocActivityResult.launch(effect.bookUrl)
+ is BookInfoEffect.OpenBookSourceEdit -> {
+ editSourceResult.launch {
+ putExtra("sourceUrl", effect.sourceUrl)
}
- noButton { super.finishAfterTransition() }
}
- }
- } else {
- finishAfterTransition()
- }
- }
- }
- private fun refreshBook() {
- upLoading(true)
- viewModel.getBook()?.let {
- viewModel.refreshBook(it)
- }
- }
+ is BookInfoEffect.OpenSourceLogin -> {
+ startActivity {
+ putExtra("type", "bookSource")
+ putExtra("key", effect.sourceUrl)
+ }
+ }
- private fun showBook(book: Book) = binding.run {
- showCover(book)
- addColorScheme(binding.ivCover.drawable)
- tvName.text = book.name
- tvRemark.text = book.remark
- if (book.remark.isNullOrEmpty())
- cdRemark.gone()
- else
- cdRemark.visible()
- tvAuthor.text = getString(R.string.author_show, book.getRealAuthor())
- tvOrigin.text = getString(R.string.origin_show, book.originName)
- tvLasted.text = getString(R.string.lasted_show, book.latestChapterTitle)
- tvChapter.text = getString(R.string.read_chapter_total, book.totalChapterNum)
- if (book.durChapterIndex + 1 == book.totalChapterNum)
- tvChapterIndex.text = "已读完"
- else
- tvChapterIndex.text = getString(R.string.read_chapter_index, book.durChapterIndex + 1)
- tvDetail.text = book.getDisplayIntro()
- tvToc.visible(!book.isWebFile)
- upTvBookshelf()
- upKinds(book)
- upGroup(book.group)
- }
+ BookInfoEffect.OpenSelectBooksDir -> localBookTreeSelect.launch {
+ title = getString(R.string.select_book_folder)
+ }
- private fun upKinds(book: Book) = binding.run {
- lifecycleScope.launch {
- var kinds = book.getKindList()
- if (book.isLocal) {
- withContext(IO) {
- val size = FileDoc.fromFile(book.bookUrl).size
- if (size > 0) {
- kinds = kinds.toMutableList()
- kinds.add(ConvertUtils.formatFileSize(size))
+ is BookInfoEffect.OpenFile -> openFileUri(effect.uri, effect.mimeType)
+ is BookInfoEffect.RunSourceCallback -> runSourceCallback(effect)
+ is BookInfoEffect.ShowVariableDialog -> {
+ showDialogFragment(
+ VariableDialog(
+ effect.title,
+ effect.key,
+ effect.variable,
+ effect.comment,
+ )
+ )
}
}
}
- if (kinds.isEmpty()) {
- lbKind.gone()
- } else {
- lbKind.visible()
- lbKind.setLabels(kinds) {}
- }
}
- }
- private fun showCover(book: Book) {
- binding.ivCover.load(
- book.getDisplayCover(),
- book.name,
- book.author,
- false,
- book.origin,
- onLoadFinish = {
- binding.ivCover.post {
- val drawable = binding.ivCover.drawable
- if (drawable != null) {
- addColorScheme(drawable)
- }
- }
- }
+ BookInfoScreen(
+ state = state,
+ onIntent = viewModel::onIntent,
+ onBack = { finishAfterTransition() },
)
- if (!AppConfig.isEInkMode) {
- //高版本使用RenderEffect
- BookCover.load(this, book.getDisplayCover(), false, book.origin)
- .into(binding.bgBook)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- BookCover.load(this, book.getDisplayCover(), false, book.origin)
- .into(binding.bgBook)
- val blurEffect = RenderEffect.createBlurEffect(60f, 60f, Shader.TileMode.CLAMP)
- binding.bgBook.setRenderEffect(blurEffect)
- }else{
- BookCover.loadBlur(this, book.getDisplayCover(), false, book.origin)
- .into(binding.bgBook)
- }
- }
- addColorScheme(binding.ivCover.drawable)
-
- }
-
- private fun addColorScheme(drawable: Drawable?) {
- currentJob?.cancel()
- currentJob = CoroutineScope(Dispatchers.Default).launch {
- val bitmap = when (drawable) {
- is BitmapDrawable -> drawable.bitmap
- is TransitionDrawable -> (drawable.getDrawable(1) as? BitmapDrawable)?.bitmap
- else -> null
- } ?: return@launch
-
- val colorAccuracy = true
- val targetWidth = if (colorAccuracy) (bitmap.width / 4).coerceAtMost(256) else 16
- val targetHeight = if (colorAccuracy) (bitmap.height / 4).coerceAtMost(256) else 16
- val scaledBitmap = bitmap.scale(targetWidth, targetHeight, false)
-
- val options = DynamicColorsOptions.Builder()
- .setContentBasedSource(scaledBitmap)
- .build()
-
- wrappedContext = DynamicColors.wrapContextIfAvailable(
- this@BookInfoActivity,
- options
- ).apply {
- resources.configuration.uiMode =
- this@BookInfoActivity.resources.configuration.uiMode
- }
-
- withContext(Dispatchers.Main) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- applyColorScheme()
- }
- }
- }
- }
-
- private suspend fun applyColorScheme() {
- val ctx = wrappedContext ?: this
-
- val colorPrimary =
- MaterialColors.getColor(ctx, androidx.appcompat.R.attr.colorPrimary, -1)
- val colorSecondary =
- MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorSecondary, -1)
- val colorTertiary =
- MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorTertiary, -1)
- val colorOnSurface =
- MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorOnSurface, -1)
- val colorSurface =
- MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorSurface, -1)
- val colorSurfaceContainer =
- MaterialColors.getColor(
- ctx,
- com.google.android.material.R.attr.colorSurfaceContainerHighest,
- -1
- )
- val colorSecondaryContainer =
- MaterialColors.getColor(
- ctx,
- com.google.android.material.R.attr.colorSecondaryContainer,
- -1
- )
- val colorOnTertiary =
- MaterialColors.getColor(ctx, com.google.android.material.R.attr.colorOnTertiary, -1)
-
- val surfaceTransition = ValueAnimator.ofArgb(surfaceFinalColor, colorSurface).apply {
- duration = 400L
- addUpdateListener { animation ->
- val nightModeFlags =
- resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
- val isDarkMode = nightModeFlags == Configuration.UI_MODE_NIGHT_YES
- var color = animation.animatedValue as Int
- if (AppConfig.pureBlack && isDarkMode) {
- color = Color.BLACK
- }
- binding.cdInfo?.setCardBackgroundColor(color)
- if (!AppConfig.isTransparent)
- {
- binding.llInfo.setBackgroundColor(color)
- (binding.llCover.background as? GradientDrawable)?.colors =
- intArrayOf(Color.TRANSPARENT, color)
- }
- }
- }
-
- val surfaceContainerTransition =
- ValueAnimator.ofArgb(surfaceContainerFinalColor, colorSurfaceContainer).apply {
- duration = 400L
- addUpdateListener { animation ->
- val color = animation.animatedValue as Int
- binding.lbKind.applyColorScheme(color, colorOnSurface)
- binding.collTopBar.setContentScrimColor(color)
- binding.cdRemark.strokeColor = color
- }
- }
-
- val buttonTransition = ValueAnimator.ofArgb(secondaryFinalColor, colorSecondary).apply {
- duration = 400L
- addUpdateListener { animation ->
- val color = animation.animatedValue as Int
- val tint = ColorStateList.valueOf(color)
- listOf(
- binding.btnShelf,
- binding.tvTocView,
- binding.tvChangeGroup,
- binding.btnChangeSource
- ).forEach { btn ->
- btn.setTextColor(color)
- btn.iconTint = tint
- }
- binding.div.setTextColor(color)
- binding.tvChapterIndex.setTextColor(color)
- binding.tvRemark.setTextColor(color)
- }
- }
-
- val backTransition =
- ValueAnimator.ofArgb(secondaryContainerFinalColor, colorSecondaryContainer).apply {
- duration = 400L
- addUpdateListener { animation ->
- val color = animation.animatedValue as Int
- if (!AppConfig.isTransparent)
- binding.bgBookMask.setBackgroundColor(color)
- }
- }
-
- val textTransition = ValueAnimator.ofArgb(onSurfaceFinalColor, colorOnSurface).apply {
- duration = 400L
- addUpdateListener { animation ->
- val color = animation.animatedValue as Int
- binding.tvName.setTextColor(color)
- binding.tvAuthor.setTextColor(color)
- binding.tvOrigin.setTextColor(color)
- binding.tvDetail.setTextColor(color)
- binding.tvToc.setTextColor(color)
- binding.tvLasted.setTextColor(color)
- binding.ivName.imageTintList = ColorStateList.valueOf(color)
- binding.ivWeb.imageTintList = ColorStateList.valueOf(color)
- }
- }
-
- val primaryTransition = ValueAnimator.ofArgb(primaryFinalColor, colorPrimary).apply {
- duration = 400L
- addUpdateListener { animation ->
- val color = animation.animatedValue as Int
- binding.tvChapter.setTextColor(color)
- }
- }
-
- val tertiaryTransition = ValueAnimator.ofArgb(tertiaryFinalColor, colorTertiary).apply {
- duration = 400L
- addUpdateListener { animation ->
- val color = animation.animatedValue as Int
- binding.btnRead.backgroundTintList = ColorStateList.valueOf(color)
- }
- }
-
- withContext(Dispatchers.Main) {
- listOf(
- surfaceTransition,
- surfaceContainerTransition,
- buttonTransition,
- backTransition,
- textTransition,
- primaryTransition,
- tertiaryTransition
- ).forEach { it.start() }
- }
-
- surfaceFinalColor = colorSurface
- secondaryFinalColor = colorSecondary
- onSurfaceFinalColor = colorOnSurface
- surfaceContainerFinalColor = colorSurfaceContainer
- primaryFinalColor = colorPrimary
- tertiaryFinalColor = colorTertiary
- secondaryContainerFinalColor = colorSecondaryContainer
-
- binding.topBar.setAllIconsColor(colorOnSurface)
- binding.btnRead.setTextColor(colorOnTertiary)
- binding.btnRead.iconTint = ColorStateList.valueOf(colorOnTertiary)
- }
-
- private fun upLoading(isLoading: Boolean, chapterList: List? = null) {
- when {
- isLoading -> {
- binding.tvToc.text = getString(R.string.toc_s, getString(R.string.loading))
- binding.tvTocView.text = getString(R.string.loading)
- binding.tvTocView.isEnabled = false
- }
-
- chapterList.isNullOrEmpty() -> {
- binding.tvToc.text = getString(
- R.string.toc_s,
- getString(R.string.error_load_toc)
- )
- binding.tvTocView.text = getString(R.string.error_load_toc)
- binding.tvTocView.isEnabled = false
- }
-
- else -> {
- book?.let {
- binding.tvToc.text = getString(R.string.toc_s, it.durChapterTitle)
- binding.tvLasted.text = getString(R.string.lasted_show, it.latestChapterTitle)
- binding.tvTocView.text = getString(R.string.view_toc)
- binding.tvTocView.isEnabled = true
- }
- }
- }
- }
-
- private fun upTvBookshelf() {
- binding.btnShelf.apply {
- if (viewModel.inBookshelf) {
- text = getString(R.string.remove_from_bookshelf)
- icon = ContextCompat.getDrawable(context, R.drawable.ic_star_fill)
- } else {
- text = getString(R.string.add_to_bookshelf)
- icon = ContextCompat.getDrawable(context, R.drawable.ic_star)
- }
- }
- editMenuItem?.isVisible = viewModel.inBookshelf
- }
-
- private fun upGroup(groupId: Long) {
- viewModel.loadGroup(groupId) {
- if (it.isNullOrEmpty()) {
- binding.tvChangeGroup.text = if (book?.isLocal == true) {
- getString(R.string.local_no_group)
- } else {
- getString(R.string.no_group)
- }
- binding.tvChangeGroup.setIconResource(R.drawable.ic_groups)
- } else {
- binding.tvChangeGroup.text = getString(R.string.group_s, it)
- binding.tvChangeGroup.setIconResource(R.drawable.ic_groups_fill)
- }
- }
- }
-
- private fun initViewEvent() = binding.run {
- ivCover.setOnClickListener {
- viewModel.getBook()?.let {
- showDialogFragment(
- ChangeCoverDialog(it.name, it.author)
- )
- }
- }
- ivCover.setOnLongClickListener {
- viewModel.getBook()?.getDisplayCover()?.let { path ->
- showDialogFragment(PhotoDialog(path))
- }
- true
- }
- btnRead.setOnClickListener {
- viewModel.getBook()?.let { book ->
- if (book.isWebFile) {
- showWebFileDownloadAlert {
- readBook(it)
- }
- } else {
- readBook(book)
- }
- }
- }
- btnShelf.setOnClickListener {
- viewModel.getBook()?.let { book ->
- if (viewModel.inBookshelf) {
- deleteBook()
- } else {
- if (book.isWebFile) {
- showWebFileDownloadAlert()
- } else {
- viewModel.addToBookshelf {
- upTvBookshelf()
- }
- }
- }
- }
- }
- tvOrigin.setOnClickListener {
- viewModel.getBook()?.let { book ->
- if (book.isLocal) return@let
- if (!appDb.bookSourceDao.has(book.origin)) {
- toastOnUi(R.string.error_no_source)
- return@let
- }
- editSourceResult.launch {
- putExtra("sourceUrl", book.origin)
- }
- }
- }
- btnChangeSource.setOnClickListener {
- viewModel.getBook()?.let { book ->
- showDialogFragment(ChangeBookSourceDialog(book.name, book.author))
- }
- }
- tvTocView.setOnClickListener {
- if (viewModel.chapterListData.value.isNullOrEmpty()) {
- toastOnUi(R.string.chapter_list_empty)
- return@setOnClickListener
- }
- viewModel.getBook()?.let { book ->
- if (!viewModel.inBookshelf) {
- viewModel.saveBook(book) {
- viewModel.saveChapterList {
- openChapterList()
- }
- }
- } else {
- openChapterList()
- }
- }
- }
- tvChangeGroup.setOnClickListener {
- viewModel.getBook()?.let {
- showDialogFragment(
- GroupSelectDialog(it.group)
- )
- }
- }
- tvAuthor.setOnClickListener {
- viewModel.getBook(false)?.let { book ->
- SourceCallBack.callBackBtn(
- this@BookInfoActivity,
- SourceCallBack.CLICK_AUTHOR,
- viewModel.bookSource,
- book,
- null
- ) {
- startActivity {
- putExtra("key", book.author)
- }
- }
- }
- }
- tvAuthor.setOnLongClickListener {
- viewModel.getBook(false)?.let { book ->
- SourceCallBack.callBackBtn(
- this@BookInfoActivity,
- SourceCallBack.LONG_CLICK_AUTHOR,
- viewModel.bookSource,
- book,
- null
- ) {
- startActivity {
- putExtra("key", book.author)
- }
- }
- }
- true
- }
- tvName.setOnClickListener {
- viewModel.getBook(false)?.let { book ->
- SourceCallBack.callBackBtn(
- this@BookInfoActivity,
- SourceCallBack.CLICK_BOOK_NAME,
- viewModel.bookSource,
- book,
- null
- ) {
- startActivity {
- putExtra("key", book.name)
- }
- }
- }
- }
- tvName.setOnLongClickListener {
- TransitionManager.beginDelayedTransition(scrollView)
- tvName.maxLines = if (tvName.maxLines == 3) 10 else 3
- true
- }
- cdRemark.setOnClickListener {
- alert(R.string.edit_remark) {
- val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
- editLayout.hint = "编辑备注"
- editView.setText(book?.remark)
- }
- customView { alertBinding.root }
- okButton {
- viewModel.saveRemark(alertBinding.editView.text.toString())
- }
- cancelButton()
- }
- }
- refreshLayout.setOnRefreshListener {
- refreshLayout.isRefreshing = false
- refreshBook()
- }
- }
-
- private fun setSourceVariable() {
- lifecycleScope.launch {
- val source = viewModel.bookSource
- if (source == null) {
- toastOnUi("书源不存在")
- return@launch
- }
- val comment =
- source.getDisplayVariableComment("源变量可在js中通过source.getVariable()获取")
- val variable = withContext(IO) { source.getVariable() }
- showDialogFragment(
- VariableDialog(
- getString(R.string.set_source_variable),
- source.getKey(),
- variable,
- comment
- )
- )
- }
- }
-
- private fun setBookVariable() {
- lifecycleScope.launch {
- val source = viewModel.bookSource
- if (source == null) {
- toastOnUi("书源不存在")
- return@launch
- }
- val book = viewModel.getBook() ?: return@launch
- val variable = withContext(IO) { book.getCustomVariable() }
- val comment = source.getDisplayVariableComment(
- """书籍变量可在js中通过book.getVariable("custom")获取"""
- )
- showDialogFragment(
- VariableDialog(
- getString(R.string.set_book_variable),
- book.bookUrl,
- variable,
- comment
- )
- )
- }
}
override fun setVariable(key: String, variable: String?) {
- when (key) {
- viewModel.bookSource?.getKey() -> viewModel.bookSource?.setVariable(variable)
- viewModel.bookData.value?.bookUrl -> viewModel.bookData.value?.let {
- it.putCustomVariable(variable)
- if (viewModel.inBookshelf) {
- viewModel.saveBook(it)
+ viewModel.setVariable(key, variable)
+ }
+
+ private fun runSourceCallback(effect: BookInfoEffect.RunSourceCallback) {
+ SourceCallBack.callBackBtn(
+ this,
+ effect.event,
+ effect.source,
+ effect.book,
+ null,
+ ) {
+ when (val action = effect.action) {
+ is BookInfoCallbackAction.Search -> {
+ startActivity {
+ putExtra("key", action.keyword)
+ }
+ }
+
+ is BookInfoCallbackAction.ShareText -> {
+ val intent = Intent(Intent.ACTION_SEND).apply {
+ putExtra(Intent.EXTRA_TEXT, action.text)
+ type = "text/plain"
+ }
+ startActivity(Intent.createChooser(intent, action.chooserTitle))
+ }
+
+ is BookInfoCallbackAction.CopyText -> {
+ sendToClip(action.text)
+ }
+
+ BookInfoCallbackAction.ClearCache -> {
+ viewModel.clearCache()
}
}
}
}
- @SuppressLint("InflateParams")
- private fun deleteBook() {
- viewModel.getBook()?.let { book ->
- if (LocalConfig.bookInfoDeleteAlert) {
- alert(
- titleResource = R.string.draw,
- messageResource = R.string.sure_del
- ) {
- var checkBox: CheckBox? = null
- if (book.isLocal) {
- checkBox = CheckBox(this@BookInfoActivity).apply {
- setText(R.string.delete_book_file)
- isChecked = LocalConfig.deleteBookOriginal
- }
- val view = LinearLayout(this@BookInfoActivity).apply {
- setPadding(16.dpToPx(), 0, 16.dpToPx(), 0)
- addView(checkBox)
- }
- customView { view }
- }
- yesButton {
- if (checkBox != null) {
- LocalConfig.deleteBookOriginal = checkBox.isChecked
- }
- SourceCallBack.callBackBook(
- SourceCallBack.DEL_BOOK_SHELF,
- viewModel.bookSource,
- book
- ) //确认后删除书架
- viewModel.delBook(LocalConfig.deleteBookOriginal) {
- setResult(RESULT_OK)
- finish()
- }
- }
- noButton()
- }
- } else {
- SourceCallBack.callBackBook(
- SourceCallBack.DEL_BOOK_SHELF,
- viewModel.bookSource,
- book
- ) //点按钮直接删除书架
- viewModel.delBook(LocalConfig.deleteBookOriginal) {
- setResult(RESULT_OK)
- finish()
- }
- }
- }
- }
-
- private fun openChapterList() {
- viewModel.getBook()?.let {
- tocActivityResult.launch(it.bookUrl)
- }
- }
-
- private fun showWebFileDownloadAlert(
- onClick: ((Book) -> Unit)? = null,
+ private fun startReadActivity(
+ book: io.legado.app.data.entities.Book,
+ inBookshelf: Boolean,
+ chapterChanged: Boolean,
) {
- val webFiles = viewModel.webFiles
- if (webFiles.isEmpty()) {
- toastOnUi("Unexpected webFileData")
- return
- }
- selector(
- R.string.download_and_import_file,
- webFiles
- ) { _, webFile, _ ->
- if (webFile.isSupported) {
- /* import */
- viewModel.importOrDownloadWebFile(webFile) {
- onClick?.invoke(it)
- }
- } else if (webFile.isSupportDecompress) {
- /* 解压筛选后再选择导入项 */
- viewModel.importOrDownloadWebFile(webFile) { uri ->
- viewModel.getArchiveFilesName(uri) { fileNames ->
- if (fileNames.size == 1) {
- viewModel.importArchiveBook(uri, fileNames[0]) {
- onClick?.invoke(it)
- }
- } else {
- showDecompressFileImportAlert(uri, fileNames, onClick)
- }
- }
- }
- } else {
- alert(
- title = getString(R.string.draw),
- message = getString(R.string.file_not_supported, webFile.name)
- ) {
- neutralButton(R.string.open_fun) {
- /* download only */
- viewModel.importOrDownloadWebFile(webFile) {
- openFileUri(it, "*/*")
- }
- }
- noButton()
- }
- }
- }
- }
-
- private fun showDecompressFileImportAlert(
- archiveFileUri: Uri,
- fileNames: List,
- success: ((Book) -> Unit)? = null,
- ) {
- if (fileNames.isEmpty()) {
- toastOnUi(R.string.unsupport_archivefile_entry)
- return
- }
- selector(
- R.string.import_select_book,
- fileNames
- ) { _, name, _ ->
- viewModel.importArchiveBook(archiveFileUri, name) {
- success?.invoke(it)
- }
- }
- }
-
- private fun readBook(book: Book) {
- if (!viewModel.inBookshelf) {
- book.addType(BookType.notShelf)
- viewModel.saveBook(book) {
- viewModel.saveChapterList {
- startReadActivity(book, binding.btnRead)
- }
- }
- } else {
- viewModel.saveBook(book) {
- startReadActivity(book, binding.btnRead)
- }
- }
- }
-
- private fun startReadActivity(book: Book, sharedView: View) {
- val transitionName = "book_${book.bookUrl}_${System.currentTimeMillis()}"
- sharedView.transitionName = transitionName
-
val cls = when {
book.isAudio -> AudioPlayActivity::class.java
!book.isLocal && book.isImage && AppConfig.showMangaUi -> ReadMangaActivity::class.java
else -> ReadBookActivity::class.java
}
-
- val intent = Intent(this, cls).apply {
- putExtra("bookUrl", book.bookUrl)
- putExtra("inBookshelf", viewModel.inBookshelf)
- putExtra("chapterChanged", chapterChanged)
- putExtra("transitionName", transitionName)
- }
-
- val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
- this,
- sharedView,
- transitionName
+ readBookResult.launch(
+ Intent(this, cls).apply {
+ putExtra("bookUrl", book.bookUrl)
+ putExtra("inBookshelf", inBookshelf)
+ putExtra("chapterChanged", chapterChanged)
+ }
)
- ActivityCompat.startActivityForResult(
- this,
- intent,
- READ_BOOK_REQUEST_CODE,
- options.toBundle()
- )
- }
-
- override val oldBook: Book?
- get() = viewModel.bookData.value
-
- override fun changeTo(source: BookSource, book: Book, toc: List) {
- viewModel.changeTo(source, book, toc)
- }
-
- override fun coverChangeTo(coverUrl: String) {
- viewModel.bookData.value?.let { book ->
- book.customCoverUrl = coverUrl
- showCover(book)
- if (viewModel.inBookshelf) {
- viewModel.saveBook(book)
- }
- }
- }
-
- override fun upGroup(requestCode: Int, groupId: Long) {
- upGroup(groupId)
- viewModel.getBook()?.let { book ->
- book.group = groupId
- if (viewModel.inBookshelf) {
- viewModel.saveBook(book)
- } else if (groupId > 0) {
- viewModel.addToBookshelf {
- upTvBookshelf()
- }
- }
- }
- }
-
- private fun upWaitDialogStatus(isShow: Boolean) {
- val showText = "Loading....."
- if (isShow) {
- waitDialog.run {
- setText(showText)
- show()
- }
- } else {
- waitDialog.dismiss()
- }
- }
-
- override fun addToBookshelf(book: Book, toc: List) {
- viewModel.addToBookshelf(book, toc) {
- toastOnUi("已添加到书架")
- }
}
}
diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoContract.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoContract.kt
new file mode 100644
index 000000000..eb839f05b
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoContract.kt
@@ -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 = emptyList(),
+ val webFiles: List = emptyList(),
+ val kindLabels: List = 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,
+ 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,
+ ) : BookInfoIntent
+ data class AddSourceAsNewBook(
+ val book: Book,
+ val toc: List,
+ ) : 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,
+}
diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt
new file mode 100644
index 000000000..76fd3e59d
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt
@@ -0,0 +1,1013 @@
+package io.legado.app.ui.book.info
+
+import android.net.Uri
+import androidx.activity.compose.BackHandler
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+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.fillMaxSize
+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.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.lazy.itemsIndexed
+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.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Book
+import androidx.compose.material.icons.filled.Bookmark
+import androidx.compose.material.icons.filled.BookmarkAdd
+import androidx.compose.material.icons.filled.Code
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.Share
+import androidx.compose.material.icons.filled.Star
+import androidx.compose.material.icons.outlined.Book
+import androidx.compose.material.icons.outlined.CollectionsBookmark
+import androidx.compose.material.icons.outlined.FolderZip
+import androidx.compose.material.icons.outlined.Image
+import androidx.compose.material.icons.outlined.Link
+import androidx.compose.material.icons.outlined.Settings
+import androidx.compose.material.icons.outlined.SwapHoriz
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExtendedFloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.pulltorefresh.PullToRefreshBox
+import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
+import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+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.draw.blur
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.lerp
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalDensity
+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 coil.ImageLoader
+import coil.compose.AsyncImage
+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.help.book.isLocal
+import io.legado.app.help.config.AppConfig
+import io.legado.app.ui.about.AppLogSheet
+import io.legado.app.ui.book.changecover.ChangeCoverViewModel
+import io.legado.app.ui.book.changesource.ChangeBookSourceViewModel
+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.config.coverConfig.CoverConfig
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.ProvideThemeOverride
+import io.legado.app.ui.theme.ThemeResolver
+import io.legado.app.ui.theme.ThemeOverrideState
+import io.legado.app.ui.theme.rememberImageSeedColor
+import io.legado.app.ui.theme.rememberThemeOverride
+import io.legado.app.ui.widget.components.AppScaffold
+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.TopBarActionButton
+import io.legado.app.ui.widget.components.button.TopBarButtonVariant
+import io.legado.app.ui.widget.components.button.TopBarNavigationButton
+import io.legado.app.ui.widget.components.card.GlassCard
+import io.legado.app.ui.widget.components.card.TextCard
+import io.legado.app.ui.widget.components.cover.BookCover
+import io.legado.app.ui.widget.components.icon.AppIcon
+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.AnimatedTextLine
+import io.legado.app.ui.widget.components.text.AppText
+import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
+import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
+import io.legado.app.ui.widget.components.topbar.MiuixGlassScrollBehavior
+import io.legado.app.utils.StartActivityContract
+import io.legado.app.utils.startActivity
+import io.legado.app.utils.toastOnUi
+import kotlinx.coroutines.delay
+import org.koin.compose.koinInject
+import org.koin.androidx.compose.koinViewModel
+import top.yukonga.miuix.kmp.basic.TopAppBar as MiuixTopAppBar
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun BookInfoScreen(
+ state: BookInfoUiState,
+ onIntent: (BookInfoIntent) -> Unit,
+ onBack: () -> Unit,
+) {
+ val bookColorTheme = rememberBookInfoColorTheme(state.book)
+
+ BookInfoColorTheme(theme = bookColorTheme) {
+ BookInfoScreenContent(
+ state = state,
+ onIntent = onIntent,
+ onBack = onBack,
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun BookInfoScreenContent(
+ state: BookInfoUiState,
+ onIntent: (BookInfoIntent) -> Unit,
+ onBack: () -> Unit,
+) {
+ val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
+ val miuixScrollBehavior = if (isMiuix) {
+ GlassTopAppBarDefaults.defaultScrollBehavior() as? MiuixGlassScrollBehavior
+ } else {
+ null
+ }
+ val miuixCollapsedFraction = miuixScrollBehavior?.collapsedFraction ?: 0f
+ val listState = rememberLazyListState()
+ val colorThresholdPx = with(LocalDensity.current) { 6.dp.roundToPx() }
+ val showAppBarColor by remember(listState, isMiuix, colorThresholdPx, miuixCollapsedFraction) {
+ derivedStateOf {
+ if (isMiuix) {
+ miuixCollapsedFraction > 0.01f
+ } else {
+ listState.firstVisibleItemIndex > 0 ||
+ listState.firstVisibleItemScrollOffset > colorThresholdPx
+ }
+ }
+ }
+ val appBarColor by animateColorAsState(
+ targetValue = if (showAppBarColor) {
+ LegadoTheme.colorScheme.surfaceContainer
+ } else {
+ LegadoTheme.colorScheme.surfaceContainer.copy(alpha = 0f)
+ },
+ label = "book-info-top-bar-color",
+ )
+ val pullState = rememberPullToRefreshState()
+ var showMenu by rememberSaveable { mutableStateOf(false) }
+
+ BackHandler { onIntent(BookInfoIntent.BackPressed) }
+
+ AppScaffold(
+ modifier = Modifier
+ .fillMaxSize()
+ .then(
+ if (miuixScrollBehavior != null) {
+ Modifier.nestedScroll(miuixScrollBehavior.nestedScrollConnection)
+ } else {
+ Modifier
+ }
+ ),
+ topBar = {
+ if (isMiuix) {
+ MiuixTopAppBar(
+ title = "",
+ color = appBarColor,
+ navigationIcon = {
+ TopBarNavigationButton(
+ onClick = { onIntent(BookInfoIntent.BackPressed) }
+ )
+ },
+ actions = {
+ BookInfoTopBarActions(
+ state = state,
+ showMenu = showMenu,
+ onShowMenuChange = { showMenu = it },
+ onMenuAction = { onIntent(BookInfoIntent.MenuAction(it)) },
+ )
+ },
+ scrollBehavior = miuixScrollBehavior?.miuixBehavior
+ )
+ } else {
+ TopAppBar(
+ title = {
+ Text(
+ text = "",
+ maxLines = 1,
+ )
+ },
+ navigationIcon = {
+ TopBarNavigationButton(
+ onClick = { onIntent(BookInfoIntent.BackPressed) }
+ )
+ },
+ actions = {
+ BookInfoTopBarActions(
+ state = state,
+ showMenu = showMenu,
+ onShowMenuChange = { showMenu = it },
+ onMenuAction = { onIntent(BookInfoIntent.MenuAction(it)) },
+ )
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = appBarColor,
+ scrolledContainerColor = appBarColor,
+ navigationIconContentColor = LegadoTheme.colorScheme.onSurface,
+ titleContentColor = LegadoTheme.colorScheme.onSurface,
+ actionIconContentColor = LegadoTheme.colorScheme.onSurface,
+ ),
+ )
+ }
+ },
+ floatingActionButton = {
+ ExtendedFloatingActionButton(
+ onClick = { onIntent(BookInfoIntent.ReadClick) },
+ containerColor = LegadoTheme.colorScheme.primaryContainer,
+ contentColor = LegadoTheme.colorScheme.onPrimaryContainer,
+ icon = { Icon(Icons.Default.Book, null) },
+ text = { Text(stringResource(R.string.reading)) },
+ )
+ },
+ ) { paddingValues ->
+ val book = state.book
+ if (book == null) {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator(
+ color = LegadoTheme.colorScheme.primary,
+ trackColor = LegadoTheme.colorScheme.surfaceContainerHighest,
+ )
+ }
+ } else {
+ Box(modifier = Modifier.fillMaxSize()) {
+ BookInfoBackdrop(book)
+ PullToRefreshBox(
+ modifier = Modifier.fillMaxSize(),
+ state = pullState,
+ isRefreshing = state.isTocLoading,
+ onRefresh = { onIntent(BookInfoIntent.MenuAction(BookInfoMenuAction.Refresh)) },
+ indicator = {
+ PullToRefreshDefaults.LoadingIndicator(
+ state = pullState,
+ isRefreshing = state.isTocLoading,
+ containerColor = LegadoTheme.colorScheme.surfaceContainerHigh,
+ color = LegadoTheme.colorScheme.primary,
+ modifier = Modifier
+ .align(Alignment.TopCenter)
+ .padding(top = paddingValues.calculateTopPadding())
+ )
+ }
+ ) {
+ LazyColumn(
+ state = listState,
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(
+ top = paddingValues.calculateTopPadding() + 8.dp,
+ bottom = paddingValues.calculateBottomPadding() + 88.dp,
+ ),
+ ) {
+ item {
+ BookInfoHeader(
+ book = book,
+ kindLabels = state.kindLabels,
+ groupNames = state.groupNames,
+ onCoverClick = { onIntent(BookInfoIntent.CoverClick) },
+ onCoverLongClick = { onIntent(BookInfoIntent.CoverLongClick) },
+ onAuthorClick = { onIntent(BookInfoIntent.AuthorClick(it)) },
+ onBookNameClick = { onIntent(BookInfoIntent.BookNameClick(it)) },
+ onOriginClick = { onIntent(BookInfoIntent.OriginClick) },
+ )
+ }
+ item {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ color = LegadoTheme.colorScheme.surface
+ )
+ .padding(bottom = 24.dp)
+ ) {
+ BookInfoActions(
+ inBookshelf = state.inBookshelf,
+ onShelfClick = { onIntent(BookInfoIntent.ShelfClick) },
+ onTocClick = { onIntent(BookInfoIntent.TocClick) },
+ onGroupClick = { onIntent(BookInfoIntent.GroupClick) },
+ onSourceClick = { onIntent(BookInfoIntent.ChangeSourceClick) },
+ )
+ BookInfoSummary(
+ book = book,
+ chapterList = state.chapterList,
+ onRemarkClick = { onIntent(BookInfoIntent.RemarkClick) },
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ val currentSheet = state.sheet
+ var renderedSheet by remember { mutableStateOf(BookInfoSheet.None) }
+
+ LaunchedEffect(currentSheet) {
+ if (currentSheet == BookInfoSheet.None) {
+ delay(300)
+ renderedSheet = BookInfoSheet.None
+ } else {
+ renderedSheet = currentSheet
+ }
+ }
+
+ when (val sheet = renderedSheet) {
+ BookInfoSheet.None -> Unit
+ BookInfoSheet.CoverPicker -> ChangeCoverSheet(
+ show = currentSheet == BookInfoSheet.CoverPicker,
+ name = state.book?.name.orEmpty(),
+ author = state.book?.author.orEmpty(),
+ onDismissRequest = { onIntent(BookInfoIntent.DismissSheet) },
+ onSelect = { onIntent(BookInfoIntent.SelectCover(it)) },
+ )
+ BookInfoSheet.GroupPicker -> GroupSelectSheet(
+ show = currentSheet == BookInfoSheet.GroupPicker,
+ currentGroupId = state.book?.group ?: 0L,
+ onDismissRequest = { onIntent(BookInfoIntent.DismissSheet) },
+ onConfirm = { onIntent(BookInfoIntent.SelectGroup(it)) },
+ )
+ BookInfoSheet.SourcePicker -> state.book?.let { book ->
+ ChangeSourceSheet(
+ show = currentSheet == BookInfoSheet.SourcePicker,
+ oldBook = book,
+ onDismissRequest = { onIntent(BookInfoIntent.DismissSheet) },
+ onReplace = { source, newBook, toc ->
+ onIntent(BookInfoIntent.ReplaceWithSource(source, newBook, toc))
+ },
+ onAddAsNew = { newBook, toc ->
+ onIntent(BookInfoIntent.AddSourceAsNewBook(newBook, toc))
+ },
+ )
+ }
+ is BookInfoSheet.WebFiles -> WebFileSheet(
+ show = currentSheet is BookInfoSheet.WebFiles,
+ files = state.webFiles,
+ title = stringResource(R.string.download_and_import_file),
+ onDismissRequest = { onIntent(BookInfoIntent.DismissSheet) },
+ onSelect = { onIntent(BookInfoIntent.SelectWebFile(it, sheet.openAfterImport)) },
+ )
+ is BookInfoSheet.ArchiveEntries -> WebFileSheet(
+ show = currentSheet is BookInfoSheet.ArchiveEntries,
+ files = sheet.entries.map { BookInfoWebFile(it, it) },
+ title = stringResource(R.string.import_select_book),
+ onDismissRequest = { onIntent(BookInfoIntent.DismissSheet) },
+ onSelect = {
+ onIntent(
+ BookInfoIntent.SelectArchiveEntry(
+ archiveUri = sheet.archiveUri,
+ entryName = it.name,
+ openAfterImport = sheet.openAfterImport,
+ )
+ )
+ },
+ )
+ }
+
+ BookInfoDialogs(state = state, onIntent = onIntent, onBack = onBack)
+}
+
+@Composable
+private fun BookInfoColorTheme(
+ theme: ThemeOverrideState?,
+ content: @Composable () -> Unit,
+) {
+ ProvideThemeOverride(theme = theme, content = content)
+}
+
+@Composable
+private fun rememberBookInfoColorTheme(book: Book?): ThemeOverrideState? {
+ val imageLoader = koinInject()
+ val coverPath = book?.getDisplayCover()
+ val sourceOrigin = book?.origin
+ val loadOnlyWifi = CoverConfig.loadCoverOnlyWifi
+
+ val seedColor = rememberImageSeedColor(
+ imageLoader = imageLoader,
+ data = coverPath,
+ requestKey = listOf(coverPath, sourceOrigin, loadOnlyWifi),
+ ) {
+ setParameter("sourceOrigin", sourceOrigin)
+ setParameter("loadOnlyWifi", loadOnlyWifi)
+ }
+
+ return rememberThemeOverride(seedColor)
+}
+
+@Composable
+private fun BookInfoTopBarActions(
+ state: BookInfoUiState,
+ showMenu: Boolean,
+ onShowMenuChange: (Boolean) -> Unit,
+ onMenuAction: (BookInfoMenuAction) -> Unit,
+) {
+ if (state.inBookshelf) {
+ TopBarActionButton(
+ onClick = { onMenuAction(BookInfoMenuAction.Edit) },
+ imageVector = Icons.Default.Edit,
+ )
+ }
+ TopBarActionButton(
+ onClick = { onMenuAction(BookInfoMenuAction.Share) },
+ imageVector = Icons.Default.Share,
+ )
+ TopBarActionButton(
+ onClick = { onShowMenuChange(true) },
+ imageVector = Icons.Default.MoreVert,
+ )
+ BookInfoOverflowMenu(
+ expanded = showMenu,
+ onDismissRequest = { onShowMenuChange(false) },
+ state = state,
+ onMenuAction = {
+ onShowMenuChange(false)
+ onMenuAction(it)
+ }
+ )
+}
+
+@Composable
+private fun BookInfoBackdrop(book: Book) {
+ val cover = book.getDisplayCover()
+ val seedOverlay = lerp(
+ LegadoTheme.colorScheme.secondaryContainer,
+ LegadoTheme.seedColor,
+ 0.42f
+ )
+ Box(modifier = Modifier.fillMaxSize()) {
+ if (!cover.isNullOrBlank()) {
+ AsyncImage(
+ model = cover,
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxSize()
+ .blur(32.dp),
+ contentScale = ContentScale.Crop,
+ )
+ }
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(seedOverlay.copy(alpha = 0.34f))
+ )
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(
+ Brush.verticalGradient(
+ colorStops = arrayOf(
+ 0f to Color.Transparent,
+ 0.18f to seedOverlay.copy(alpha = 0.10f),
+ 0.34f to seedOverlay.copy(alpha = 0.18f),
+ 0.52f to LegadoTheme.colorScheme.surface.copy(alpha = 0.82f),
+ 0.72f to LegadoTheme.colorScheme.surface,
+ 1f to LegadoTheme.colorScheme.surface,
+ )
+ )
+ )
+ )
+ }
+}
+
+@Composable
+private fun BookInfoOverflowMenu(
+ expanded: Boolean,
+ onDismissRequest: () -> Unit,
+ state: BookInfoUiState,
+ onMenuAction: (BookInfoMenuAction) -> Unit,
+) {
+ val book = state.book
+ RoundDropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) {
+ if (state.inBookshelf) {
+ RoundDropdownMenuItem(
+ text = stringResource(R.string.edit),
+ onClick = { onMenuAction(BookInfoMenuAction.Edit) }
+ )
+ }
+ RoundDropdownMenuItem(
+ text = stringResource(R.string.refresh),
+ onClick = { onMenuAction(BookInfoMenuAction.Refresh) }
+ )
+ if (book?.isLocal == true) {
+ RoundDropdownMenuItem(
+ text = stringResource(R.string.re_sync_webdav),
+ onClick = { onMenuAction(BookInfoMenuAction.SyncRemote) }
+ )
+ RoundDropdownMenuItem(
+ text = stringResource(R.string.upload_to_remote),
+ onClick = { onMenuAction(BookInfoMenuAction.Upload) }
+ )
+ }
+ if (!state.bookSource?.loginUrl.isNullOrBlank()) {
+ RoundDropdownMenuItem(
+ text = stringResource(R.string.login),
+ onClick = { onMenuAction(BookInfoMenuAction.Login) }
+ )
+ }
+ if (state.bookSource != null) {
+ RoundDropdownMenuItem(
+ text = stringResource(R.string.set_source_variable),
+ onClick = { onMenuAction(BookInfoMenuAction.SetSourceVariable) }
+ )
+ RoundDropdownMenuItem(
+ text = stringResource(R.string.set_book_variable),
+ onClick = { onMenuAction(BookInfoMenuAction.SetBookVariable) }
+ )
+ RoundDropdownMenuItem(
+ text = stringResource(R.string.copy_book_url),
+ onClick = { onMenuAction(BookInfoMenuAction.CopyBookUrl) }
+ )
+ RoundDropdownMenuItem(
+ text = stringResource(R.string.copy_toc_url),
+ onClick = { onMenuAction(BookInfoMenuAction.CopyTocUrl) }
+ )
+ }
+ RoundDropdownMenuItem(
+ text = stringResource(R.string.to_top),
+ onClick = { onMenuAction(BookInfoMenuAction.Top) }
+ )
+ RoundDropdownMenuItem(
+ text = stringResource(R.string.allow_update),
+ onClick = { onMenuAction(BookInfoMenuAction.ToggleCanUpdate) },
+ trailingIcon = { if (book?.canUpdate == true) Icon(Icons.Default.Star, null) }
+ )
+ if (book?.isLocal == true && book.type and io.legado.app.constant.BookType.text > 0) {
+ RoundDropdownMenuItem(
+ text = stringResource(R.string.split_long_chapter),
+ onClick = { onMenuAction(BookInfoMenuAction.ToggleSplitLongChapter) },
+ trailingIcon = { if (book.getSplitLongChapter()) Icon(Icons.Default.Star, null) }
+ )
+ }
+ RoundDropdownMenuItem(
+ text = stringResource(R.string.delete_alert),
+ onClick = { onMenuAction(BookInfoMenuAction.ToggleDeleteAlert) },
+ trailingIcon = { if (io.legado.app.help.config.LocalConfig.bookInfoDeleteAlert) Icon(Icons.Default.Star, null) }
+ )
+ RoundDropdownMenuItem(
+ text = stringResource(R.string.clear_cache),
+ onClick = { onMenuAction(BookInfoMenuAction.ClearCache) }
+ )
+ RoundDropdownMenuItem(
+ text = stringResource(R.string.log),
+ onClick = { onMenuAction(BookInfoMenuAction.ShowLog) }
+ )
+ }
+}
+
+@Composable
+private fun BookInfoHeader(
+ book: Book,
+ kindLabels: List,
+ groupNames: String?,
+ onCoverClick: () -> Unit,
+ onCoverLongClick: () -> Unit,
+ onAuthorClick: (Boolean) -> Unit,
+ onBookNameClick: (Boolean) -> Unit,
+ onOriginClick: () -> Unit,
+) {
+ val labelBackground = lerp(
+ LegadoTheme.colorScheme.surface,
+ LegadoTheme.seedColor,
+ 0.1f
+ ).copy(alpha = 0.82f)
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ Brush.verticalGradient(
+ colors = listOf(
+ Color.Transparent,
+ lerp(LegadoTheme.colorScheme.surface, LegadoTheme.seedColor, 0.08f)
+ .copy(alpha = 0.5f),
+ LegadoTheme.colorScheme.surface,
+ )
+ )
+ )
+ .padding(top = 16.dp, bottom = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.Top,
+ ) {
+ Box(
+ modifier = Modifier
+ .width(112.dp)
+ .combinedClickable(onClick = onCoverClick, onLongClick = onCoverLongClick)
+ ) {
+ BookCover(
+ name = book.name,
+ author = book.author,
+ path = book.getDisplayCover(),
+ sourceOrigin = book.origin,
+ modifier = Modifier.width(112.dp)
+ )
+ }
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .align(Alignment.CenterVertically)
+ .padding(top = 8.dp, bottom = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ AppText(
+ text = book.name,
+ style = LegadoTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold,
+ maxLines = 3,
+ modifier = Modifier.combinedClickable(
+ onClick = { onBookNameClick(false) },
+ onLongClick = { onBookNameClick(true) }
+ )
+ )
+ AppText(
+ text = stringResource(R.string.author_show, book.getRealAuthor()),
+ style = LegadoTheme.typography.bodyLarge,
+ color = LegadoTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.combinedClickable(
+ onClick = { onAuthorClick(false) },
+ onLongClick = { onAuthorClick(true) }
+ )
+ )
+ AppText(
+ text = stringResource(R.string.origin_show, book.originName),
+ style = LegadoTheme.typography.labelMedium,
+ color = LegadoTheme.colorScheme.primary,
+ modifier = Modifier.clickable(onClick = onOriginClick)
+ )
+ }
+ }
+ if (kindLabels.isNotEmpty() || !groupNames.isNullOrBlank()) {
+ LazyRow(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ itemsIndexed(
+ items = kindLabels,
+ key = { index, label -> "kind-$index-$label" }
+ ) { _, label ->
+ TextCard(
+ text = label,
+ textStyle = LegadoTheme.typography.labelLargeEmphasized,
+ backgroundColor = labelBackground,
+ contentColor = LegadoTheme.colorScheme.onSurface,
+ )
+ }
+ groupNames?.takeIf { it.isNotBlank() }?.let {
+ item(key = "group-$it") {
+ TextCard(
+ text = stringResource(R.string.group_s, it),
+ textStyle = LegadoTheme.typography.labelLargeEmphasized,
+ backgroundColor = labelBackground,
+ contentColor = LegadoTheme.colorScheme.onSurface,
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun BookInfoActions(
+ inBookshelf: Boolean,
+ onShelfClick: () -> Unit,
+ onTocClick: () -> Unit,
+ onGroupClick: () -> Unit,
+ onSourceClick: () -> Unit,
+) {
+ var awaitingShelfAddition by rememberSaveable { mutableStateOf(false) }
+ var showShelfRemoveHint by rememberSaveable { mutableStateOf(false) }
+
+ LaunchedEffect(inBookshelf, awaitingShelfAddition) {
+ if (awaitingShelfAddition && inBookshelf) {
+ awaitingShelfAddition = false
+ showShelfRemoveHint = true
+ delay(1000)
+ showShelfRemoveHint = false
+ } else if (!inBookshelf) {
+ showShelfRemoveHint = false
+ }
+ }
+
+ val shelfLabel = when {
+ !inBookshelf -> stringResource(R.string.add_to_bookshelf)
+ showShelfRemoveHint -> stringResource(R.string.click_to_remove)
+ else -> stringResource(R.string.remove_from_bookshelf)
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LegadoTheme.colorScheme.surface)
+ .padding(horizontal = 16.dp, vertical = 4.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ BookInfoActionCard(
+ modifier = Modifier.weight(1f),
+ icon = if (inBookshelf) Icons.Outlined.Book else Icons.Default.BookmarkAdd,
+ label = shelfLabel,
+ onClick = {
+ if (!inBookshelf) {
+ awaitingShelfAddition = true
+ } else {
+ awaitingShelfAddition = false
+ showShelfRemoveHint = false
+ }
+ onShelfClick()
+ },
+ )
+ BookInfoActionCard(
+ modifier = Modifier.weight(1f),
+ icon = Icons.AutoMirrored.Outlined.FormatListBulleted,
+ label = stringResource(R.string.view_toc),
+ onClick = onTocClick
+ )
+ BookInfoActionCard(
+ modifier = Modifier.weight(1f),
+ icon = Icons.Default.Bookmark,
+ label = stringResource(R.string.change_group),
+ onClick = onGroupClick
+ )
+ BookInfoActionCard(
+ modifier = Modifier.weight(1f),
+ icon = Icons.Default.Code,
+ label = stringResource(R.string.book_source),
+ onClick = onSourceClick
+ )
+ }
+}
+
+@Composable
+private fun BookInfoActionCard(
+ modifier: Modifier = Modifier,
+ icon: androidx.compose.ui.graphics.vector.ImageVector,
+ label: String,
+ onClick: () -> Unit
+) {
+ GlassCard(
+ modifier = modifier,
+ onClick = onClick,
+ containerColor = LegadoTheme.colorScheme.surfaceContainer.copy(alpha = 0.5f),
+ contentColor = LegadoTheme.colorScheme.onSurface,
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp, vertical = 12.dp),
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ AppIcon(icon, null)
+ AnimatedTextLine(
+ text = label,
+ style = LegadoTheme.typography.bodySmall,
+ maxLines = 2,
+ )
+ }
+ }
+}
+
+@Composable
+private fun BookInfoSummary(
+ book: Book,
+ chapterList: List,
+ onRemarkClick: () -> Unit,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(LegadoTheme.colorScheme.surface)
+ .padding(horizontal = 16.dp, vertical = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ AppText(
+ text = stringResource(R.string.toc_s, book.durChapterTitle ?: stringResource(R.string.loading)),
+ style = LegadoTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ )
+ AppText(
+ text = stringResource(R.string.lasted_show, book.latestChapterTitle ?: ""),
+ style = LegadoTheme.typography.bodyMedium,
+ color = LegadoTheme.colorScheme.onSurfaceVariant,
+ )
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ AppText(
+ text = stringResource(R.string.read_chapter_total, book.totalChapterNum),
+ style = LegadoTheme.typography.bodyMedium,
+ color = LegadoTheme.colorScheme.primary,
+ fontWeight = FontWeight.Bold,
+ )
+ AppText(text = "|", color = LegadoTheme.colorScheme.secondary)
+ AppText(
+ text = if (book.durChapterIndex + 1 == book.totalChapterNum && book.totalChapterNum > 0) "已读完" else stringResource(R.string.read_chapter_index, book.durChapterIndex + 1),
+ style = LegadoTheme.typography.labelMedium,
+ color = LegadoTheme.colorScheme.secondary,
+ )
+ }
+ if (chapterList.isEmpty()) {
+ AppText(
+ text = stringResource(R.string.error_load_toc),
+ style = LegadoTheme.typography.bodySmall,
+ color = LegadoTheme.colorScheme.error
+ )
+ }
+ Spacer(modifier = Modifier.height(4.dp))
+ book.remark?.takeIf { it.isNotBlank() }?.let { remark ->
+ ElevatedCard(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = onRemarkClick,
+ colors = CardDefaults.elevatedCardColors(
+ containerColor = LegadoTheme.colorScheme.surfaceContainerHigh,
+ contentColor = LegadoTheme.colorScheme.onSurface,
+ )
+ ) {
+ Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ AppText(text = stringResource(R.string.book_remark), style = LegadoTheme.typography.titleSmall)
+ AppText(text = remark, style = LegadoTheme.typography.labelMediumEmphasized)
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(4.dp))
+ AppText(
+ text = book.getDisplayIntro().orEmpty().ifBlank { stringResource(R.string.intro_show_null) },
+ style = LegadoTheme.typography.bodyMedium,
+ )
+ }
+}
+@Composable
+private fun BookInfoDialogs(
+ state: BookInfoUiState,
+ onIntent: (BookInfoIntent) -> Unit,
+ onBack: () -> Unit,
+) {
+ val dialog = state.dialog
+ var deleteOriginal by remember(dialog) { mutableStateOf(io.legado.app.help.config.LocalConfig.deleteBookOriginal) }
+ var remarkText by remember(dialog) { mutableStateOf((dialog as? BookInfoDialog.EditRemark)?.remark.orEmpty()) }
+
+ if (dialog is BookInfoDialog.AddToShelfOnBack) {
+ AppAlertDialog(
+ show = true,
+ onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
+ title = stringResource(R.string.add_to_bookshelf),
+ text = stringResource(R.string.check_add_bookshelf, state.book?.name.orEmpty()),
+ confirmText = stringResource(android.R.string.ok),
+ onConfirm = { onIntent(BookInfoIntent.ConfirmBackAddToShelf) },
+ dismissText = stringResource(android.R.string.cancel),
+ onDismiss = onBack,
+ )
+ }
+
+ if (dialog is BookInfoDialog.DeleteBook) {
+ AppAlertDialog(
+ show = true,
+ onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
+ title = stringResource(R.string.draw),
+ text = stringResource(R.string.sure_del),
+ confirmText = stringResource(android.R.string.ok),
+ onConfirm = {
+ io.legado.app.help.config.LocalConfig.deleteBookOriginal = deleteOriginal
+ onIntent(BookInfoIntent.ConfirmDelete(deleteOriginal))
+ },
+ dismissText = stringResource(android.R.string.cancel),
+ onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
+ content = {
+ if (dialog.isLocal) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ androidx.compose.material3.Checkbox(
+ checked = deleteOriginal,
+ onCheckedChange = { deleteOriginal = it },
+ colors = androidx.compose.material3.CheckboxDefaults.colors(
+ checkedColor = LegadoTheme.colorScheme.primary,
+ checkmarkColor = LegadoTheme.colorScheme.onPrimary,
+ uncheckedColor = LegadoTheme.colorScheme.onSurfaceVariant,
+ )
+ )
+ Text(text = stringResource(R.string.delete_book_file))
+ }
+ }
+ }
+ )
+ }
+
+ if (dialog is BookInfoDialog.EditRemark) {
+ AppAlertDialog(
+ show = true,
+ onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
+ title = stringResource(R.string.edit_remark),
+ confirmText = stringResource(android.R.string.ok),
+ onConfirm = { onIntent(BookInfoIntent.UpdateRemark(remarkText)) },
+ dismissText = stringResource(android.R.string.cancel),
+ onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
+ content = {
+ AppTextField(
+ value = remarkText,
+ onValueChange = { remarkText = it },
+ label = "备注",
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ )
+ }
+
+ if (dialog is BookInfoDialog.UnsupportedWebFile) {
+ AppAlertDialog(
+ show = true,
+ onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
+ title = stringResource(R.string.draw),
+ text = stringResource(R.string.file_not_supported, dialog.webFile.name),
+ confirmText = stringResource(R.string.open_fun),
+ onConfirm = { onIntent(BookInfoIntent.OpenUnsupportedWebFile(dialog.webFile)) },
+ dismissText = stringResource(android.R.string.cancel),
+ onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
+ )
+ }
+
+ if (dialog is BookInfoDialog.PhotoPreview) {
+ AppAlertDialog(
+ show = true,
+ onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
+ title = stringResource(R.string.img_cover),
+ confirmText = stringResource(android.R.string.ok),
+ onConfirm = { onIntent(BookInfoIntent.DismissDialog) },
+ content = {
+ AsyncImage(
+ model = dialog.path,
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(max = 420.dp),
+ contentScale = ContentScale.Fit,
+ )
+ }
+ )
+ }
+
+ if (state.isBusy) {
+ AppAlertDialog(
+ show = true,
+ onDismissRequest = {},
+ content = {
+ Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator(
+ color = LegadoTheme.colorScheme.primary,
+ trackColor = LegadoTheme.colorScheme.surfaceContainerHighest,
+ )
+ }
+ }
+ )
+ }
+
+ AppLogSheet(show = state.showAppLogSheet, onDismissRequest = { onIntent(BookInfoIntent.DismissAppLogSheet) })
+}
diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt
new file mode 100644
index 000000000..274d03e07
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt
@@ -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,
+ 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(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) -> Unit,
+ onAddAsNew: (Book, List) -> 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(null) }
+ var mismatchBook by remember { mutableStateOf(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() },
+ 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() }
+ })
+ }
+}
diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
index f5776d12e..50b17063b 100644
--- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
+++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
@@ -1,9 +1,9 @@
package io.legado.app.ui.book.info
+import android.app.Activity.RESULT_OK
import android.app.Application
import android.content.Intent
import android.net.Uri
-import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import io.legado.app.R
import io.legado.app.base.BaseViewModel
@@ -19,6 +19,7 @@ import io.legado.app.data.repository.RemoteBookRepository
import io.legado.app.exception.NoBooksDirException
import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.book.BookHelp
+import io.legado.app.help.book.addType
import io.legado.app.help.book.getExportFileName
import io.legado.app.help.book.isLocal
import io.legado.app.help.book.isNotShelf
@@ -26,6 +27,8 @@ import io.legado.app.help.book.isSameNameAuthor
import io.legado.app.help.book.isWebFile
import io.legado.app.help.book.removeType
import io.legado.app.help.book.updateTo
+import io.legado.app.help.config.AppConfig
+import io.legado.app.help.config.LocalConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.webdav.ObjectNotFoundException
import io.legado.app.model.AudioPlay
@@ -37,381 +40,375 @@ import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.webBook.WebBook
import io.legado.app.utils.ArchiveUtils
+import io.legado.app.utils.ConvertUtils
+import io.legado.app.utils.FileDoc
+import io.legado.app.utils.GSON
import io.legado.app.utils.UrlUtil
import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
class BookInfoViewModel(
application: Application,
private val remoteBookRepository: RemoteBookRepository
) : BaseViewModel(application) {
- val bookData = MutableLiveData()
- val chapterListData = MutableLiveData>()
- val webFiles = mutableListOf()
+
+ private val _uiState = MutableStateFlow(BookInfoUiState())
+ val uiState = _uiState.asStateFlow()
+
+ private val _effects = MutableSharedFlow(extraBufferCapacity = 8)
+ val effects = _effects.asSharedFlow()
+
+ private var currentBook: Book? = null
+ private var currentChapterList: List = emptyList()
+ private var currentWebFiles: List = emptyList()
+ private var currentKindLabels: List = emptyList()
+ private var currentGroupNames: String? = null
+ private var chapterChanged = false
+
var inBookshelf = false
+ private set
var bookSource: BookSource? = null
+ private set
+
private var changeSourceCoroutine: Coroutine<*>? = null
- val waitDialogData = MutableLiveData()
- val actionLive = MutableLiveData()
fun initData(intent: Intent) {
+ if (currentBook != null) return
execute {
val bookUrl = intent.getStringExtra("bookUrl") ?: ""
appDb.bookDao.getBook(bookUrl)?.let {
inBookshelf = !it.isNotShelf
- upBook(it)
- return@execute
+ return@execute it
}
appDb.searchBookDao.getSearchBook(bookUrl)?.toBook()?.let {
- upBook(it)
- return@execute
+ inBookshelf = false
+ return@execute it
}
throw NoStackTraceException("未找到书籍")
- }
- }
-
- fun upBook(intent: Intent) {
- execute {
- val name = intent.getStringExtra("name") ?: ""
- val author = intent.getStringExtra("author") ?: ""
- val bookUrl = intent.getStringExtra("bookUrl") ?: ""
- val book = when {
- bookUrl.isNotBlank() -> appDb.bookDao.getBook(bookUrl)
- else -> appDb.bookDao.getBook(name, author)
- }
- book?.let { upBook(it) }
- }
- }
-
- private fun upBook(book: Book) {
- execute {
- bookData.postValue(book)
- upCoverByRule(book)
- bookSource = if (book.isLocal) null else
- appDb.bookSourceDao.getBookSource(book.origin)
- if (book.tocUrl.isEmpty() && !book.isLocal) {
- loadBookInfo(book, runPreUpdateJs = inBookshelf)
- } else {
- val chapterList = appDb.bookChapterDao.getChapterList(book.bookUrl)
- if (chapterList.isNotEmpty()) {
- chapterListData.postValue(chapterList)
- } else {
- loadChapter(book)
- }
- }
- }
- }
-
- private fun upCoverByRule(book: Book) {
- execute {
- if (book.coverUrl.isNullOrBlank() && book.customCoverUrl.isNullOrBlank()) {
- val coverUrl = BookCover.searchCover(book)
- if (coverUrl.isNullOrBlank()) {
- return@execute
- }
- book.customCoverUrl = coverUrl
- bookData.postValue(book)
- if (inBookshelf) {
- saveBook(book)
- }
- }
- }
- }
-
- fun refreshBook(book: Book) {
- executeLazy(executeContext = IO) {
- if (book.isLocal) {
- book.tocUrl = ""
- remoteBookRepository.refreshLocalBook(book)
- } else {
- val bs = bookSource ?: return@executeLazy
- if (book.originName != bs.bookSourceName) {
- book.originName = bs.bookSourceName
- }
- }
+ }.onSuccess {
+ upBook(it)
}.onError {
- when (it) {
- is ObjectNotFoundException -> {
- book.origin = BookType.localTag
- }
-
- else -> {
- AppLog.put("下载远程书籍<${book.name}>失败", it)
- }
- }
- }.onFinally {
- loadBookInfo(book, false)
- }.start()
- }
-
- fun syncFromRemote() {
- val book = bookData.value ?: return
- if (!book.isLocal) return
-
- execute {
- waitDialogData.postValue(true)
- val newBook = remoteBookRepository.syncBookFromRemote(book)
-
- appDb.bookDao.delete(book)
- appDb.bookDao.insert(newBook)
-
- loadChapter(newBook)
-
- bookData.postValue(newBook)
- context.toastOnUi("同步完成")
- }.onFinally {
- waitDialogData.postValue(false)
- }.onError {
- context.toastOnUi(it.localizedMessage)
+ context.toastOnUi(it.localizedMessage ?: "未找到书籍")
+ emitEffect(BookInfoEffect.Finish(afterTransition = true))
}
}
- fun uploadBook(book: Book, success: () -> Unit) {
- execute {
- waitDialogData.postValue(true)
- remoteBookRepository.uploadBook(book)
- saveBook(book)
- success.invoke()
- }.onFinally {
- waitDialogData.postValue(false)
- }.onError {
- context.toastOnUi(it.localizedMessage)
- }
- }
-
- fun loadBookInfo(
- book: Book,
- canReName: Boolean = true,
- runPreUpdateJs: Boolean = true,
- scope: CoroutineScope = viewModelScope
- ) {
- if (book.isLocal) {
- LocalBook.upBookInfo(book)
- bookData.postValue(book)
- loadChapter(book)
- } else {
- val bookSource = bookSource ?: let {
- chapterListData.postValue(emptyList())
- context.toastOnUi(R.string.error_no_source)
- return
+ fun onIntent(intent: BookInfoIntent) {
+ when (intent) {
+ BookInfoIntent.BackPressed -> onBackPressed()
+ BookInfoIntent.DismissSheet -> dismissSheet()
+ BookInfoIntent.DismissDialog -> dismissDialog()
+ is BookInfoIntent.MenuAction -> handleMenuAction(intent.action)
+ is BookInfoIntent.AuthorClick -> onAuthorClick(intent.longClick)
+ is BookInfoIntent.BookNameClick -> onBookNameClick(intent.longClick)
+ BookInfoIntent.OriginClick -> onOriginClick()
+ BookInfoIntent.DismissAppLogSheet -> {
+ _uiState.update { it.copy(showAppLogSheet = false) }
}
- WebBook.getBookInfo(scope, bookSource, book, canReName = canReName)
- .onSuccess(IO) {
- val dbBook = appDb.bookDao.getBook(book.name, book.author)
- if (!inBookshelf && dbBook != null && !dbBook.isNotShelf && dbBook.origin == book.origin) {
- /**
- * book 来自搜索时(inBookshelf == false),搜索的书名不存在于书架,但是加载详情后,书名更新,存在同名书籍
- * 此时 book 的数据会与数据库中的不同,需要更新 #3652 #4619
- * book 加载详情后虽然书名作者相同,但是又可能不是数据库中(书源不同)的那本书 #3149
- */
- dbBook.updateTo(it)
- inBookshelf = true
+
+ BookInfoIntent.ReadClick -> onReadClick()
+ BookInfoIntent.ShelfClick -> onShelfClick()
+ BookInfoIntent.TocClick -> onTocClick()
+ BookInfoIntent.CoverClick -> setSheet(BookInfoSheet.CoverPicker)
+ BookInfoIntent.CoverLongClick -> currentBook?.getDisplayCover()?.takeIf { it.isNotBlank() }
+ ?.let { showDialog(BookInfoDialog.PhotoPreview(it)) }
+
+ BookInfoIntent.GroupClick -> setSheet(BookInfoSheet.GroupPicker)
+ BookInfoIntent.ChangeSourceClick -> setSheet(BookInfoSheet.SourcePicker)
+ BookInfoIntent.RemarkClick -> showDialog(BookInfoDialog.EditRemark(currentBook?.remark))
+ BookInfoIntent.ConfirmBackAddToShelf -> {
+ dismissDialog()
+ addToBookshelf {
+ emitEffect(BookInfoEffect.Finish(afterTransition = true))
+ }
+ }
+
+ is BookInfoIntent.ConfirmDelete -> {
+ dismissDialog()
+ deleteBook(intent.deleteOriginal)
+ }
+
+ is BookInfoIntent.UpdateRemark -> {
+ dismissDialog()
+ saveRemark(intent.remark)
+ }
+
+ is BookInfoIntent.SelectGroup -> {
+ dismissSheet()
+ updateGroup(intent.groupId)
+ }
+
+ is BookInfoIntent.SelectCover -> {
+ dismissSheet()
+ updateCover(intent.coverUrl)
+ }
+
+ is BookInfoIntent.ReplaceWithSource -> {
+ dismissSheet()
+ changeTo(intent.source, intent.book, intent.toc)
+ }
+
+ is BookInfoIntent.AddSourceAsNewBook -> {
+ addToBookshelf(intent.book, intent.toc) {
+ context.toastOnUi("已添加到书架")
+ }
+ }
+
+ is BookInfoIntent.SelectWebFile -> handleWebFileSelection(
+ intent.webFile,
+ intent.openAfterImport
+ )
+
+ is BookInfoIntent.OpenUnsupportedWebFile -> {
+ dismissDialog()
+ importOrDownloadWebFile(intent.webFile) { uri ->
+ emitEffect(BookInfoEffect.OpenFile(uri, "*/*"))
+ }
+ }
+
+ is BookInfoIntent.SelectArchiveEntry -> {
+ dismissSheet()
+ importArchiveBook(intent.archiveUri, intent.entryName) { book ->
+ if (intent.openAfterImport) {
+ openReader(book)
}
- bookData.postValue(it)
- if (inBookshelf) {
- it.save()
- }
- if (it.isWebFile) {
- loadWebFile(it)
- } else {
- loadChapter(it, runPreUpdateJs)
- }
- }.onError {
- AppLog.put("获取书籍信息失败\n${it.localizedMessage}", it)
- context.toastOnUi(R.string.error_get_book_info)
- }
- }
- }
-
- private fun loadChapter(
- book: Book,
- runPreUpdateJs: Boolean = true,
- scope: CoroutineScope = viewModelScope
- ) {
- if (book.isLocal) {
- execute(scope) {
- LocalBook.getChapterList(book).let {
- appDb.bookDao.update(book)
- appDb.bookChapterDao.delByBook(book.bookUrl)
- appDb.bookChapterDao.insert(*it.toTypedArray())
- ReadBook.onChapterListUpdated(book)
- bookData.postValue(book)
- chapterListData.postValue(it)
- }
- }.onError {
- context.toastOnUi("LoadTocError:${it.localizedMessage}")
- }
- } else {
- val bookSource = bookSource ?: let {
- chapterListData.postValue(emptyList())
- context.toastOnUi(R.string.error_no_source)
- return
- }
- val oldBook = book.copy()
- WebBook.getChapterList(scope, bookSource, book, runPreUpdateJs)
- .onSuccess(IO) {
- if (inBookshelf) {
- appDb.bookDao.replace(oldBook, book)
- /**
- * runPreUpdateJs 有可能会修改 book 的 bookUrl
- */
- if (oldBook.bookUrl != book.bookUrl) {
- BookHelp.updateCacheFolder(oldBook, book)
- }
- appDb.bookChapterDao.delByBook(oldBook.bookUrl)
- appDb.bookChapterDao.insert(*it.toTypedArray())
- ReadBook.onChapterListUpdated(book)
- }
- bookData.postValue(book)
- chapterListData.postValue(it)
- }.onError {
- chapterListData.postValue(emptyList())
- AppLog.put("获取目录失败\n${it.localizedMessage}", it)
- context.toastOnUi(R.string.error_get_chapter_list)
- }
- }
- }
-
-
- fun loadGroup(groupId: Long, success: ((groupNames: String?) -> Unit)) {
- execute {
- appDb.bookGroupDao.getGroupNames(groupId).joinToString(",")
- }.onSuccess {
- success.invoke(it)
- }
- }
-
- private fun loadWebFile(book: Book) {
- execute {
- webFiles.clear()
- val fileNameNoExtension = if (book.author.isBlank()) book.name
- else "${book.name} 作者:${book.author}"
- book.downloadUrls!!.map {
- val analyzeUrl = AnalyzeUrl(
- it, source = bookSource,
- coroutineContext = coroutineContext
- )
- val mFileName = UrlUtil.getFileName(analyzeUrl)
- ?: "${fileNameNoExtension}.${analyzeUrl.type}"
- WebFile(it, mFileName)
- }
- }.onError {
- context.toastOnUi("LoadWebFileError\n${it.localizedMessage}")
- }.onSuccess {
- webFiles.addAll(it)
- }
- }
-
- /* 导入或者下载在线文件 */
- fun importOrDownloadWebFile(webFile: WebFile, success: ((T) -> Unit)?) {
- bookSource ?: return
- execute {
- waitDialogData.postValue(true)
- if (webFile.isSupported) {
- val book = LocalBook.importFileOnLine(
- webFile.url,
- bookData.value!!.getExportFileName(webFile.suffix),
- bookSource
- )
- changeToLocalBook(book)
- } else {
- LocalBook.saveBookFile(
- webFile.url,
- bookData.value!!.getExportFileName(webFile.suffix),
- bookSource
- )
- }
- }.onSuccess {
- @Suppress("unchecked_cast")
- success?.invoke(it as T)
- }.onError {
- when (it) {
- is NoBooksDirException -> actionLive.postValue("selectBooksDir")
- else -> {
- AppLog.put("ImportWebFileError\n${it.localizedMessage}", it)
- context.toastOnUi("ImportWebFileError\n${it.localizedMessage}")
- webFiles.remove(webFile)
}
}
- }.onFinally {
- waitDialogData.postValue(false)
}
}
- fun getArchiveFilesName(archiveFileUri: Uri, onSuccess: (List) -> Unit) {
- execute {
- ArchiveUtils.getArchiveFilesName(archiveFileUri) {
- AppPattern.bookFileRegex.matches(it)
+ fun openEdit() {
+ currentBook?.let {
+ emitEffect(BookInfoEffect.OpenBookInfoEdit(it.bookUrl))
+ }
+ }
+
+ fun showAppLog() {
+ _uiState.update { it.copy(showAppLogSheet = true) }
+ }
+
+ fun refreshCurrentBook() {
+ currentBook?.let {
+ refreshBook(it)
+ }
+ }
+
+ fun onSourceEdited() {
+ currentBook?.let { book ->
+ bookSource = appDb.bookSourceDao.getBookSource(book.origin)
+ syncUiState()
+ refreshBook(book)
+ }
+ }
+
+ fun onInfoEdited() {
+ currentBook?.bookUrl?.let { bookUrl ->
+ appDb.bookDao.getBook(bookUrl)?.let { upBook(it) }
+ }
+ }
+
+ fun onTocResult(result: Triple?) {
+ if (result == null) {
+ if (!inBookshelf) {
+ delBook()
}
- }.onError {
- AppLog.put("getArchiveEntriesName Error:\n${it.localizedMessage}", it)
- context.toastOnUi("getArchiveEntriesName Error:\n${it.localizedMessage}")
- }.onSuccess {
- onSuccess.invoke(it)
+ return
}
- }
-
- fun importArchiveBook(
- archiveFileUri: Uri,
- archiveEntryName: String,
- success: ((Book) -> Unit)? = null
- ) {
+ chapterChanged = result.third
+ val book = currentBook ?: return
execute {
- val suffix = archiveEntryName.substringAfterLast(".")
- LocalBook.importArchiveFile(
- archiveFileUri,
- bookData.value!!.getExportFileName(suffix)
- ) {
- it.contains(archiveEntryName)
- }.first()
+ book.durChapterIndex = result.first
+ book.durChapterPos = result.second
+ appDb.bookDao.update(book)
+ book
}.onSuccess {
- val book = changeToLocalBook(it)
- success?.invoke(book)
- }.onError {
- AppLog.put("importArchiveBook Error:\n${it.localizedMessage}", it)
- context.toastOnUi("importArchiveBook Error:\n${it.localizedMessage}")
+ currentBook = it
+ syncUiState(isTocLoading = false)
+ openReader(it)
}
}
- fun changeTo(source: BookSource, book: Book, toc: List) {
- changeSourceCoroutine?.cancel()
- changeSourceCoroutine = execute {
- bookSource = source
- bookData.value?.migrateTo(book, toc)
+ fun onReaderResult(resultCode: Int) {
+ when (resultCode) {
+ RESULT_OK -> {
+ inBookshelf = true
+ syncUiState()
+ }
+
+ io.legado.app.ui.book.read.ReadBookActivity.RESULT_DELETED -> {
+ emitEffect(BookInfoEffect.Finish(resultCode = RESULT_OK))
+ }
+ }
+ }
+
+ fun toggleCanUpdate() {
+ currentBook?.let { book ->
+ book.canUpdate = !book.canUpdate
if (inBookshelf) {
- book.removeType(BookType.updateError)
- bookData.value?.delete()
- appDb.bookDao.insert(book)
- appDb.bookChapterDao.insert(*toc.toTypedArray())
+ if (!book.canUpdate) {
+ book.removeType(BookType.updateError)
+ }
+ saveBook(book)
+ }
+ syncUiState()
+ }
+ }
+
+ fun toggleSplitLongChapter() {
+ currentBook?.takeIf { it.isLocal && it.type and BookType.text > 0 }?.let { book ->
+ book.setSplitLongChapter(!book.getSplitLongChapter())
+ syncUiState(isTocLoading = true)
+ loadBookInfo(book, canReName = false)
+ if (!book.getSplitLongChapter()) {
+ context.toastOnUi(context.getString(R.string.need_more_time_load_content))
+ }
+ }
+ }
+
+ fun toggleDeleteAlert() {
+ LocalConfig.bookInfoDeleteAlert = !LocalConfig.bookInfoDeleteAlert
+ syncUiState()
+ }
+
+ fun requestSourceVariableDialog() {
+ execute {
+ val source = bookSource ?: throw NoStackTraceException("书源不存在")
+ val comment = source.getDisplayVariableComment("源变量可在js中通过source.getVariable()获取")
+ val variable = source.getVariable()
+ BookInfoEffect.ShowVariableDialog(
+ title = context.getString(R.string.set_source_variable),
+ key = source.getKey(),
+ variable = variable,
+ comment = comment,
+ )
+ }.onSuccess {
+ emitEffect(it)
+ }.onError {
+ context.toastOnUi(it.localizedMessage ?: "书源不存在")
+ }
+ }
+
+ fun requestBookVariableDialog() {
+ execute {
+ val source = bookSource ?: throw NoStackTraceException("书源不存在")
+ val book = currentBook ?: throw NoStackTraceException("book is null")
+ val variable = book.getCustomVariable()
+ val comment = source.getDisplayVariableComment(
+ "书籍变量可在js中通过book.getVariable(\"custom\")获取"
+ )
+ BookInfoEffect.ShowVariableDialog(
+ title = context.getString(R.string.set_book_variable),
+ key = book.bookUrl,
+ variable = variable,
+ comment = comment,
+ )
+ }.onSuccess {
+ emitEffect(it)
+ }.onError {
+ context.toastOnUi(it.localizedMessage ?: "书源不存在")
+ }
+ }
+
+ fun setVariable(key: String, variable: String?) {
+ when (key) {
+ bookSource?.getKey() -> bookSource?.setVariable(variable)
+ currentBook?.bookUrl -> currentBook?.let {
+ it.putCustomVariable(variable)
+ if (inBookshelf) {
+ saveBook(it)
+ }
}
- bookData.postValue(book)
- chapterListData.postValue(toc)
- }.onFinally {
- postEvent(EventBus.SOURCE_CHANGED, book.bookUrl)
}
}
fun topBook() {
- execute {
- bookData.value?.let { book ->
+ currentBook?.let { book ->
+ execute {
val minOrder = appDb.bookDao.minOrder
book.order = minOrder - 1
book.durChapterTime = System.currentTimeMillis()
appDb.bookDao.update(book)
+ book
+ }.onSuccess {
+ currentBook = it
+ syncUiState()
+ }
+ }
+ }
+ fun syncFromRemote() {
+ val book = currentBook ?: return
+ if (!book.isLocal) return
+
+ execute {
+ setBusy(true)
+ val newBook = remoteBookRepository.syncBookFromRemote(book)
+ appDb.bookDao.delete(book)
+ appDb.bookDao.insert(newBook)
+ newBook
+ }.onSuccess { newBook ->
+ currentBook = newBook
+ inBookshelf = true
+ syncUiState(isTocLoading = true)
+ loadChapter(newBook)
+ context.toastOnUi("同步完成")
+ }.onFinally {
+ setBusy(false)
+ }.onError {
+ context.toastOnUi(it.localizedMessage)
+ }
+ }
+
+ fun uploadBook(success: () -> Unit) {
+ val book = currentBook ?: return
+ execute {
+ setBusy(true)
+ remoteBookRepository.uploadBook(book)
+ saveBook(book)
+ }.onSuccess {
+ success.invoke()
+ }.onFinally {
+ setBusy(false)
+ }.onError {
+ context.toastOnUi(it.localizedMessage)
+ }
+ }
+
+ fun clearCache() {
+ currentBook?.let { book ->
+ execute {
+ BookHelp.clearCache(book)
+ if (ReadBook.book?.bookUrl == book.bookUrl) {
+ ReadBook.clearTextChapter()
+ }
+ if (ReadManga.book?.bookUrl == book.bookUrl) {
+ ReadManga.clearMangaChapter()
+ }
+ }.onSuccess {
+ context.toastOnUi(R.string.clear_cache_success)
+ }.onError {
+ context.toastOnUi("清理缓存出错\n${it.localizedMessage}")
}
}
}
fun saveRemark(remark: String, success: (() -> Unit)? = null) {
- execute {
- bookData.value?.let {
- it.remark = remark
- it.save()
+ currentBook?.let { book ->
+ execute {
+ book.remark = remark
+ book.save()
+ book
+ }.onSuccess {
+ currentBook = it
+ syncUiState()
+ success?.invoke()
}
- }.onSuccess {
- success?.invoke()
}
}
@@ -432,98 +429,69 @@ class BookInfoViewModel(
} else if (AudioPlay.book?.isSameNameAuthor(book) == true) {
AudioPlay.book = book
}
+ book
+ }.onSuccess {
+ if (currentBook?.bookUrl == it.bookUrl) {
+ currentBook = it
+ syncUiState()
+ }
+ success?.invoke()
+ }
+ }
+
+ fun saveChapterList(success: (() -> Unit)? = null) {
+ execute {
+ appDb.bookChapterDao.insert(*currentChapterList.toTypedArray())
}.onSuccess {
success?.invoke()
}
}
- fun saveChapterList(success: (() -> Unit)?) {
+ fun addToBookshelf(success: (() -> Unit)? = null) {
+ val book = currentBook ?: return
execute {
- chapterListData.value?.let {
- appDb.bookChapterDao.insert(*it.toTypedArray())
+ book.removeType(BookType.notShelf)
+ if (book.order == 0) {
+ book.order = appDb.bookDao.minOrder - 1
}
+ appDb.bookDao.getBook(book.name, book.author)?.let {
+ book.durChapterIndex = it.durChapterIndex
+ book.durChapterPos = it.durChapterPos
+ book.durChapterTitle = it.durChapterTitle
+ }
+ if (ReadBook.book?.isSameNameAuthor(book) == true) {
+ ReadBook.book = book
+ } else if (AudioPlay.book?.isSameNameAuthor(book) == true) {
+ AudioPlay.book = book
+ }
+ book.save()
+ SourceCallBack.callBackBook(SourceCallBack.ADD_BOOK_SHELF, bookSource, book)
+ appDb.bookChapterDao.insert(*currentChapterList.toTypedArray())
+ book
}.onSuccess {
- success?.invoke()
- }
- }
-
- fun addToBookshelf(success: (() -> Unit)?) { //点击书架按钮或在加分组时触发
- execute {
- bookData.value?.let { book ->
- book.removeType(BookType.notShelf)
- if (book.order == 0) {
- book.order = appDb.bookDao.minOrder - 1
- }
- appDb.bookDao.getBook(book.name, book.author)?.let {
- book.durChapterIndex = it.durChapterIndex
- book.durChapterPos = it.durChapterPos
- book.durChapterTitle = it.durChapterTitle
- }
- if (ReadBook.book?.isSameNameAuthor(book) == true) {
- ReadBook.book = book
- } else if (AudioPlay.book?.isSameNameAuthor(book) == true) {
- AudioPlay.book = book
- }
- book.save()
- SourceCallBack.callBackBook(SourceCallBack.ADD_BOOK_SHELF, bookSource, book)
- }
- chapterListData.value?.let {
- appDb.bookChapterDao.insert(*it.toTypedArray())
- }
+ currentBook = it
inBookshelf = true
- }.onSuccess {
+ syncUiState()
success?.invoke()
}
}
- fun getBook(toastNull: Boolean = true): Book? {
- val book = bookData.value
- if (toastNull && book == null) {
- context.toastOnUi("book is null")
- }
- return book
- }
-
- fun delBook(deleteOriginal: Boolean = false, success: (() -> Unit)? = null) {
- execute {
- bookData.value?.let {
- inBookshelf = false
- if (it.isLocal) {
- LocalBook.deleteBook(it, deleteOriginal)
- }
- it.delete()
- }
- }.onSuccess {
- success?.invoke()
- }
- }
-
- fun clearCache(book: Book) {
- execute {
- BookHelp.clearCache(book)
- if (ReadBook.book?.bookUrl == book.bookUrl) {
- ReadBook.clearTextChapter()
- }
- if (ReadManga.book?.bookUrl == book.bookUrl) {
- ReadManga.clearMangaChapter()
- }
- }.onSuccess {
- context.toastOnUi(R.string.clear_cache_success)
- }.onError {
- context.toastOnUi("清理缓存出错\n${it.localizedMessage}")
- }
- }
-
fun addToBookshelf(book: Book, toc: List, success: (() -> Unit)? = null) {
execute {
book.removeType(BookType.notShelf)
if (book.order == 0) {
book.order = appDb.bookDao.minOrder - 1
}
-
appDb.bookDao.insert(book)
appDb.bookChapterDao.insert(*toc.toTypedArray())
+ book
}.onSuccess {
+ if (currentBook?.bookUrl == it.bookUrl) {
+ currentBook = it
+ currentChapterList = toc
+ inBookshelf = true
+ syncUiState(isTocLoading = false)
+ }
success?.invoke()
}.onError {
AppLog.put("添加书籍到书架失败", it)
@@ -531,41 +499,632 @@ class BookInfoViewModel(
}
}
- fun upEditBook() {
- bookData.value?.let {
- appDb.bookDao.getBook(it.bookUrl)?.let { book ->
- bookData.postValue(book)
+ fun delBook(deleteOriginal: Boolean = false, success: (() -> Unit)? = null) {
+ val book = currentBook ?: return
+ execute {
+ inBookshelf = false
+ if (book.isLocal) {
+ LocalBook.deleteBook(book, deleteOriginal)
+ }
+ book.delete()
+ }.onSuccess {
+ success?.invoke()
+ }
+ }
+
+ fun refreshBook(book: Book) {
+ syncUiState(isTocLoading = true)
+ execute {
+ if (book.isLocal) {
+ book.tocUrl = ""
+ remoteBookRepository.refreshLocalBook(book)
+ } else {
+ val bs = bookSource ?: return@execute
+ if (book.originName != bs.bookSourceName) {
+ book.originName = bs.bookSourceName
+ }
+ }
+ book
+ }.onError {
+ when (it) {
+ is ObjectNotFoundException -> {
+ book.origin = BookType.localTag
+ }
+
+ else -> {
+ AppLog.put("下载远程书籍<${book.name}>失败", it)
+ }
+ }
+ }.onFinally {
+ loadBookInfo(book, canReName = false)
+ }
+ }
+
+ fun loadBookInfo(
+ book: Book,
+ canReName: Boolean = true,
+ runPreUpdateJs: Boolean = true,
+ scope: CoroutineScope = viewModelScope,
+ ) {
+ syncUiState(isTocLoading = true)
+ if (book.isLocal) {
+ LocalBook.upBookInfo(book)
+ currentBook = book
+ syncUiState(isTocLoading = true)
+ loadChapter(book)
+ } else {
+ val source = bookSource ?: run {
+ currentChapterList = emptyList()
+ syncUiState(isTocLoading = false)
+ context.toastOnUi(R.string.error_no_source)
+ return
+ }
+ WebBook.getBookInfo(scope, source, book, canReName = canReName)
+ .onSuccess(IO) { loadedBook ->
+ val dbBook = appDb.bookDao.getBook(loadedBook.name, loadedBook.author)
+ if (!inBookshelf && dbBook != null && !dbBook.isNotShelf && dbBook.origin == loadedBook.origin) {
+ dbBook.updateTo(loadedBook)
+ inBookshelf = true
+ }
+ currentBook = loadedBook
+ if (inBookshelf) {
+ loadedBook.save()
+ }
+ syncUiState(isTocLoading = true)
+ refreshMeta(loadedBook)
+ if (loadedBook.isWebFile) {
+ loadWebFile(loadedBook)
+ currentChapterList = emptyList()
+ syncUiState(isTocLoading = false)
+ } else {
+ loadChapter(loadedBook, runPreUpdateJs)
+ }
+ }.onError {
+ AppLog.put("获取书籍信息失败\n${it.localizedMessage}", it)
+ context.toastOnUi(R.string.error_get_book_info)
+ syncUiState(isTocLoading = false)
+ }
+ }
+ }
+ fun changeTo(source: BookSource, book: Book, toc: List) {
+ changeSourceCoroutine?.cancel()
+ changeSourceCoroutine = execute {
+ bookSource = source
+ currentBook?.migrateTo(book, toc)
+ if (inBookshelf) {
+ book.removeType(BookType.updateError)
+ currentBook?.delete()
+ appDb.bookDao.insert(book)
+ appDb.bookChapterDao.insert(*toc.toTypedArray())
+ }
+ book
+ }.onSuccess {
+ currentBook = it
+ currentChapterList = toc
+ currentGroupNames = null
+ currentKindLabels = emptyList()
+ syncUiState(isTocLoading = false)
+ refreshMeta(it)
+ }.onFinally {
+ postEvent(EventBus.SOURCE_CHANGED, book.bookUrl)
+ }
+ }
+
+ private fun upBook(book: Book) {
+ currentBook = book
+ currentChapterList = emptyList()
+ currentWebFiles = emptyList()
+ currentKindLabels = emptyList()
+ currentGroupNames = null
+ syncUiState(isTocLoading = true)
+ refreshMeta(book)
+ upCoverByRule(book)
+ bookSource = if (book.isLocal) null else appDb.bookSourceDao.getBookSource(book.origin)
+ syncUiState(isTocLoading = true)
+ if (book.tocUrl.isEmpty() && !book.isLocal) {
+ loadBookInfo(book, runPreUpdateJs = inBookshelf)
+ } else {
+ execute {
+ appDb.bookChapterDao.getChapterList(book.bookUrl)
+ }.onSuccess { chapters ->
+ if (chapters.isNotEmpty()) {
+ currentChapterList = chapters
+ syncUiState(isTocLoading = false)
+ } else {
+ loadChapter(book)
+ }
+ }.onError {
+ loadChapter(book)
}
}
}
+ private fun upCoverByRule(book: Book) {
+ execute {
+ if (book.coverUrl.isNullOrBlank() && book.customCoverUrl.isNullOrBlank()) {
+ val coverUrl = BookCover.searchCover(book)
+ if (!coverUrl.isNullOrBlank()) {
+ book.customCoverUrl = coverUrl
+ if (inBookshelf) {
+ saveBook(book)
+ }
+ }
+ }
+ book
+ }.onSuccess {
+ if (currentBook?.bookUrl == it.bookUrl) {
+ currentBook = it
+ syncUiState()
+ }
+ }
+ }
+
+ private fun refreshMeta(book: Book) {
+ execute {
+ val kinds = book.getKindList().toMutableList()
+ if (book.isLocal) {
+ val size = FileDoc.fromFile(book.bookUrl).size
+ if (size > 0) {
+ kinds.add(ConvertUtils.formatFileSize(size))
+ }
+ }
+ val groupNames = appDb.bookGroupDao.getGroupNames(book.group).joinToString(",")
+ kinds.toList() to groupNames.ifBlank { null }
+ }.onSuccess {
+ currentKindLabels = it.first
+ currentGroupNames = it.second
+ syncUiState()
+ }
+ }
+
+ private fun loadChapter(
+ book: Book,
+ runPreUpdateJs: Boolean = true,
+ scope: CoroutineScope = viewModelScope,
+ ) {
+ syncUiState(isTocLoading = true)
+ if (book.isLocal) {
+ execute(scope) {
+ LocalBook.getChapterList(book).also {
+ appDb.bookDao.update(book)
+ appDb.bookChapterDao.delByBook(book.bookUrl)
+ appDb.bookChapterDao.insert(*it.toTypedArray())
+ ReadBook.onChapterListUpdated(book)
+ }
+ }.onSuccess {
+ currentBook = book
+ currentChapterList = it
+ syncUiState(isTocLoading = false)
+ }.onError {
+ currentChapterList = emptyList()
+ syncUiState(isTocLoading = false)
+ context.toastOnUi("LoadTocError:${it.localizedMessage}")
+ }
+ } else {
+ val source = bookSource ?: run {
+ currentChapterList = emptyList()
+ syncUiState(isTocLoading = false)
+ context.toastOnUi(R.string.error_no_source)
+ return
+ }
+ val oldBook = book.copy()
+ WebBook.getChapterList(scope, source, book, runPreUpdateJs)
+ .onSuccess(IO) { chapters ->
+ if (inBookshelf) {
+ appDb.bookDao.replace(oldBook, book)
+ if (oldBook.bookUrl != book.bookUrl) {
+ BookHelp.updateCacheFolder(oldBook, book)
+ }
+ appDb.bookChapterDao.delByBook(oldBook.bookUrl)
+ appDb.bookChapterDao.insert(*chapters.toTypedArray())
+ ReadBook.onChapterListUpdated(book)
+ }
+ currentBook = book
+ currentChapterList = chapters
+ syncUiState(isTocLoading = false)
+ }.onError {
+ currentChapterList = emptyList()
+ syncUiState(isTocLoading = false)
+ AppLog.put("获取目录失败\n${it.localizedMessage}", it)
+ context.toastOnUi(R.string.error_get_chapter_list)
+ }
+ }
+ }
+
+ private fun loadWebFile(book: Book) {
+ execute {
+ val fileNameNoExtension = if (book.author.isBlank()) book.name else "${book.name} 作者:${book.author}"
+ book.downloadUrls.orEmpty().map { url ->
+ val analyzeUrl = AnalyzeUrl(
+ url,
+ source = bookSource,
+ coroutineContext = coroutineContext,
+ )
+ val fileName = UrlUtil.getFileName(analyzeUrl)
+ ?: "${fileNameNoExtension}.${analyzeUrl.type}"
+ BookInfoWebFile(url = url, name = fileName)
+ }
+ }.onSuccess {
+ currentWebFiles = it
+ syncUiState(isTocLoading = false)
+ }.onError {
+ currentWebFiles = emptyList()
+ context.toastOnUi("LoadWebFileError\n${it.localizedMessage}")
+ syncUiState(isTocLoading = false)
+ }
+ }
+
+ private fun onBackPressed() {
+ if (!inBookshelf && AppConfig.showAddToShelfAlert && currentBook != null) {
+ showDialog(BookInfoDialog.AddToShelfOnBack)
+ } else {
+ emitEffect(BookInfoEffect.Finish(afterTransition = true))
+ }
+ }
+
+ private fun onReadClick() {
+ val book = currentBook ?: return
+ if (book.isWebFile) {
+ setSheet(BookInfoSheet.WebFiles(openAfterImport = true))
+ } else {
+ readBook(book)
+ }
+ }
+
+ private fun onShelfClick() {
+ val book = currentBook ?: return
+ if (inBookshelf) {
+ showDialog(BookInfoDialog.DeleteBook(book.isLocal))
+ } else if (book.isWebFile) {
+ setSheet(BookInfoSheet.WebFiles(openAfterImport = false))
+ } else {
+ addToBookshelf()
+ }
+ }
+
+ private fun onTocClick() {
+ val book = currentBook ?: return
+ if (currentChapterList.isEmpty()) {
+ context.toastOnUi(R.string.chapter_list_empty)
+ return
+ }
+ if (!inBookshelf) {
+ saveBook(book) {
+ saveChapterList {
+ emitEffect(BookInfoEffect.OpenToc(book.bookUrl))
+ }
+ }
+ } else {
+ emitEffect(BookInfoEffect.OpenToc(book.bookUrl))
+ }
+ }
+
+ private fun updateGroup(groupId: Long) {
+ currentBook?.let { book ->
+ book.group = groupId
+ currentGroupNames = null
+ refreshMeta(book)
+ if (inBookshelf) {
+ saveBook(book)
+ } else if (groupId > 0) {
+ addToBookshelf()
+ } else {
+ syncUiState()
+ }
+ }
+ }
+
+ private fun updateCover(coverUrl: String) {
+ currentBook?.let { book ->
+ book.customCoverUrl = coverUrl
+ currentBook = book
+ syncUiState()
+ if (inBookshelf) {
+ saveBook(book)
+ }
+ }
+ }
+ private fun deleteBook(deleteOriginal: Boolean) {
+ currentBook?.let { book ->
+ SourceCallBack.callBackBook(SourceCallBack.DEL_BOOK_SHELF, bookSource, book)
+ delBook(deleteOriginal) {
+ emitEffect(BookInfoEffect.Finish(resultCode = RESULT_OK))
+ }
+ }
+ }
+
+ private fun handleWebFileSelection(webFile: BookInfoWebFile, openAfterImport: Boolean) {
+ when {
+ webFile.isSupported -> {
+ dismissSheet()
+ importOrDownloadWebFile(webFile) { book ->
+ if (openAfterImport) {
+ openReader(book)
+ }
+ }
+ }
+
+ webFile.isSupportDecompress -> {
+ importOrDownloadWebFile(webFile) { uri ->
+ getArchiveFilesName(uri) { fileNames ->
+ if (fileNames.size == 1) {
+ importArchiveBook(uri, fileNames.first()) { book ->
+ if (openAfterImport) {
+ openReader(book)
+ }
+ }
+ } else {
+ setSheet(
+ BookInfoSheet.ArchiveEntries(
+ archiveUri = uri,
+ entries = fileNames,
+ openAfterImport = openAfterImport,
+ )
+ )
+ }
+ }
+ }
+ }
+
+ else -> {
+ showDialog(BookInfoDialog.UnsupportedWebFile(webFile, openAfterImport))
+ }
+ }
+ }
+
+ private fun readBook(book: Book) {
+ if (!inBookshelf) {
+ book.addType(BookType.notShelf)
+ saveBook(book) {
+ saveChapterList {
+ openReader(book)
+ }
+ }
+ } else {
+ saveBook(book) {
+ openReader(book)
+ }
+ }
+ }
+
+ private fun openReader(book: Book) {
+ emitEffect(BookInfoEffect.OpenReader(book.uiCopy(), inBookshelf, chapterChanged))
+ }
+
+ private fun handleMenuAction(action: BookInfoMenuAction) {
+ val book = currentBook ?: return
+ when (action) {
+ BookInfoMenuAction.Edit -> openEdit()
+ BookInfoMenuAction.Share -> {
+ val bookJson = GSON.toJson(book)
+ emitEffect(
+ BookInfoEffect.RunSourceCallback(
+ event = SourceCallBack.CLICK_SHARE_BOOK,
+ source = bookSource,
+ book = book.uiCopy(),
+ action = BookInfoCallbackAction.ShareText(
+ chooserTitle = book.name,
+ text = "${book.bookUrl}#$bookJson",
+ )
+ )
+ )
+ }
+
+ BookInfoMenuAction.Upload -> uploadBook {
+ context.toastOnUi("上传成功")
+ }
+ BookInfoMenuAction.SyncRemote -> syncFromRemote()
+ BookInfoMenuAction.Refresh -> refreshCurrentBook()
+ BookInfoMenuAction.Login -> bookSource?.let {
+ emitEffect(BookInfoEffect.OpenSourceLogin(it.bookSourceUrl))
+ }
+
+ BookInfoMenuAction.Top -> topBook()
+ BookInfoMenuAction.SetSourceVariable -> requestSourceVariableDialog()
+ BookInfoMenuAction.SetBookVariable -> requestBookVariableDialog()
+ BookInfoMenuAction.CopyBookUrl -> emitEffect(
+ BookInfoEffect.RunSourceCallback(
+ event = SourceCallBack.CLICK_COPY_BOOK_URL,
+ source = bookSource,
+ book = book.uiCopy(),
+ action = BookInfoCallbackAction.CopyText(book.bookUrl),
+ )
+ )
+
+ BookInfoMenuAction.CopyTocUrl -> emitEffect(
+ BookInfoEffect.RunSourceCallback(
+ event = SourceCallBack.CLICK_COPY_TOC_URL,
+ source = bookSource,
+ book = book.uiCopy(),
+ action = BookInfoCallbackAction.CopyText(book.tocUrl),
+ )
+ )
+
+ BookInfoMenuAction.ToggleCanUpdate -> toggleCanUpdate()
+ BookInfoMenuAction.ToggleSplitLongChapter -> toggleSplitLongChapter()
+ BookInfoMenuAction.ToggleDeleteAlert -> toggleDeleteAlert()
+ BookInfoMenuAction.ClearCache -> emitEffect(
+ BookInfoEffect.RunSourceCallback(
+ event = SourceCallBack.CLICK_CLEAR_CACHE,
+ source = bookSource,
+ book = book.uiCopy(),
+ action = BookInfoCallbackAction.ClearCache,
+ )
+ )
+
+ BookInfoMenuAction.ShowLog -> showAppLog()
+ }
+ }
+
+ private fun onAuthorClick(longClick: Boolean) {
+ val book = currentBook ?: return
+ emitEffect(
+ BookInfoEffect.RunSourceCallback(
+ event = if (longClick) SourceCallBack.LONG_CLICK_AUTHOR else SourceCallBack.CLICK_AUTHOR,
+ source = bookSource,
+ book = book.uiCopy(),
+ action = BookInfoCallbackAction.Search(book.author),
+ )
+ )
+ }
+
+ private fun onBookNameClick(longClick: Boolean) {
+ val book = currentBook ?: return
+ emitEffect(
+ BookInfoEffect.RunSourceCallback(
+ event = if (longClick) SourceCallBack.LONG_CLICK_BOOK_NAME else SourceCallBack.CLICK_BOOK_NAME,
+ source = bookSource,
+ book = book.uiCopy(),
+ action = BookInfoCallbackAction.Search(book.name),
+ )
+ )
+ }
+
+ private fun onOriginClick() {
+ val book = currentBook ?: return
+ if (book.isLocal) return
+ if (!appDb.bookSourceDao.has(book.origin)) {
+ context.toastOnUi(R.string.error_no_source)
+ return
+ }
+ emitEffect(BookInfoEffect.OpenBookSourceEdit(book.origin))
+ }
+
+ fun getArchiveFilesName(archiveFileUri: Uri, onSuccess: (List) -> Unit) {
+ execute {
+ ArchiveUtils.getArchiveFilesName(archiveFileUri) {
+ AppPattern.bookFileRegex.matches(it)
+ }
+ }.onError {
+ AppLog.put("getArchiveEntriesName Error:\n${it.localizedMessage}", it)
+ context.toastOnUi("getArchiveEntriesName Error:\n${it.localizedMessage}")
+ }.onSuccess {
+ onSuccess.invoke(it)
+ }
+ }
+
+ fun importArchiveBook(
+ archiveFileUri: Uri,
+ archiveEntryName: String,
+ success: ((Book) -> Unit)? = null,
+ ) {
+ execute {
+ val suffix = archiveEntryName.substringAfterLast(".")
+ LocalBook.importArchiveFile(
+ archiveFileUri,
+ currentBook!!.getExportFileName(suffix)
+ ) {
+ it.contains(archiveEntryName)
+ }.first()
+ }.onSuccess {
+ val book = changeToLocalBook(it)
+ success?.invoke(book)
+ }.onError {
+ AppLog.put("importArchiveBook Error:\n${it.localizedMessage}", it)
+ context.toastOnUi("importArchiveBook Error:\n${it.localizedMessage}")
+ }
+ }
+
+ fun importOrDownloadWebFile(webFile: BookInfoWebFile, success: ((T) -> Unit)? = null) {
+ bookSource ?: return
+ val book = currentBook ?: return
+ execute {
+ setBusy(true)
+ if (webFile.isSupported) {
+ val localBook = LocalBook.importFileOnLine(
+ webFile.url,
+ book.getExportFileName(webFile.suffix),
+ bookSource
+ )
+ changeToLocalBook(localBook)
+ } else {
+ LocalBook.saveBookFile(
+ webFile.url,
+ book.getExportFileName(webFile.suffix),
+ bookSource
+ )
+ }
+ }.onSuccess {
+ @Suppress("UNCHECKED_CAST")
+ success?.invoke(it as T)
+ }.onError {
+ when (it) {
+ is NoBooksDirException -> emitEffect(BookInfoEffect.OpenSelectBooksDir)
+ else -> {
+ AppLog.put("ImportWebFileError\n${it.localizedMessage}", it)
+ context.toastOnUi("ImportWebFileError\n${it.localizedMessage}")
+ }
+ }
+ }.onFinally {
+ setBusy(false)
+ }
+ }
+
private fun changeToLocalBook(localBook: Book): Book {
- return LocalBook.mergeBook(localBook, bookData.value).let {
- bookData.postValue(it)
- loadChapter(it)
+ return LocalBook.mergeBook(localBook, currentBook).let {
+ currentBook = it
+ currentWebFiles = emptyList()
inBookshelf = true
+ syncUiState(isTocLoading = true)
+ refreshMeta(it)
+ loadChapter(it)
it
}
}
- data class WebFile(
- val url: String,
- val name: String,
- ) {
-
- override fun toString(): String {
- return name
- }
-
- // 后缀
- val suffix: String = UrlUtil.getSuffix(name)
-
- // txt epub umd pdf等文件
- val isSupported: Boolean = AppPattern.bookFileRegex.matches(name)
-
- // 压缩包形式的txt epub umd pdf文件
- val isSupportDecompress: Boolean = AppPattern.archiveFileRegex.matches(name)
-
+ private fun dismissSheet() {
+ setSheet(BookInfoSheet.None)
}
+ private fun setSheet(sheet: BookInfoSheet) {
+ _uiState.update { it.copy(sheet = sheet) }
+ }
+
+ private fun dismissDialog() {
+ showDialog(null)
+ }
+
+ private fun showDialog(dialog: BookInfoDialog?) {
+ _uiState.update { it.copy(dialog = dialog) }
+ }
+
+ private fun setBusy(isBusy: Boolean) {
+ _uiState.update { it.copy(isBusy = isBusy) }
+ }
+
+ private fun syncUiState(isTocLoading: Boolean = _uiState.value.isTocLoading) {
+ _uiState.update {
+ it.copy(
+ book = currentBook?.uiCopy(),
+ chapterList = currentChapterList,
+ webFiles = currentWebFiles,
+ kindLabels = currentKindLabels,
+ groupNames = currentGroupNames,
+ inBookshelf = inBookshelf,
+ bookSource = bookSource,
+ isTocLoading = isTocLoading,
+ )
+ }
+ }
+
+ private fun emitEffect(effect: BookInfoEffect) {
+ _effects.tryEmit(effect)
+ }
+
+ private fun Book.uiCopy(): Book {
+ return copy().also { snapshot ->
+ snapshot.infoHtml = infoHtml
+ snapshot.tocHtml = tocHtml
+ snapshot.downloadUrls = downloadUrls
+ }
+ }
}
+
+private val BookInfoWebFile.suffix: String
+ get() = UrlUtil.getSuffix(name)
+
+private val BookInfoWebFile.isSupported: Boolean
+ get() = AppPattern.bookFileRegex.matches(name)
+
+private val BookInfoWebFile.isSupportDecompress: Boolean
+ get() = AppPattern.archiveFileRegex.matches(name)
diff --git a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditScreen.kt b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditScreen.kt
index e53b377dd..a93f84c90 100644
--- a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditScreen.kt
+++ b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditScreen.kt
@@ -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
diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/LauncherIconPickerSheet.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/LauncherIconPickerSheet.kt
index 045d7fd84..5d9b968fa 100644
--- a/app/src/main/java/io/legado/app/ui/config/themeConfig/LauncherIconPickerSheet.kt
+++ b/app/src/main/java/io/legado/app/ui/config/themeConfig/LauncherIconPickerSheet.kt
@@ -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 {
diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt
index b34636be3..e2f846e22 100644
--- a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt
+++ b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt
@@ -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")
diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt
index 9b5e0d2fc..82c0b4843 100644
--- a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt
+++ b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt
@@ -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,
diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeListDialog.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeListDialog.kt
index eb865dfca..c5310591a 100644
--- a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeListDialog.kt
+++ b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeListDialog.kt
@@ -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
diff --git a/app/src/main/java/io/legado/app/ui/theme/AppContentColor.kt b/app/src/main/java/io/legado/app/ui/theme/AppContentColor.kt
new file mode 100644
index 000000000..d4a7d28fb
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/theme/AppContentColor.kt
@@ -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
+ )
+}
diff --git a/app/src/main/java/io/legado/app/ui/theme/AppTheme.kt b/app/src/main/java/io/legado/app/ui/theme/AppTheme.kt
index 8ab051faf..1199280b6 100644
--- a/app/src/main/java/io/legado/app/ui/theme/AppTheme.kt
+++ b/app/src/main/java/io/legado/app/ui/theme/AppTheme.kt
@@ -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)
+}
diff --git a/app/src/main/java/io/legado/app/ui/theme/AppThemeMode.kt b/app/src/main/java/io/legado/app/ui/theme/AppThemeMode.kt
index e4904f449..e5e0d4a1d 100644
--- a/app/src/main/java/io/legado/app/ui/theme/AppThemeMode.kt
+++ b/app/src/main/java/io/legado/app/ui/theme/AppThemeMode.kt
@@ -13,7 +13,7 @@ enum class AppThemeMode {
Yuuka,
Phoebe,
Mujika,
- CUSTOM,
+ Custom,
Transparent
}
diff --git a/app/src/main/java/io/legado/app/ui/theme/ImageSeedColorExtractor.kt b/app/src/main/java/io/legado/app/ui/theme/ImageSeedColorExtractor.kt
new file mode 100644
index 000000000..483df3f66
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/theme/ImageSeedColorExtractor.kt
@@ -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(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)
+ )
+}
diff --git a/app/src/main/java/io/legado/app/ui/theme/OpaqueColorScheme.kt b/app/src/main/java/io/legado/app/ui/theme/OpaqueColorScheme.kt
index 429cf7966..883dbc880 100644
--- a/app/src/main/java/io/legado/app/ui/theme/OpaqueColorScheme.kt
+++ b/app/src/main/java/io/legado/app/ui/theme/OpaqueColorScheme.kt
@@ -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
)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/legado/app/ui/theme/ThemeColorSchemeOverride.kt b/app/src/main/java/io/legado/app/ui/theme/ThemeColorSchemeOverride.kt
new file mode 100644
index 000000000..9d67a8e12
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/theme/ThemeColorSchemeOverride.kt
@@ -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(
+ 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 = 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 }
+ )
+}
diff --git a/app/src/main/java/io/legado/app/ui/theme/ThemeEngine.kt b/app/src/main/java/io/legado/app/ui/theme/ThemeEngine.kt
new file mode 100644
index 000000000..3b3d0e00c
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/theme/ThemeEngine.kt
@@ -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 = 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,
+ )
+ }
+}
diff --git a/app/src/main/java/io/legado/app/ui/theme/ThemeManager.kt b/app/src/main/java/io/legado/app/ui/theme/ThemeManager.kt
deleted file mode 100644
index 4e2b0c773..000000000
--- a/app/src/main/java/io/legado/app/ui/theme/ThemeManager.kt
+++ /dev/null
@@ -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 = 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
- }
-}
diff --git a/app/src/main/java/io/legado/app/ui/theme/ThemeOverride.kt b/app/src/main/java/io/legado/app/ui/theme/ThemeOverride.kt
new file mode 100644
index 000000000..dedfcab4e
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/theme/ThemeOverride.kt
@@ -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(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
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/io/legado/app/ui/theme/ThemeResolver.kt b/app/src/main/java/io/legado/app/ui/theme/ThemeResolver.kt
index 6ed598a0a..268f702d8 100644
--- a/app/src/main/java/io/legado/app/ui/theme/ThemeResolver.kt
+++ b/app/src/main/java/io/legado/app/ui/theme/ThemeResolver.kt
@@ -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
+ }
+ }
}
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/AppFloatingActionButton.kt b/app/src/main/java/io/legado/app/ui/widget/components/AppFloatingActionButton.kt
index 5c72e0a93..a4e9d8440 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/AppFloatingActionButton.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/AppFloatingActionButton.kt
@@ -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
)
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/alert/AppAlertDialog.kt b/app/src/main/java/io/legado/app/ui/widget/components/alert/AppAlertDialog.kt
index 02404ad16..d62311f37 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/alert/AppAlertDialog.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/alert/AppAlertDialog.kt
@@ -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 AppAlertDialog(
content = content?.let { { it(currentData) } }
)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/button/AppButton.kt b/app/src/main/java/io/legado/app/ui/widget/components/button/ConfirmDismissButtonsRow.kt
similarity index 57%
rename from app/src/main/java/io/legado/app/ui/widget/components/button/AppButton.kt
rename to app/src/main/java/io/legado/app/ui/widget/components/button/ConfirmDismissButtonsRow.kt
index 5595c43ff..1fe2dfff3 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/button/AppButton.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/button/ConfirmDismissButtonsRow.kt
@@ -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(
)
}
}
-}
\ No newline at end of file
+}
+
+@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
+ )
+ }
+}
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/button/MediumIconButton.kt b/app/src/main/java/io/legado/app/ui/widget/components/button/MediumIconButton.kt
index 4aa076371..46facd67f 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/button/MediumIconButton.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/button/MediumIconButton.kt
@@ -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
)
}
}
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/button/TopBarButton.kt b/app/src/main/java/io/legado/app/ui/widget/components/button/TopBarButton.kt
index c3e926dcb..805b213ee 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/button/TopBarButton.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/button/TopBarButton.kt
@@ -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
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt b/app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt
index 79dfc772e..73288bb7f 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt
@@ -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(
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt b/app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt
index 747ab58a8..05a39a824 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt
@@ -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,
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/card/TextCard.kt b/app/src/main/java/io/legado/app/ui/widget/components/card/TextCard.kt
index 47886a9e7..0ecb22b16 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/card/TextCard.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/card/TextCard.kt
@@ -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
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/dialog/ColorPickerSheet.kt b/app/src/main/java/io/legado/app/ui/widget/components/dialog/ColorPickerSheet.kt
index 76bae95ae..5e6bc6e08 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/dialog/ColorPickerSheet.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/dialog/ColorPickerSheet.kt
@@ -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')}"
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenu.kt b/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenu.kt
index 1ab23f7b3..e78a8c1fe 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenu.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenu.kt
@@ -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))
+ }
}
}
}
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt
index 437baaf81..60b0f1b3c 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt
@@ -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
+ )
}
}
}
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/modalBottomSheet/AppModalBottomSheet.kt b/app/src/main/java/io/legado/app/ui/widget/components/modalBottomSheet/AppModalBottomSheet.kt
index cc42c9f1e..fefb7b05e 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/modalBottomSheet/AppModalBottomSheet.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/modalBottomSheet/AppModalBottomSheet.kt
@@ -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(
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/InputSettingItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/InputSettingItem.kt
index eefe85ac7..fa03749da 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/InputSettingItem.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/InputSettingItem.kt
@@ -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)
+ )
}
)
}
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt
index 833350d91..f2fb4fce9 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt
@@ -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)
+ )
}
)
}
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/topbar/MiuixScrollBehavior.kt b/app/src/main/java/io/legado/app/ui/widget/components/topbar/MiuixScrollBehavior.kt
index f393d8b8e..52a576005 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/topbar/MiuixScrollBehavior.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/topbar/MiuixScrollBehavior.kt
@@ -33,4 +33,4 @@ class MiuixGlassScrollBehavior(
override val collapsedFraction: Float
get() = miuixBehavior.state.collapsedFraction
-}
\ No newline at end of file
+}
diff --git a/app/src/main/res/layout/dialog_content_edit.xml b/app/src/main/res/layout/dialog_content_edit.xml
index 6900cc49b..4b7163be5 100644
--- a/app/src/main/res/layout/dialog_content_edit.xml
+++ b/app/src/main/res/layout/dialog_content_edit.xml
@@ -33,7 +33,7 @@
沉浸式导航栏
导航栏颜色透明
放入书架
+ 点击删除
继续阅读
封面地址
覆盖
@@ -1237,6 +1238,8 @@
刷新发现
未分组
动态取色
+ 使用基于壁纸的主题颜色
+ 输入数值(%1$d-%2$d)
图标已修改
配置替换规则
更多设置
@@ -1289,6 +1292,8 @@
浅色
深色
纯黑深色模式
+ 自定义主题色
+ 为 Miuix 使用自定义主题配色
透明主题请于设置背景图片后设置
使用种子颜色生成主题颜色
种子色
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index 36151103c..b4674b9c0 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -370,6 +370,7 @@
沉浸式导航栏
导航栏颜色透明
放入書架
+ 點擊刪除
繼續閲讀
封面地址
覆蓋
@@ -1223,4 +1224,8 @@
%d
網頁文件
顯示分組書籍數量
+ 使用基於桌布的主題顏色
+ 輸入數值(%1$d-%2$d)
+ Miuix Monet
+ 為 Miuix 使用 Monet 演算法生成配色。目前主題為動態主題且系統為 Android 12 或以上時,會優先使用系統取色;否則使用下方種子色。
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 44cdbf65f..d7efe30e4 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -372,6 +372,7 @@
沉浸式導航欄
導航欄顏色透明
放入書架
+ 點擊刪除
繼續閱讀
封面地址
覆蓋
@@ -1225,4 +1226,8 @@
%d
網頁文件
顯示分組書籍數量
+ 使用基於桌布的主題顏色
+ 輸入數值(%1$d-%2$d)
+ Miuix Monet
+ 為 Miuix 使用 Monet 演算法產生配色。目前主題為動態主題且系統為 Android 12 以上時,會優先使用系統取色;否則使用下方種子色。
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 762183443..657971090 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -397,6 +397,7 @@
immersion navigation bar
The navigation bar becomes transparent
Add to Bookshelf
+ Tap to remove
Continue reading
Cover path
Cover
@@ -1240,6 +1241,8 @@
Refresh Explore
Ungrouped
Dynamic Colors
+ Use wallpaper-based theme colors
+ Enter value (%1$d-%2$d)
Icon Updated
Configure Replace Rules
More Settings
@@ -1292,6 +1295,8 @@
Light
Dark
True Black Mode
+ Miuix Monet
+ 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.
Please set the transparent theme after setting a background image.
Create a theme from a seed color
Seed Color