From 8362a0fe8b7a0e00f537a1f854376f78ddaa9298 Mon Sep 17 00:00:00 2001
From: HapeLee <1321903405@qq.com>
Date: Fri, 2 Jan 2026 05:49:22 +0800
Subject: [PATCH] =?UTF-8?q?[=E4=BC=98=E5=8C=96]=20=E4=BC=98=E5=8C=96?=
=?UTF-8?q?=E4=BA=86=E5=A4=A7=E9=83=A8=E5=88=86Compose=E7=95=8C=E9=9D=A2?=
=?UTF-8?q?=E7=9A=84=E5=8A=A8=E7=94=BB=E4=B8=8E=E9=80=BB=E8=BE=91=EF=BC=88?=
=?UTF-8?q?=E6=AD=A3=E6=96=87=E9=80=89=E5=AD=97=E6=90=9C=E7=B4=A2=E3=80=81?=
=?UTF-8?q?=E9=83=A8=E5=88=86=E7=95=8C=E9=9D=A2=E7=9A=84=E5=8A=A8=E7=94=BB?=
=?UTF-8?q?=E6=95=88=E6=9E=9C=E7=AD=89=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/src/main/AndroidManifest.xml | 5 -
.../legado/app/ui/about/ReadRecordActivity.kt | 246 ------------------
.../app/ui/book/bookmark/AllBookmarkScreen.kt | 114 +++++---
.../ui/book/bookmark/AllBookmarkViewModel.kt | 29 ++-
.../app/ui/book/read/ReadBookActivity.kt | 2 +-
.../ui/book/readRecord/ReadRecordScreen.kt | 232 +++++++++--------
.../searchContent/SearchContentActivity.kt | 5 +
.../book/searchContent/SearchContentScreen.kt | 56 +++-
.../app/ui/replace/ReplaceRuleScreen.kt | 65 ++---
.../ui/widget/components/EmptyMessageView.kt | 36 ++-
.../importComponents/ImportComponents.kt | 2 +-
11 files changed, 323 insertions(+), 469 deletions(-)
delete mode 100644 app/src/main/java/io/legado/app/ui/about/ReadRecordActivity.kt
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e99c90a27..95dbdc1b0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -339,11 +339,6 @@
android:enableOnBackInvokedCallback="true"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true" />
-
() {
-
- private val adapter by lazy { RecordAdapter(this) }
- private var sortMode
- get() = LocalConfig.getInt("readRecordSort")
- set(value) {
- LocalConfig.putInt("readRecordSort", value)
- }
- private val searchView: SearchView by lazy {
- binding.titleBar.findViewById(R.id.search_view)
- }
-
- override val binding by viewBinding(ActivityReadRecordBinding::inflate)
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- initView()
- initAllTime()
- initData()
- }
-
- override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.book_read_record, menu)
- return super.onCompatCreateOptionsMenu(menu)
- }
-
- override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
- menu.findItem(R.id.menu_enable_record)?.isChecked = AppConfig.enableReadRecord
- when (sortMode) {
- 1 -> menu.findItem(R.id.menu_sort_read_long)?.isChecked = true
- 2 -> menu.findItem(R.id.menu_sort_read_time)?.isChecked = true
- else -> menu.findItem(R.id.menu_sort_name)?.isChecked = true
- }
- return super.onMenuOpened(featureId, menu)
- }
-
- override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- R.id.menu_sort_name -> {
- sortMode = 0
- item.isChecked = true
- initData()
- }
-
- R.id.menu_sort_read_long -> {
- sortMode = 1
- item.isChecked = true
- initData()
- }
-
- R.id.menu_sort_read_time -> {
- sortMode = 2
- item.isChecked = true
- initData()
- }
-
- R.id.menu_enable_record -> {
- AppConfig.enableReadRecord = !item.isChecked
- }
- }
- return super.onCompatOptionsItemSelected(item)
- }
-
- private fun initView() {
- initSearchView()
- binding.tvBookName.setText(R.string.all_read_time)
- binding.tvRemove.setOnClickListener {
- alert(R.string.delete, R.string.sure_del) {
- yesButton {
- appDb.readRecordDao.clear()
- initData()
- }
- noButton()
- }
- }
- binding.recyclerView.adapter = adapter
- binding.recyclerView.applyNavigationBarPadding()
- }
-
- private fun initSearchView() {
- //searchView.applyTint(primaryTextColor)
- searchView.isSubmitButtonEnabled = true
- searchView.queryHint = getString(R.string.search)
- searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
- override fun onQueryTextSubmit(query: String): Boolean {
- searchView.clearFocus()
- return false
- }
-
- override fun onQueryTextChange(newText: String?): Boolean {
- initData(newText)
- return false
- }
- })
- }
-
- private fun initAllTime() {
- lifecycleScope.launch {
- val allTime = withContext(IO) {
- appDb.readRecordDao.getTotalReadTime()
- }
- binding.tvReadingTime.text = formatDuring(allTime)
- }
- }
-
- private fun initData(searchKey: String? = null) {
- lifecycleScope.launch {
- val readRecords = withContext(IO) {
- appDb.readRecordDao.search(searchKey ?: "").let { records ->
- when (sortMode) {
- 1 -> records.sortedByDescending { it.readTime }
- 2 -> records.sortedByDescending { it.lastRead }
- else -> records.sortedWith { o1, o2 ->
- o1.bookName.cnCompare(o2.bookName)
- }
- }
- }
- }
- adapter.setItems(readRecords)
- }
- }
-
- inner class RecordAdapter(context: Context) :
- RecyclerAdapter(context) {
-
- private val dateFormat = SimpleDateFormat("yyyy年MM月dd日", Locale.getDefault())
-
- override fun getViewBinding(parent: ViewGroup): ItemReadRecordBinding {
- return ItemReadRecordBinding.inflate(inflater, parent, false)
- }
-
- override fun convert(
- holder: ItemViewHolder,
- binding: ItemReadRecordBinding,
- item: ReadRecordShow,
- payloads: MutableList,
- ) {
- binding.apply {
- tvBookName.text = item.bookName
- tvReadingTime.text = formatDuring(item.readTime)
- if (item.lastRead > 0) {
- tvLastReadTime.text = dateFormat.format(item.lastRead)
- } else {
- tvLastReadTime.text = "时间未知"
- }
-
- lifecycleScope.launch {
- val book = withContext(IO) {
- appDb.bookDao.findByName(item.bookName).firstOrNull()
- }
- if (book != null) {
- BookCover.load(root.context, book.getDisplayCover(), false, book.origin)
- .into(ivCover)
- }
- }
- }
- }
-
- override fun registerListener(holder: ItemViewHolder, binding: ItemReadRecordBinding) {
- binding.apply {
- root.setOnClickListener {
- val item = getItem(holder.layoutPosition) ?: return@setOnClickListener
- lifecycleScope.launch {
- val book = withContext(IO) {
- appDb.bookDao.findByName(item.bookName).firstOrNull()
- }
- if (book == null) {
- SearchActivity.start(this@ReadRecordActivity, item.bookName)
- } else {
- startActivityForBook(book)
- }
- }
- }
- tvRemove.setOnClickListener {
- getItem(holder.layoutPosition)?.let { item ->
- sureDelAlert(item)
- }
- }
- }
- }
-
- private fun sureDelAlert(item: ReadRecordShow) {
- alert(R.string.delete) {
- setMessage(getString(R.string.sure_del_any, item.bookName))
- yesButton {
- appDb.readRecordDao.deleteByName(item.bookName)
- initData()
- }
- noButton()
- }
- }
-
- }
-
- fun formatDuring(mss: Long): String {
- val days = mss / (1000 * 60 * 60 * 24)
- val hours = mss % (1000 * 60 * 60 * 24) / (1000 * 60 * 60)
- val minutes = mss % (1000 * 60 * 60) / (1000 * 60)
- val seconds = mss % (1000 * 60) / 1000
- val d = if (days > 0) "${days}天" else ""
- val h = if (hours > 0) "${hours}小时" else ""
- val m = if (minutes > 0) "${minutes}分钟" else ""
- val s = if (seconds > 0) "${seconds}秒" else ""
- var time = "$d$h$m$s"
- if (time.isBlank()) {
- time = "0秒"
- }
- return time
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt b/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt
index 2fa6d4bdc..bbc0cdedf 100644
--- a/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt
+++ b/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt
@@ -4,6 +4,7 @@ import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateFloatAsState
@@ -33,12 +34,15 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.legado.app.data.entities.Bookmark
+import io.legado.app.ui.widget.components.EmptyMessageView
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
import io.legado.app.ui.widget.components.lazylist.Scroller
import io.legado.app.ui.widget.components.SearchBarSection
import org.koin.androidx.compose.koinViewModel
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
+ ExperimentalMaterial3ExpressiveApi::class
+)
@Composable
fun AllBookmarkScreen(
viewModel: AllBookmarkViewModel = koinViewModel(),
@@ -46,7 +50,7 @@ fun AllBookmarkScreen(
) {
val context = LocalContext.current
val searchText by viewModel.searchQuery.collectAsState()
- val bookmarksGrouped by viewModel.bookmarksState.collectAsState()
+ val uiState by viewModel.bookmarksState.collectAsState()
val collapsedGroups by viewModel.collapsedGroups.collectAsState()
var showMenu by remember { mutableStateOf(false) }
@@ -56,6 +60,7 @@ fun AllBookmarkScreen(
var pendingExportIsMd by remember { mutableStateOf(false) }
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+ val bookmarksGrouped = (uiState as? BookmarkUiState.Success)?.bookmarks ?: emptyMap()
val allKeys = bookmarksGrouped.keys
val isAllCollapsed = allKeys.isNotEmpty() && allKeys.all { collapsedGroups.contains(it.toString()) }
@@ -137,54 +142,89 @@ fun AllBookmarkScreen(
SearchBarSection(
query = searchText,
onQueryChange = { viewModel.onSearchQueryChanged(it) },
- placeholder = "模糊搜索"
+ placeholder = "搜索..."
)
}
}
}
) { paddingValues ->
- FastScrollLazyColumn(
+ Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
- bookmarksGrouped.forEach { (headerKey, bookmarks) ->
-
- val isCollapsed = collapsedGroups.contains(headerKey.toString())
-
- stickyHeader(key = "${Scroller.STICKY_HEADER_KEY_PREFIX}${headerKey}") {
- BookAuthorHeader(
- bookTitle = headerKey.bookName,
- bookAuthor = headerKey.bookAuthor,
- isCollapsed = isCollapsed,
- onToggle = { viewModel.toggleGroupCollapse(headerKey) }
- )
- }
-
- item(key = "content_${headerKey}") {
- AnimatedVisibility(
- visible = !isCollapsed,
- enter = expandVertically() + fadeIn(),
- exit = shrinkVertically() + fadeOut()
- ) {
- Column(
+ AnimatedContent(
+ targetState = uiState,
+ label = "bookmarkTransition"
+ ) { state ->
+ when (state) {
+ BookmarkUiState.Loading -> {
+ EmptyMessageView(
+ message = "加载中...",
+ isLoading = true,
modifier = Modifier
- .animateContentSize()
- ) {
- bookmarks.forEach { bookmark ->
- BookmarkItem(
- bookmark = bookmark,
- modifier = Modifier
- .animateItem()
- .fillMaxWidth(),
- onClick = {
- editingBookmark = bookmark
- showBottomSheet = true
+ .fillMaxSize()
+ )
+ }
+
+ is BookmarkUiState.Success -> {
+ if (state.bookmarks.isEmpty()) {
+ EmptyMessageView(
+ message = "没有书签!",
+ modifier = Modifier
+ .fillMaxSize()
+ )
+ } else {
+ FastScrollLazyColumn(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ state.bookmarks.forEach { (headerKey, bookmarks) ->
+
+ val isCollapsed = collapsedGroups.contains(headerKey.toString())
+
+ stickyHeader(key = "${Scroller.STICKY_HEADER_KEY_PREFIX}${headerKey}") {
+ BookAuthorHeader(
+ bookTitle = headerKey.bookName,
+ bookAuthor = headerKey.bookAuthor,
+ isCollapsed = isCollapsed,
+ onToggle = { viewModel.toggleGroupCollapse(headerKey) }
+ )
}
- )
+
+ item(key = "content_${headerKey}") {
+ AnimatedVisibility(
+ visible = !isCollapsed,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
+ ) {
+ Column(
+ modifier = Modifier.animateContentSize()
+ ) {
+ bookmarks.forEach { bookmark ->
+ BookmarkItem(
+ bookmark = bookmark,
+ modifier = Modifier
+ .animateItem()
+ .fillMaxWidth(),
+ onClick = {
+ editingBookmark = bookmark
+ showBottomSheet = true
+ }
+ )
+ }
+ }
+ }
+ }
+ }
}
}
}
+
+ is BookmarkUiState.Error -> {
+ EmptyMessageView(
+ message = state.throwable.localizedMessage ?: "发生错误",
+ )
+ }
}
}
}
@@ -407,4 +447,4 @@ fun BookmarkEditSheet(
}
)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkViewModel.kt b/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkViewModel.kt
index 4a4e41eed..2c812f259 100644
--- a/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkViewModel.kt
+++ b/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkViewModel.kt
@@ -15,14 +15,11 @@ import io.legado.app.utils.writeToOutputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.channels.BufferOverflow
-import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
@@ -39,7 +36,13 @@ data class BookmarkGroupHeader(
val bookName: String,
val bookAuthor: String
) {
- override fun toString(): String = "$bookName|${bookAuthor}"
+ override fun toString(): String = "$bookName|$bookAuthor"
+}
+
+sealed class BookmarkUiState {
+ object Loading : BookmarkUiState()
+ data class Success(val bookmarks: Map>) : BookmarkUiState()
+ data class Error(val throwable: Throwable) : BookmarkUiState()
}
class AllBookmarkViewModel(
@@ -55,24 +58,28 @@ class AllBookmarkViewModel(
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
- val bookmarksState: StateFlow