[优化] 优化了大部分Compose界面的动画与逻辑(正文选字搜索、部分界面的动画效果等)
This commit is contained in:
@@ -1,246 +0,0 @@
|
||||
package io.legado.app.ui.about
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseActivity
|
||||
import io.legado.app.base.adapter.ItemViewHolder
|
||||
import io.legado.app.base.adapter.RecyclerAdapter
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.ReadRecordShow
|
||||
import io.legado.app.databinding.ActivityReadRecordBinding
|
||||
import io.legado.app.databinding.ItemReadRecordBinding
|
||||
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.model.BookCover
|
||||
//import io.legado.app.lib.theme.primaryTextColor
|
||||
import io.legado.app.ui.book.search.SearchActivity
|
||||
import io.legado.app.utils.applyNavigationBarPadding
|
||||
import io.legado.app.utils.cnCompare
|
||||
import io.legado.app.utils.getInt
|
||||
import io.legado.app.utils.putInt
|
||||
import io.legado.app.utils.startActivityForBook
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class ReadRecordActivity : BaseActivity<ActivityReadRecordBinding>() {
|
||||
|
||||
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<ReadRecordShow, ItemReadRecordBinding>(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<Any>,
|
||||
) {
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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(
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<BookmarkGroupHeader, List<Bookmark>>) : BookmarkUiState()
|
||||
data class Error(val throwable: Throwable) : BookmarkUiState()
|
||||
}
|
||||
|
||||
class AllBookmarkViewModel(
|
||||
@@ -55,24 +58,28 @@ class AllBookmarkViewModel(
|
||||
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||
val bookmarksState: StateFlow<Map<BookmarkGroupHeader, List<Bookmark>>> = _searchQuery
|
||||
val bookmarksState: StateFlow<BookmarkUiState> = _searchQuery
|
||||
.debounce(300L)
|
||||
.flatMapLatest { query ->
|
||||
if (query.isBlank()) {
|
||||
val flow = if (query.isBlank()) {
|
||||
bookmarkDao.flowAll()
|
||||
} else {
|
||||
bookmarkDao.flowSearchAll(query)
|
||||
}
|
||||
flow.map<List<Bookmark>, BookmarkUiState> { list ->
|
||||
BookmarkUiState.Success(list.groupBy { BookmarkGroupHeader(it.bookName, it.bookAuthor) })
|
||||
}
|
||||
.onStart { emit(BookmarkUiState.Loading) }
|
||||
.catch { e ->
|
||||
e.printStackTrace()
|
||||
emit(BookmarkUiState.Error(e))
|
||||
}
|
||||
}
|
||||
.map { list ->
|
||||
list.groupBy { BookmarkGroupHeader(it.bookName, it.bookAuthor) }
|
||||
}
|
||||
.catch { e -> e.printStackTrace() }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(5000),
|
||||
emptyMap()
|
||||
BookmarkUiState.Loading
|
||||
)
|
||||
|
||||
fun toggleGroupCollapse(groupKey: BookmarkGroupHeader) {
|
||||
|
||||
@@ -1318,7 +1318,7 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
val book = ReadBook.book ?: return
|
||||
searchContentActivity.launch {
|
||||
putExtra("bookUrl", book.bookUrl)
|
||||
putExtra("searchWord", searchWord ?: viewModel.searchContentQuery)
|
||||
putExtra("searchWord", searchWord)
|
||||
putExtra("searchResultIndex", viewModel.searchResultIndex)
|
||||
viewModel.searchResultList?.first()?.let {
|
||||
if (it.query == viewModel.searchContentQuery) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package io.legado.app.ui.book.readRecord
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -15,6 +14,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -182,128 +182,134 @@ fun ReadRecordScreen(
|
||||
}
|
||||
) { padding ->
|
||||
Column(modifier = Modifier.padding(padding).fillMaxSize()) {
|
||||
val isEmpty = when (displayMode) {
|
||||
DisplayMode.AGGREGATE -> state.groupedRecords.isEmpty()
|
||||
DisplayMode.TIMELINE -> state.timelineRecords.isEmpty()
|
||||
DisplayMode.LATEST -> state.latestRecords.isEmpty()
|
||||
val contentState = when {
|
||||
state.isLoading -> "LOADING"
|
||||
(displayMode == DisplayMode.AGGREGATE && state.groupedRecords.isEmpty()) ||
|
||||
(displayMode == DisplayMode.TIMELINE && state.timelineRecords.isEmpty()) ||
|
||||
(displayMode == DisplayMode.LATEST && state.latestRecords.isEmpty()) -> "EMPTY"
|
||||
else -> "CONTENT"
|
||||
}
|
||||
Crossfade(
|
||||
targetState = isEmpty,
|
||||
animationSpec = tween(durationMillis = 500),
|
||||
label = "ContentCrossfade"
|
||||
) { isListEmpty ->
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
) {
|
||||
|
||||
item {
|
||||
val selectedDate = state.selectedDate
|
||||
|
||||
if (selectedDate != null) {
|
||||
val dateKey = selectedDate.format(DateTimeFormatter.ISO_LOCAL_DATE)
|
||||
val dailyDetails = state.groupedRecords[dateKey] ?: emptyList()
|
||||
|
||||
if (dailyDetails.isNotEmpty()) {
|
||||
val distinctBooks = dailyDetails.map { it.bookName }.distinct()
|
||||
val dailyTime = dailyDetails.sumOf { it.readTime }
|
||||
|
||||
ReadingSummaryCard(
|
||||
title = selectedDate.format(DateTimeFormatter.ofPattern("M月d日阅读概览")),
|
||||
bookCount = distinctBooks.size,
|
||||
totalTimeMillis = dailyTime,
|
||||
bookNamesForCover = distinctBooks.take(3),
|
||||
viewModel = viewModel,
|
||||
onClick = { }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val allBooksCount = state.latestRecords.size
|
||||
val totalTime = state.totalReadTime
|
||||
|
||||
if (allBooksCount > 0) {
|
||||
ReadingSummaryCard(
|
||||
title = "累计阅读成就",
|
||||
bookCount = allBooksCount,
|
||||
totalTimeMillis = totalTime,
|
||||
bookNamesForCover = state.latestRecords.take(5).map { it.bookName },
|
||||
viewModel = viewModel,
|
||||
onClick = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedContent(
|
||||
targetState = contentState,
|
||||
label = "MainContentAnimation"
|
||||
) { targetState ->
|
||||
when (targetState) {
|
||||
"LOADING" -> {
|
||||
EmptyMessageView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
message = "加载中"
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
if (isListEmpty){
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
EmptyMessageView(
|
||||
message = "没有记录"
|
||||
)
|
||||
}
|
||||
}
|
||||
"EMPTY" -> {
|
||||
EmptyMessageView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
message = "没有记录"
|
||||
)
|
||||
}
|
||||
|
||||
when(displayMode){
|
||||
DisplayMode.AGGREGATE -> {
|
||||
state.groupedRecords.forEach { (date, details) ->
|
||||
|
||||
val dailyTotalTime = details.sumOf { it.readTime }
|
||||
|
||||
stickyHeader {
|
||||
DateHeader(date, dailyTotalTime)
|
||||
}
|
||||
|
||||
items(
|
||||
items = details,
|
||||
key = { it.bookName + it.readTime.toString() }
|
||||
) { detail ->
|
||||
ReadRecordItem(
|
||||
detail = detail,
|
||||
viewModel = viewModel,
|
||||
onClick = { onBookClick(detail.bookName) },
|
||||
onDelete = { viewModel.deleteDetail(detail) },
|
||||
modifier = Modifier.animateItem()
|
||||
)
|
||||
}
|
||||
"CONTENT" -> {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
) {
|
||||
item(key = "summary_card") {
|
||||
SummarySection(state, viewModel)
|
||||
}
|
||||
}
|
||||
DisplayMode.TIMELINE -> {
|
||||
state.timelineRecords.forEach { (date, sessions) ->
|
||||
stickyHeader { DateHeader(date) }
|
||||
|
||||
val timelineItems = sessions.mapIndexed { index, session ->
|
||||
val showHeader = true
|
||||
TimelineItem(session, showHeader)
|
||||
}
|
||||
|
||||
items(items = timelineItems, key = { it.session.id }) { item ->
|
||||
TimelineSessionItem(
|
||||
item = item,
|
||||
onBookClick = onBookClick,
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplayMode.LATEST -> {
|
||||
items(items = state.latestRecords, key = { it.bookName + it.deviceId }) { record ->
|
||||
LatestReadItem(
|
||||
record = record,
|
||||
viewModel = viewModel,
|
||||
onClick = { onBookClick(record.bookName) },
|
||||
modifier = Modifier.animateItem()
|
||||
)
|
||||
}
|
||||
renderListByMode(displayMode, state, viewModel, onBookClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SummarySection(
|
||||
state: ReadRecordUiState,
|
||||
viewModel: ReadRecordViewModel
|
||||
) {
|
||||
val selectedDate = state.selectedDate
|
||||
|
||||
if (selectedDate != null) {
|
||||
val dateKey = selectedDate.format(DateTimeFormatter.ISO_LOCAL_DATE)
|
||||
val dailyDetails = state.groupedRecords[dateKey] ?: emptyList()
|
||||
|
||||
if (dailyDetails.isNotEmpty()) {
|
||||
val distinctBooks = dailyDetails.map { it.bookName }.distinct()
|
||||
val dailyTime = dailyDetails.sumOf { it.readTime }
|
||||
|
||||
ReadingSummaryCard(
|
||||
title = selectedDate.format(DateTimeFormatter.ofPattern("M月d日阅读概览")),
|
||||
bookCount = distinctBooks.size,
|
||||
totalTimeMillis = dailyTime,
|
||||
bookNamesForCover = distinctBooks.take(3),
|
||||
viewModel = viewModel,
|
||||
onClick = { }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val allBooksCount = state.latestRecords.size
|
||||
val totalTime = state.totalReadTime
|
||||
|
||||
if (allBooksCount > 0) {
|
||||
ReadingSummaryCard(
|
||||
title = "累计阅读成就",
|
||||
bookCount = allBooksCount,
|
||||
totalTimeMillis = totalTime,
|
||||
bookNamesForCover = state.latestRecords.take(5).map { it.bookName },
|
||||
viewModel = viewModel,
|
||||
onClick = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LazyListScope.renderListByMode(
|
||||
displayMode: DisplayMode,
|
||||
state: ReadRecordUiState,
|
||||
viewModel: ReadRecordViewModel,
|
||||
onBookClick: (String) -> Unit
|
||||
) {
|
||||
when (displayMode) {
|
||||
DisplayMode.AGGREGATE -> {
|
||||
state.groupedRecords.forEach { (date, details) ->
|
||||
stickyHeader(key = "header_$date") {
|
||||
DateHeader(date, details.sumOf { it.readTime })
|
||||
}
|
||||
items(items = details, key = { "${it.bookName}_${it.readTime}_$date" }) { detail ->
|
||||
ReadRecordItem(
|
||||
detail = detail,
|
||||
viewModel = viewModel,
|
||||
onClick = { onBookClick(detail.bookName) },
|
||||
onDelete = { viewModel.deleteDetail(detail) },
|
||||
modifier = Modifier.animateItem()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplayMode.TIMELINE -> {
|
||||
state.timelineRecords.forEach { (date, sessions) ->
|
||||
stickyHeader(key = "timeline_header_$date") { DateHeader(date) }
|
||||
items(items = sessions, key = { it.id }) { session ->
|
||||
TimelineSessionItem(
|
||||
item = TimelineItem(session, true),
|
||||
onBookClick = onBookClick,
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplayMode.LATEST -> {
|
||||
items(items = state.latestRecords, key = { it.bookName }) { record ->
|
||||
LatestReadItem(
|
||||
record = record,
|
||||
viewModel = viewModel,
|
||||
onClick = { onBookClick(record.bookName) },
|
||||
modifier = Modifier.animateItem()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,14 @@ import io.legado.app.base.BaseComposeActivity
|
||||
class SearchContentActivity : BaseComposeActivity() {
|
||||
|
||||
private var bookUrl: String? = null
|
||||
private var searchWord: String? = null
|
||||
private var searchResultIndex: Int = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
bookUrl = intent.getStringExtra("bookUrl")
|
||||
searchWord = intent.getStringExtra("searchWord")
|
||||
searchResultIndex = intent.getIntExtra("searchResultIndex", 0)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -20,6 +24,7 @@ class SearchContentActivity : BaseComposeActivity() {
|
||||
bookUrl?.let {
|
||||
SearchContentScreen(
|
||||
bookUrl = it,
|
||||
searchWord = searchWord,
|
||||
onBack = { finish() }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.compose.material.icons.filled.Stop
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
@@ -31,9 +32,15 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MediumTopAppBar
|
||||
import androidx.compose.material3.PlainTooltip
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TooltipAnchorPosition
|
||||
import androidx.compose.material3.TooltipBox
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.material3.rememberTooltipState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -48,17 +55,17 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.ui.widget.components.AnimatedText
|
||||
import io.legado.app.ui.widget.components.AnimatedTextLine
|
||||
import io.legado.app.ui.widget.components.EmptyMessageView
|
||||
import io.legado.app.ui.widget.components.SearchBarSection
|
||||
import io.legado.app.ui.widget.components.TextCard
|
||||
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun SearchContentScreen(
|
||||
bookUrl: String,
|
||||
searchWord: String?,
|
||||
onBack: () -> Unit,
|
||||
viewModel: SearchContentViewModel = koinViewModel()
|
||||
) {
|
||||
@@ -73,8 +80,15 @@ fun SearchContentScreen(
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||
|
||||
LaunchedEffect(bookUrl) {
|
||||
var searchQuery by remember(searchWord) { mutableStateOf(searchWord ?: "") }
|
||||
var replaceEnabled by remember { mutableStateOf(false) }
|
||||
var regexReplace by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(bookUrl, searchWord) {
|
||||
viewModel.initBook(bookUrl)
|
||||
if (!searchWord.isNullOrBlank()) {
|
||||
viewModel.startSearch(searchWord, replaceEnabled, regexReplace)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -98,9 +112,6 @@ fun SearchContentScreen(
|
||||
}
|
||||
}*/
|
||||
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
var replaceEnabled by remember { mutableStateOf(false) }
|
||||
var regexReplace by remember { mutableStateOf(false) }
|
||||
val contentState = when {
|
||||
error != null -> SearchContentState.Error(error)
|
||||
isSearching -> SearchContentState.Loading
|
||||
@@ -150,12 +161,22 @@ fun SearchContentScreen(
|
||||
) {
|
||||
FilterChip(
|
||||
selected = replaceEnabled,
|
||||
onClick = { replaceEnabled = !replaceEnabled },
|
||||
onClick = {
|
||||
replaceEnabled = !replaceEnabled
|
||||
if (searchQuery.isNotBlank()) {
|
||||
viewModel.startSearch(searchQuery, replaceEnabled, regexReplace)
|
||||
}
|
||||
},
|
||||
label = { Text("启用替换") }
|
||||
)
|
||||
FilterChip(
|
||||
selected = regexReplace,
|
||||
onClick = { regexReplace = !regexReplace },
|
||||
onClick = {
|
||||
regexReplace = !regexReplace
|
||||
if (searchQuery.isNotBlank()) {
|
||||
viewModel.startSearch(searchQuery, replaceEnabled, regexReplace)
|
||||
}
|
||||
},
|
||||
label = { Text("正则匹配") }
|
||||
)
|
||||
}
|
||||
@@ -168,8 +189,19 @@ fun SearchContentScreen(
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (isSearching) {
|
||||
FloatingActionButton(onClick = { viewModel.stopSearch() }) {
|
||||
TooltipBox(
|
||||
positionProvider =
|
||||
TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = { PlainTooltip { Text("Localized description") } },
|
||||
state = rememberTooltipState(),
|
||||
) {
|
||||
FloatingActionButton(
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = isSearching,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
onClick = { viewModel.stopSearch() }
|
||||
) {
|
||||
Icon(Icons.Default.Stop, contentDescription = "停止搜索")
|
||||
}
|
||||
}
|
||||
@@ -206,7 +238,7 @@ fun SearchContentScreen(
|
||||
|
||||
SearchContentState.EmptyResult -> {
|
||||
EmptyMessageView(
|
||||
message = "没有找到相关内容",
|
||||
message = "没有找到相关内容!",
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.wrapContentSize()
|
||||
@@ -323,5 +355,3 @@ fun SearchResultItem(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,12 +12,9 @@ import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
@@ -30,10 +27,12 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -332,10 +331,10 @@ fun ReplaceRuleScreen(
|
||||
Column {
|
||||
MediumTopAppBar(
|
||||
title = {
|
||||
val titleText = remember(isUploading, inSelectionMode, selectedRuleIds.size, rules.size) {
|
||||
val titleText = remember(isUploading, inSelectionMode, selectedRuleIds, rules) {
|
||||
when {
|
||||
isUploading -> "正在上传..."
|
||||
inSelectionMode -> "已选择 ${selectedRuleIds.size}/${rules.size}"
|
||||
inSelectionMode -> "已选择 ${rules.count { it.id in selectedRuleIds }}/${rules.size}"
|
||||
else -> "替换规则"
|
||||
}
|
||||
}
|
||||
@@ -434,8 +433,8 @@ fun ReplaceRuleScreen(
|
||||
placeholder = stringResource(id = R.string.replace_purify_search)
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(visible = !inSelectionMode) {
|
||||
val allString = stringResource(R.string.all)
|
||||
val allString = stringResource(R.string.all)
|
||||
AnimatedVisibility(visible = groups.isNotEmpty()) {
|
||||
PrimaryScrollableTabRow(
|
||||
selectedTabIndex = selectedTabIndex,
|
||||
edgePadding = 0.dp,
|
||||
@@ -470,14 +469,21 @@ fun ReplaceRuleScreen(
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(
|
||||
visible = !inSelectionMode,
|
||||
enter = fadeIn() + scaleIn(),
|
||||
exit = fadeOut() + scaleOut()
|
||||
TooltipBox(
|
||||
positionProvider =
|
||||
TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = { PlainTooltip { Text("Localized description") } },
|
||||
state = rememberTooltipState(),
|
||||
) {
|
||||
FloatingActionButton(onClick = {
|
||||
context.startActivity(ReplaceEditActivity.startIntent(context))
|
||||
}) {
|
||||
FloatingActionButton(
|
||||
modifier = Modifier.animateFloatingActionButton(
|
||||
visible = !inSelectionMode,
|
||||
alignment = Alignment.BottomEnd,
|
||||
),
|
||||
onClick = {
|
||||
context.startActivity(ReplaceEditActivity.startIntent(context))
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Add Rule")
|
||||
}
|
||||
}
|
||||
@@ -568,9 +574,8 @@ fun ReplaceRuleScreen(
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = inSelectionMode,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 24.dp),
|
||||
modifier =
|
||||
Modifier.align(Alignment.BottomCenter).offset(y = -ScreenOffset).zIndex(1f),
|
||||
enter = slideInVertically { it } + fadeIn(),
|
||||
exit = slideOutVertically { it } + fadeOut()
|
||||
) {
|
||||
@@ -842,13 +847,9 @@ fun ReplaceRuleItem(
|
||||
)
|
||||
|
||||
Card(
|
||||
onClick = { onToggleSelection() },
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = { onToggleSelection() },
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
),
|
||||
.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = containerColor
|
||||
@@ -900,26 +901,26 @@ fun ReplaceRuleItem(
|
||||
onDismissRequest = { showRuleMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("删除") },
|
||||
onClick = {
|
||||
onDelete()
|
||||
showRuleMenu = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("置顶") },
|
||||
text = { Text("移至顶部") },
|
||||
onClick = {
|
||||
onToTop()
|
||||
showRuleMenu = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("置底") },
|
||||
text = { Text("移至底部") },
|
||||
onClick = {
|
||||
onToBottom()
|
||||
showRuleMenu = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("删除") },
|
||||
onClick = {
|
||||
onDelete()
|
||||
showRuleMenu = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.legado.app.ui.widget.components
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -8,6 +9,8 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.LoadingIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -21,10 +24,12 @@ import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun EmptyMessageView(
|
||||
message: String,
|
||||
modifier: Modifier = Modifier,
|
||||
isLoading: Boolean = false,
|
||||
faces: List<String> = listOf(
|
||||
"(;′⌒`)", "(つ﹏⊂)", "(•̀ᴗ•́)و", "(๑•́ ₃ •̀๑)",
|
||||
"(눈‸눈)", "(ಥ﹏ಥ)", "(。•́︿•̀。)"
|
||||
@@ -40,16 +45,25 @@ fun EmptyMessageView(
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
AnimatedTextLine(
|
||||
text = currentFace,
|
||||
fontSize = faceTextSize,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
currentFace = faces.random()
|
||||
onFaceClick?.invoke()
|
||||
}
|
||||
)
|
||||
AnimatedContent(
|
||||
targetState = isLoading,
|
||||
label = "LoadingStateAnimation"
|
||||
) { loading ->
|
||||
if (loading) {
|
||||
LoadingIndicator()
|
||||
} else {
|
||||
AnimatedTextLine(
|
||||
text = currentFace,
|
||||
fontSize = faceTextSize,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
currentFace = faces.random()
|
||||
onFaceClick?.invoke()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
@@ -67,6 +81,7 @@ fun EmptyMessageView(
|
||||
fun EmptyMessageView(
|
||||
@StringRes messageResId: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
isLoading: Boolean = false,
|
||||
faces: List<String> = listOf(
|
||||
"(;′⌒`)", "(つ﹏⊂)", "(•̀ᴗ•́)و", "(๑•́ ₃ •̀๑)",
|
||||
"(눈‸눈)", "(ಥ﹏ಥ)", "(。•́︿•̀。)"
|
||||
@@ -78,6 +93,7 @@ fun EmptyMessageView(
|
||||
EmptyMessageView(
|
||||
message = message,
|
||||
modifier = modifier,
|
||||
isLoading = isLoading,
|
||||
faces = faces,
|
||||
faceTextSize = faceTextSize,
|
||||
onFaceClick = onFaceClick
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ fun SourceInputDialog(
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
OutlinedButton(
|
||||
onClick = { if (text.isNotBlank()) onConfirm(text) }
|
||||
) { Text(stringResource(android.R.string.ok)) }
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user