[新增] 由Compose重写的发现界面,可能存在bug
This commit is contained in:
@@ -29,7 +29,9 @@ import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel
|
||||
import io.legado.app.ui.config.otherConfig.OtherConfigViewModel
|
||||
import io.legado.app.ui.config.readConfig.ReadConfigViewModel
|
||||
import io.legado.app.ui.dict.rule.DictRuleViewModel
|
||||
import io.legado.app.ui.main.explore.ExploreViewModel
|
||||
import io.legado.app.ui.main.my.MyViewModel
|
||||
import io.legado.app.ui.main.rss.RssViewModel
|
||||
import io.legado.app.ui.replace.ReplaceEditRoute
|
||||
import io.legado.app.ui.replace.ReplaceRuleViewModel
|
||||
import io.legado.app.ui.replace.edit.ReplaceEditViewModel
|
||||
@@ -82,6 +84,8 @@ val appModule = module {
|
||||
viewModelOf(::TocViewModel)
|
||||
viewModelOf(::RemoteBookViewModel)
|
||||
viewModelOf(::BookInfoViewModel)
|
||||
viewModelOf(::ExploreViewModel)
|
||||
viewModelOf(::RssViewModel)
|
||||
|
||||
viewModel { (route: ReplaceEditRoute) ->
|
||||
ReplaceEditViewModel(
|
||||
|
||||
@@ -28,4 +28,11 @@ object ThemeConfig {
|
||||
!bgImageLight.isNullOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
fun hasImageBg(isDark: Boolean): Boolean {
|
||||
return if (isDark) bgImageDark.isNullOrBlank() else bgImageLight.isNullOrBlank()
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
@@ -1,256 +1,48 @@
|
||||
package io.legado.app.ui.main.explore
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.SubMenu
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.transition.TransitionManager
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.VMBaseFragment
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.data.AppDatabase
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.BookSourcePart
|
||||
import io.legado.app.databinding.FragmentExploreBinding
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.ui.book.explore.ExploreShowActivity
|
||||
import io.legado.app.ui.book.search.SearchActivity
|
||||
import io.legado.app.ui.book.search.SearchScope
|
||||
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.legado.app.ui.main.MainFragmentInterface
|
||||
import io.legado.app.utils.flowWithLifecycleAndDatabaseChange
|
||||
import io.legado.app.utils.startActivity
|
||||
import io.legado.app.utils.transaction
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
import io.legado.app.ui.theme.AppTheme
|
||||
|
||||
/**
|
||||
* 发现界面
|
||||
*/
|
||||
class ExploreFragment() : VMBaseFragment<ExploreViewModel>(R.layout.fragment_explore),
|
||||
MainFragmentInterface,
|
||||
ExploreAdapter.CallBack {
|
||||
class ExploreFragment() : Fragment(), MainFragmentInterface {
|
||||
|
||||
constructor(position: Int) : this() {
|
||||
val bundle = Bundle()
|
||||
bundle.putInt("position", position)
|
||||
arguments = bundle
|
||||
arguments = Bundle().apply {
|
||||
putInt("position", position)
|
||||
}
|
||||
}
|
||||
|
||||
override val position: Int? get() = arguments?.getInt("position")
|
||||
override val position: Int
|
||||
get() = arguments?.getInt("position") ?: 0
|
||||
|
||||
override val viewModel by viewModels<ExploreViewModel>()
|
||||
private val binding by viewBinding(FragmentExploreBinding::bind)
|
||||
private val adapter by lazy {
|
||||
ExploreAdapter(requireContext(), this)
|
||||
}
|
||||
private val linearLayoutManager by lazy { LinearLayoutManager(context) }
|
||||
private val searchView: SearchView by lazy { binding.searchBar }
|
||||
private val diffItemCallBack = ExploreDiffItemCallBack()
|
||||
private val groups = linkedSetOf<String>()
|
||||
private var exploreFlowJob: Job? = null
|
||||
private var groupsMenu: SubMenu? = null
|
||||
|
||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
||||
setSupportToolbar(binding.topBar)
|
||||
initSearchView()
|
||||
initRecyclerView()
|
||||
initGroupData()
|
||||
upExploreData()
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S)
|
||||
binding.appBar.fitsSystemWindows = true
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
searchView.clearFocus()
|
||||
}
|
||||
|
||||
private fun initSearchView() {
|
||||
searchView.queryHint = getString(R.string.screen_find)
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
upExploreData(query)
|
||||
searchView.clearFocus()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String): Boolean {
|
||||
upExploreData(newText)
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private fun initRecyclerView() {
|
||||
//binding.rvFind.setEdgeEffectColor(primaryColor)
|
||||
binding.rvFind.layoutManager = linearLayoutManager
|
||||
binding.rvFind.adapter = adapter
|
||||
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
super.onItemRangeInserted(positionStart, itemCount)
|
||||
if (positionStart == 0) {
|
||||
binding.rvFind.scrollToPosition(0)
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setViewCompositionStrategy(
|
||||
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
|
||||
)
|
||||
setContent {
|
||||
AppTheme {
|
||||
ExploreScreen()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun initGroupData() {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
appDb.bookSourceDao.flowExploreGroups()
|
||||
.flowWithLifecycleAndDatabaseChange(
|
||||
viewLifecycleOwner.lifecycle,
|
||||
Lifecycle.State.RESUMED,
|
||||
AppDatabase.BOOK_SOURCE_TABLE_NAME
|
||||
)
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
groups.clear()
|
||||
groups.addAll(it)
|
||||
upGroupsMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun upExploreData(searchKey: String? = null) {
|
||||
exploreFlowJob?.cancel()
|
||||
exploreFlowJob = viewLifecycleOwner.lifecycleScope.launch {
|
||||
val query = searchKey ?: searchView.query?.toString() ?: ""
|
||||
|
||||
when {
|
||||
query.isBlank() -> {
|
||||
appDb.bookSourceDao.flowExplore()
|
||||
}
|
||||
query.startsWith("group:") -> {
|
||||
val key = query.substringAfter("group:")
|
||||
appDb.bookSourceDao.flowGroupExplore(key)
|
||||
}
|
||||
else -> {
|
||||
appDb.bookSourceDao.flowExplore(query)
|
||||
}
|
||||
}.flowWithLifecycleAndDatabaseChange(
|
||||
viewLifecycleOwner.lifecycle,
|
||||
Lifecycle.State.RESUMED,
|
||||
AppDatabase.BOOK_SOURCE_TABLE_NAME
|
||||
).catch {
|
||||
AppLog.put("发现界面更新数据出错", it)
|
||||
}.conflate().flowOn(IO).collect {
|
||||
binding.emptyView.isGone = it.isNotEmpty() || query.isNotEmpty()
|
||||
adapter.setItems(it, diffItemCallBack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun upGroupsMenu() = groupsMenu?.transaction { subMenu ->
|
||||
subMenu.removeGroup(R.id.menu_group_text)
|
||||
subMenu.add(R.id.menu_group_text, Menu.NONE, Menu.NONE, getString(R.string.all))
|
||||
groups.forEach {
|
||||
subMenu.add(R.id.menu_group_text, Menu.NONE, Menu.NONE, it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override val scope: CoroutineScope
|
||||
get() = viewLifecycleOwner.lifecycleScope
|
||||
|
||||
override fun onCompatCreateOptionsMenu(menu: Menu) {
|
||||
menuInflater.inflate(R.menu.main_explore, menu)
|
||||
groupsMenu = menu.findItem(R.id.menu_group)?.subMenu
|
||||
upGroupsMenu()
|
||||
}
|
||||
|
||||
override fun onCompatOptionsItemSelected(item: MenuItem) {
|
||||
super.onCompatOptionsItemSelected(item)
|
||||
if (item.groupId == R.id.menu_group_text) {
|
||||
val title = item.title.toString()
|
||||
if (title == getString(R.string.all)) {
|
||||
searchView.setQuery("", false)
|
||||
upExploreData("")
|
||||
} else {
|
||||
searchView.setQuery(title, false)
|
||||
}
|
||||
} else if (item.itemId == R.id.menu_exp_search) {
|
||||
TransitionManager.beginDelayedTransition(binding.rootView)
|
||||
if (searchView.isVisible) {
|
||||
searchView.isVisible = false
|
||||
searchView.clearFocus()
|
||||
} else {
|
||||
searchView.isVisible = true
|
||||
searchView.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun scrollTo(pos: Int) {
|
||||
(binding.rvFind.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(pos, 0)
|
||||
}
|
||||
|
||||
override fun openExplore(sourceUrl: String, title: String, exploreUrl: String?) {
|
||||
if (exploreUrl.isNullOrBlank()) return
|
||||
startActivity<ExploreShowActivity> {
|
||||
putExtra("exploreName", title)
|
||||
putExtra("sourceUrl", sourceUrl)
|
||||
putExtra("exploreUrl", exploreUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun editSource(sourceUrl: String) {
|
||||
startActivity<BookSourceEditActivity> {
|
||||
putExtra("sourceUrl", sourceUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toTop(source: BookSourcePart) {
|
||||
viewModel.topSource(source)
|
||||
}
|
||||
|
||||
override fun deleteSource(source: BookSourcePart) {
|
||||
alert(R.string.draw) {
|
||||
setMessage(getString(R.string.sure_del) + "\n" + source.bookSourceName)
|
||||
noButton()
|
||||
yesButton {
|
||||
viewModel.deleteSource(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchBook(bookSource: BookSourcePart) {
|
||||
startActivity<SearchActivity> {
|
||||
putExtra("searchScope", SearchScope(bookSource).toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun compressExplore() {
|
||||
if (!adapter.compressExplore()) {
|
||||
if (AppConfig.isEInkMode) {
|
||||
binding.rvFind.scrollToPosition(0)
|
||||
} else {
|
||||
binding.rvFind.smoothScrollToPosition(0)
|
||||
}
|
||||
}
|
||||
// Compose version handled by ViewModel state
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
package io.legado.app.ui.main.explore
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Login
|
||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Group
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.VerticalAlignTop
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.data.entities.BookSourcePart
|
||||
import io.legado.app.data.entities.rule.ExploreKind
|
||||
import io.legado.app.ui.book.explore.ExploreShowActivity
|
||||
import io.legado.app.ui.book.search.SearchActivity
|
||||
import io.legado.app.ui.book.search.SearchScope
|
||||
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
||||
import io.legado.app.ui.login.SourceLoginActivity
|
||||
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.divider.PillHeaderDivider
|
||||
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
|
||||
import io.legado.app.ui.widget.components.list.ListScaffold
|
||||
import io.legado.app.ui.widget.components.menuItem.MenuItemIcon
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||
import io.legado.app.utils.startActivity
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ExploreScreen(
|
||||
viewModel: ExploreViewModel = koinViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
var sourceToDelete by remember { mutableStateOf<BookSourcePart?>(null) }
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// 自动滚动置顶
|
||||
LaunchedEffect(uiState.expandedId) {
|
||||
uiState.expandedId?.let { id ->
|
||||
var realIndex = 0
|
||||
for (item in uiState.items) {
|
||||
if (item.bookSourceUrl == id) break
|
||||
realIndex++
|
||||
}
|
||||
if (realIndex >= 0) {
|
||||
listState.animateScrollToItem(realIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val stickyHeaderSource by remember {
|
||||
derivedStateOf {
|
||||
val expandedId = uiState.expandedId ?: return@derivedStateOf null
|
||||
val expandedSource =
|
||||
uiState.items.find { it.bookSourceUrl == expandedId } ?: return@derivedStateOf null
|
||||
|
||||
var headerIndex = 0
|
||||
var contentRowCount = 0
|
||||
for (item in uiState.items) {
|
||||
if (item.bookSourceUrl == expandedId) {
|
||||
contentRowCount = calculateRows(uiState.exploreKinds, 6).size
|
||||
break
|
||||
}
|
||||
headerIndex++
|
||||
}
|
||||
|
||||
val lastContentIndex = headerIndex + contentRowCount
|
||||
val firstVisible = listState.firstVisibleItemIndex
|
||||
|
||||
if (firstVisible in (headerIndex + 1)..lastContentIndex) {
|
||||
expandedSource
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListScaffold(
|
||||
title = stringResource(R.string.screen_find),
|
||||
state = uiState,
|
||||
subtitle = uiState.selectedGroup.ifEmpty { stringResource(R.string.all) },
|
||||
onSearchQueryChange = { viewModel.search(it) },
|
||||
onSearchToggle = { viewModel.toggleSearchVisible(it) },
|
||||
searchPlaceholder = stringResource(R.string.search),
|
||||
dropDownMenuContent = { dismiss ->
|
||||
RoundDropdownMenuItem(
|
||||
leadingIcon = { MenuItemIcon(Icons.Default.Group) },
|
||||
text = { Text(stringResource(R.string.all)) },
|
||||
onClick = { viewModel.setGroup(""); dismiss() }
|
||||
)
|
||||
uiState.groups.forEach { group ->
|
||||
RoundDropdownMenuItem(
|
||||
leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Outlined.Label) },
|
||||
text = { Text(group) },
|
||||
onClick = { viewModel.setGroup(group); dismiss() }
|
||||
)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
FastScrollLazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = paddingValues.calculateBottomPadding() + 16.dp
|
||||
)
|
||||
) {
|
||||
uiState.items.forEach { item ->
|
||||
val isExpanded = uiState.expandedId == item.bookSourceUrl
|
||||
|
||||
item(key = item.bookSourceUrl) {
|
||||
ExploreSourceHeader(
|
||||
modifier = Modifier.animateItem(),
|
||||
item = item,
|
||||
isExpanded = isExpanded,
|
||||
loadingKinds = if (isExpanded) uiState.loadingKinds else false,
|
||||
onClick = { viewModel.toggleExpand(item) },
|
||||
onTop = { viewModel.topSource(item) },
|
||||
onEdit = {
|
||||
context.startActivity<BookSourceEditActivity> {
|
||||
putExtra("sourceUrl", item.bookSourceUrl)
|
||||
}
|
||||
},
|
||||
onSearch = {
|
||||
context.startActivity<SearchActivity> {
|
||||
putExtra("searchScope", SearchScope(item).toString())
|
||||
}
|
||||
},
|
||||
onLogin = {
|
||||
context.startActivity<SourceLoginActivity> {
|
||||
putExtra("type", "bookSource")
|
||||
putExtra("key", item.bookSourceUrl)
|
||||
}
|
||||
},
|
||||
onRefresh = { viewModel.refreshExploreKinds(item) },
|
||||
onDelete = { sourceToDelete = item }
|
||||
)
|
||||
}
|
||||
|
||||
if (isExpanded) {
|
||||
val rows = calculateRows(uiState.exploreKinds, 6)
|
||||
itemsIndexed(
|
||||
items = rows,
|
||||
key = { index, _ -> "${item.bookSourceUrl}_$index" }
|
||||
) { _, rowItems ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem()
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
rowItems.forEach { (kind, span) ->
|
||||
val isClickable = !kind.url.isNullOrBlank()
|
||||
ExploreKindItem(
|
||||
kind = kind,
|
||||
isClickable = isClickable,
|
||||
modifier = Modifier.weight(span.toFloat()),
|
||||
onClick = {
|
||||
if (isClickable) {
|
||||
context.startActivity<ExploreShowActivity> {
|
||||
putExtra("exploreName", kind.title)
|
||||
putExtra("sourceUrl", item.bookSourceUrl)
|
||||
putExtra("exploreUrl", kind.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val totalSpan = rowItems.sumOf { it.second }
|
||||
if (totalSpan < 6) {
|
||||
Spacer(
|
||||
modifier = Modifier.weight((6 - totalSpan).toFloat())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = stickyHeaderSource != null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(top = paddingValues.calculateTopPadding() + 4.dp, start = 8.dp),
|
||||
enter = fadeIn() + slideInVertically { -it },
|
||||
exit = fadeOut() + slideOutVertically { -it }
|
||||
) {
|
||||
var lastSource by remember { mutableStateOf<BookSourcePart?>(null) }
|
||||
stickyHeaderSource?.let { lastSource = it }
|
||||
|
||||
lastSource?.let { item ->
|
||||
ExploreStickyCard(
|
||||
item = item,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val index =
|
||||
uiState.items.indexOfFirst { it.bookSourceUrl == item.bookSourceUrl }
|
||||
if (index >= 0) listState.animateScrollToItem(index)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceToDelete?.let { source ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { sourceToDelete = null },
|
||||
title = { Text(stringResource(R.string.draw)) },
|
||||
text = { Text(stringResource(R.string.sure_del) + "\n" + source.bookSourceName) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
viewModel.deleteSource(source)
|
||||
sourceToDelete = null
|
||||
}) {
|
||||
Text(stringResource(R.string.yes))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { sourceToDelete = null }) {
|
||||
Text(stringResource(R.string.no))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateRows(
|
||||
kinds: List<ExploreKind>,
|
||||
maxSpan: Int
|
||||
): List<List<Pair<ExploreKind, Int>>> {
|
||||
val rows = mutableListOf<MutableList<Pair<ExploreKind, Int>>>()
|
||||
var currentRow = mutableListOf<Pair<ExploreKind, Int>>()
|
||||
var currentSpan = 0
|
||||
kinds.forEach { kind ->
|
||||
val style = kind.style()
|
||||
val span = when {
|
||||
style.layout_wrapBefore || style.layout_flexBasisPercent >= 1.0f -> maxSpan
|
||||
style.layout_flexBasisPercent > 0 -> (maxSpan * style.layout_flexBasisPercent).roundToInt()
|
||||
.coerceIn(1, maxSpan)
|
||||
|
||||
style.layout_flexGrow > 0f -> 3
|
||||
else -> 2
|
||||
}
|
||||
if ((style.layout_wrapBefore && currentRow.isNotEmpty()) || (currentSpan + span > maxSpan)) {
|
||||
rows.add(currentRow)
|
||||
currentRow = mutableListOf()
|
||||
currentSpan = 0
|
||||
}
|
||||
currentRow.add(kind to span)
|
||||
currentSpan += span
|
||||
if (currentSpan >= maxSpan) {
|
||||
rows.add(currentRow)
|
||||
currentRow = mutableListOf()
|
||||
currentSpan = 0
|
||||
}
|
||||
}
|
||||
if (currentRow.isNotEmpty()) rows.add(currentRow)
|
||||
return rows
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ExploreSourceHeader(
|
||||
modifier: Modifier = Modifier,
|
||||
item: BookSourcePart,
|
||||
isExpanded: Boolean,
|
||||
loadingKinds: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onTop: () -> Unit,
|
||||
onEdit: () -> Unit,
|
||||
onSearch: () -> Unit,
|
||||
onLogin: () -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
val rotation by animateFloatAsState(if (isExpanded) 90f else 0f, label = "rotation")
|
||||
val containerColor by animateColorAsState(
|
||||
targetValue = if (isExpanded)
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing),
|
||||
label = "CardColor"
|
||||
)
|
||||
|
||||
GlassCard(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = CardDefaults.cardColors(containerColor = containerColor),
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = { showMenu = true }
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = item.bookSourceName,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = if (isExpanded) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
if (loadingKinds) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(18.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.rotate(rotation)
|
||||
.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
RoundDropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
|
||||
PillHeaderDivider(title = item.bookSourceName)
|
||||
RoundDropdownMenuItem(
|
||||
leadingIcon = { MenuItemIcon(Icons.Default.VerticalAlignTop) },
|
||||
text = { Text(stringResource(R.string.to_top)) },
|
||||
onClick = { onTop(); showMenu = false }
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
leadingIcon = { MenuItemIcon(Icons.Default.Edit) },
|
||||
text = { Text(stringResource(R.string.edit)) },
|
||||
onClick = { onEdit(); showMenu = false }
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
leadingIcon = { MenuItemIcon(Icons.Default.Search) },
|
||||
text = { Text(stringResource(R.string.search)) },
|
||||
onClick = { onSearch(); showMenu = false }
|
||||
)
|
||||
if (item.hasLoginUrl) {
|
||||
RoundDropdownMenuItem(
|
||||
leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Filled.Login) },
|
||||
text = { Text(stringResource(R.string.login)) },
|
||||
onClick = { onLogin(); showMenu = false }
|
||||
)
|
||||
}
|
||||
RoundDropdownMenuItem(
|
||||
leadingIcon = { MenuItemIcon(Icons.Default.Refresh) },
|
||||
text = { Text(stringResource(R.string.refresh)) },
|
||||
onClick = { onRefresh(); showMenu = false }
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
leadingIcon = {
|
||||
MenuItemIcon(
|
||||
Icons.Default.Delete,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
stringResource(R.string.delete),
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
},
|
||||
onClick = { onDelete(); showMenu = false }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExploreStickyCard(
|
||||
item: BookSourcePart,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
TextCard(
|
||||
text = item.bookSourceName,
|
||||
backgroundColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
textStyle = MaterialTheme.typography.labelLarge,
|
||||
horizontalPadding = 8.dp,
|
||||
verticalPadding = 6.dp,
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExploreKindItem(
|
||||
kind: ExploreKind,
|
||||
isClickable: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalMinimumInteractiveComponentSize provides Dp.Unspecified
|
||||
) {
|
||||
|
||||
val shape = MaterialTheme.shapes.medium
|
||||
|
||||
if (isClickable) {
|
||||
GlassCard(
|
||||
onClick = onClick,
|
||||
shape = shape,
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.6f),
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
),
|
||||
modifier = modifier
|
||||
) {
|
||||
KindText(kind)
|
||||
}
|
||||
} else {
|
||||
OutlinedCard(
|
||||
shape = shape,
|
||||
colors = CardDefaults.outlinedCardColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
),
|
||||
modifier = modifier
|
||||
) {
|
||||
KindText(kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
private fun KindText(
|
||||
kind: ExploreKind
|
||||
) {
|
||||
Text(
|
||||
text = kind.title,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
style = MaterialTheme.typography.labelMediumEmphasized,
|
||||
textAlign = TextAlign.Center,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,135 @@
|
||||
package io.legado.app.ui.main.explore
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.BookSourcePart
|
||||
import io.legado.app.data.entities.rule.ExploreKind
|
||||
import io.legado.app.help.source.SourceHelp
|
||||
import io.legado.app.help.source.clearExploreKindsCache
|
||||
import io.legado.app.help.source.exploreKinds
|
||||
import io.legado.app.ui.widget.components.rules.ListUiState
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ExploreViewModel(application: Application) : BaseViewModel(application) {
|
||||
|
||||
private val _uiState = MutableStateFlow(ExploreUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
private var exploreJob: Job? = null
|
||||
private var kindsJob: Job? = null
|
||||
|
||||
init {
|
||||
observeGroups()
|
||||
observeExplore()
|
||||
}
|
||||
|
||||
private fun observeGroups() {
|
||||
viewModelScope.launch {
|
||||
appDb.bookSourceDao.flowExploreGroups().collectLatest { groups ->
|
||||
_uiState.update { it.copy(groups = groups) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun search(key: String) {
|
||||
_uiState.update { it.copy(searchKey = key, expandedId = null) }
|
||||
observeExplore()
|
||||
}
|
||||
|
||||
fun setGroup(group: String) {
|
||||
_uiState.update { it.copy(selectedGroup = group, expandedId = null) }
|
||||
observeExplore()
|
||||
}
|
||||
|
||||
fun toggleSearchVisible(visible: Boolean) {
|
||||
_uiState.update { it.copy(isSearch = visible) }
|
||||
if (!visible) {
|
||||
search("")
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeExplore() {
|
||||
exploreJob?.cancel()
|
||||
exploreJob = viewModelScope.launch {
|
||||
val state = _uiState.value
|
||||
val query = state.searchKey
|
||||
val selectedGroup = state.selectedGroup
|
||||
|
||||
val flow = when {
|
||||
query.isNotBlank() -> {
|
||||
if (query.startsWith("group:")) {
|
||||
val key = query.substringAfter("group:")
|
||||
appDb.bookSourceDao.flowGroupExplore(key)
|
||||
} else {
|
||||
appDb.bookSourceDao.flowExplore(query)
|
||||
}
|
||||
}
|
||||
|
||||
selectedGroup.isNotBlank() -> {
|
||||
appDb.bookSourceDao.flowGroupExplore(selectedGroup)
|
||||
}
|
||||
|
||||
else -> {
|
||||
appDb.bookSourceDao.flowExplore()
|
||||
}
|
||||
}
|
||||
|
||||
flow.flowOn(IO).collectLatest { items ->
|
||||
_uiState.update { it.copy(items = items) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleExpand(source: BookSourcePart) {
|
||||
val newExpandedId =
|
||||
if (_uiState.value.expandedId == source.bookSourceUrl) null else source.bookSourceUrl
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
expandedId = newExpandedId,
|
||||
exploreKinds = emptyList(),
|
||||
loadingKinds = newExpandedId != null
|
||||
)
|
||||
}
|
||||
|
||||
if (newExpandedId != null) {
|
||||
loadExploreKinds(source)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadExploreKinds(source: BookSourcePart) {
|
||||
kindsJob?.cancel()
|
||||
kindsJob = viewModelScope.launch(IO) {
|
||||
try {
|
||||
val kinds = source.exploreKinds()
|
||||
_uiState.update {
|
||||
if (it.expandedId == source.bookSourceUrl) {
|
||||
it.copy(exploreKinds = kinds, loadingKinds = false)
|
||||
} else it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_uiState.update { it.copy(loadingKinds = false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshExploreKinds(source: BookSourcePart) {
|
||||
viewModelScope.launch(IO) {
|
||||
source.clearExploreKindsCache()
|
||||
if (_uiState.value.expandedId == source.bookSourceUrl) {
|
||||
loadExploreKinds(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun topSource(bookSource: BookSourcePart) {
|
||||
execute {
|
||||
val minXh = appDb.bookSourceDao.minOrder
|
||||
@@ -22,4 +144,17 @@ class ExploreViewModel(application: Application) : BaseViewModel(application) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
data class ExploreUiState(
|
||||
override val items: List<BookSourcePart> = emptyList(),
|
||||
override val selectedIds: Set<String> = emptySet(),
|
||||
override val searchKey: String = "",
|
||||
override val isSearch: Boolean = false,
|
||||
override val isLoading: Boolean = false,
|
||||
val groups: List<String> = emptyList(),
|
||||
val selectedGroup: String = "",
|
||||
val expandedId: String? = null,
|
||||
val exploreKinds: List<ExploreKind> = emptyList(),
|
||||
val loadingKinds: Boolean = false
|
||||
) : ListUiState<BookSourcePart>
|
||||
|
||||
}
|
||||
|
||||
@@ -5,38 +5,24 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.fragment.app.viewModels
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.VMBaseFragment
|
||||
import io.legado.app.data.entities.RssSource
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.ui.login.SourceLoginActivity
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.legado.app.ui.main.MainFragmentInterface
|
||||
import io.legado.app.ui.rss.article.RssSortActivity
|
||||
import io.legado.app.ui.rss.favorites.RssFavoritesActivity
|
||||
import io.legado.app.ui.rss.read.ReadRssActivity
|
||||
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
|
||||
import io.legado.app.ui.rss.source.manage.RssSourceActivity
|
||||
import io.legado.app.ui.rss.subscription.RuleSubActivity
|
||||
import io.legado.app.ui.theme.AppTheme
|
||||
import io.legado.app.utils.openUrl
|
||||
import io.legado.app.utils.startActivity
|
||||
|
||||
/**
|
||||
* 订阅界面
|
||||
*/
|
||||
class RssFragment() : VMBaseFragment<RssViewModel>(R.layout.fragment_rss),
|
||||
MainFragmentInterface {
|
||||
class RssFragment() : Fragment(), MainFragmentInterface {
|
||||
|
||||
constructor(position: Int) : this() {
|
||||
val bundle = Bundle()
|
||||
bundle.putInt("position", position)
|
||||
arguments = bundle
|
||||
arguments = Bundle().apply {
|
||||
putInt("position", position)
|
||||
}
|
||||
}
|
||||
|
||||
override val position: Int? get() = arguments?.getInt("position")
|
||||
|
||||
override val viewModel by viewModels<RssViewModel>()
|
||||
override val position: Int
|
||||
get() = arguments?.getInt("position") ?: 0
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -44,66 +30,14 @@ class RssFragment() : VMBaseFragment<RssViewModel>(R.layout.fragment_rss),
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setViewCompositionStrategy(
|
||||
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
|
||||
)
|
||||
setContent {
|
||||
AppTheme {
|
||||
RssScreen(
|
||||
viewModel = viewModel,
|
||||
onOpenRss = { openRss(it) },
|
||||
onEdit = { edit(it) },
|
||||
onOpenStar = { startActivity<RssFavoritesActivity>() },
|
||||
onOpenConfig = { startActivity<RssSourceActivity>() },
|
||||
onOpenRuleSub = { startActivity<RuleSubActivity>() },
|
||||
onDelete = { del(it) },
|
||||
onLogin = { login(it) }
|
||||
)
|
||||
RssScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
||||
// Compose handles UI
|
||||
}
|
||||
|
||||
private fun openRss(rssSource: RssSource) {
|
||||
if (rssSource.singleUrl) {
|
||||
viewModel.getSingleUrl(rssSource) { url ->
|
||||
if (url.startsWith("http", true)) {
|
||||
startActivity<ReadRssActivity> {
|
||||
putExtra("title", rssSource.sourceName)
|
||||
putExtra("origin", url)
|
||||
}
|
||||
} else {
|
||||
context?.openUrl(url)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startActivity<RssSortActivity> {
|
||||
putExtra("url", rssSource.sourceUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun edit(rssSource: RssSource) {
|
||||
startActivity<RssSourceEditActivity> {
|
||||
putExtra("sourceUrl", rssSource.sourceUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private fun login(rssSource: RssSource) {
|
||||
startActivity<SourceLoginActivity> {
|
||||
putExtra("type", "rssSource")
|
||||
putExtra("key", rssSource.sourceUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private fun del(rssSource: RssSource) {
|
||||
alert(R.string.draw) {
|
||||
setMessage(getString(R.string.sure_del) + "\n" + rssSource.sourceName)
|
||||
noButton()
|
||||
yesButton {
|
||||
viewModel.del(rssSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,15 @@ import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Group
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material.icons.filled.Subscriptions
|
||||
import androidx.compose.material.icons.filled.VerticalAlignTop
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.Star
|
||||
import androidx.compose.material.icons.outlined.Subscriptions
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -39,12 +41,20 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.data.entities.RssSource
|
||||
import io.legado.app.ui.login.SourceLoginActivity
|
||||
import io.legado.app.ui.rss.article.RssSortActivity
|
||||
import io.legado.app.ui.rss.favorites.RssFavoritesActivity
|
||||
import io.legado.app.ui.rss.read.ReadRssActivity
|
||||
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
|
||||
import io.legado.app.ui.rss.source.manage.RssSourceActivity
|
||||
import io.legado.app.ui.rss.subscription.RuleSubActivity
|
||||
import io.legado.app.ui.widget.components.SourceIcon
|
||||
import io.legado.app.ui.widget.components.button.TopBarActionButton
|
||||
import io.legado.app.ui.widget.components.divider.PillDivider
|
||||
@@ -53,20 +63,50 @@ import io.legado.app.ui.widget.components.list.ListScaffold
|
||||
import io.legado.app.ui.widget.components.menuItem.MenuItemIcon
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||
import io.legado.app.utils.openUrl
|
||||
import io.legado.app.utils.startActivity
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RssScreen(
|
||||
viewModel: RssViewModel,
|
||||
onOpenRss: (RssSource) -> Unit,
|
||||
onEdit: (RssSource) -> Unit,
|
||||
onOpenStar: () -> Unit,
|
||||
onOpenConfig: () -> Unit,
|
||||
onOpenRuleSub: () -> Unit,
|
||||
onDelete: (RssSource) -> Unit,
|
||||
onLogin: (RssSource) -> Unit
|
||||
viewModel: RssViewModel = koinViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
var sourceToDelete by remember { mutableStateOf<RssSource?>(null) }
|
||||
|
||||
val openRss: (RssSource) -> Unit = { rssSource ->
|
||||
if (rssSource.singleUrl) {
|
||||
viewModel.getSingleUrl(rssSource) { url ->
|
||||
if (url.startsWith("http", true)) {
|
||||
context.startActivity<ReadRssActivity> {
|
||||
putExtra("title", rssSource.sourceName)
|
||||
putExtra("origin", url)
|
||||
}
|
||||
} else {
|
||||
context.openUrl(url)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
context.startActivity<RssSortActivity> {
|
||||
putExtra("url", rssSource.sourceUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val edit: (RssSource) -> Unit = { rssSource ->
|
||||
context.startActivity<RssSourceEditActivity> {
|
||||
putExtra("sourceUrl", rssSource.sourceUrl)
|
||||
}
|
||||
}
|
||||
|
||||
val login: (RssSource) -> Unit = { rssSource ->
|
||||
context.startActivity<SourceLoginActivity> {
|
||||
putExtra("type", "rssSource")
|
||||
putExtra("key", rssSource.sourceUrl)
|
||||
}
|
||||
}
|
||||
|
||||
ListScaffold(
|
||||
title = stringResource(R.string.rss),
|
||||
@@ -78,20 +118,23 @@ fun RssScreen(
|
||||
searchPlaceholder = stringResource(R.string.search_rss_source),
|
||||
topBarActions = {
|
||||
TopBarActionButton(
|
||||
onClick = onOpenRuleSub,
|
||||
imageVector = Icons.Outlined.Subscriptions,
|
||||
onClick = { context.startActivity<RuleSubActivity>() },
|
||||
imageVector = Icons.Default.Subscriptions,
|
||||
contentDescription = stringResource(R.string.rule_subscription)
|
||||
)
|
||||
TopBarActionButton(
|
||||
onClick = onOpenStar,
|
||||
imageVector = Icons.Outlined.Star,
|
||||
onClick = { context.startActivity<RssFavoritesActivity>() },
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = stringResource(R.string.favorite)
|
||||
)
|
||||
},
|
||||
dropDownMenuContent = { dismiss ->
|
||||
RoundDropdownMenuItem(
|
||||
onClick = onOpenConfig,
|
||||
leadingIcon = { MenuItemIcon(Icons.Outlined.Settings) },
|
||||
onClick = {
|
||||
context.startActivity<RssSourceActivity>()
|
||||
dismiss()
|
||||
},
|
||||
leadingIcon = { MenuItemIcon(Icons.Default.Settings) },
|
||||
text = { Text("订阅源管理") }
|
||||
)
|
||||
PillDivider()
|
||||
@@ -131,16 +174,39 @@ fun RssScreen(
|
||||
RssSourceGridItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
source = source,
|
||||
onClick = { onOpenRss(source) },
|
||||
onClick = { openRss(source) },
|
||||
onTop = { viewModel.topSource(source) },
|
||||
onEdit = { onEdit(source) },
|
||||
onDelete = { onDelete(source) },
|
||||
onEdit = { edit(source) },
|
||||
onDelete = { sourceToDelete = source },
|
||||
onDisable = { viewModel.disable(source) },
|
||||
onLogin = { onLogin(source) }
|
||||
onLogin = { login(source) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceToDelete?.let { source ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { sourceToDelete = null },
|
||||
title = { Text(stringResource(R.string.draw)) },
|
||||
text = { Text(stringResource(R.string.sure_del) + "\n" + source.sourceName) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
viewModel.del(source)
|
||||
sourceToDelete = null
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.yes))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { sourceToDelete = null }) {
|
||||
Text(stringResource(R.string.no))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
|
||||
@@ -1,299 +0,0 @@
|
||||
package io.legado.app.ui.widget.components
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
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.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.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.TextStyle
|
||||
import java.util.Locale
|
||||
|
||||
const val START_PAGE_OFFSET = 100
|
||||
|
||||
/**
|
||||
* @param modifier 外部修饰符
|
||||
* @param initialDate 初始显示的月份日期
|
||||
* @param onDateSelected 日期被选中时的回调
|
||||
*/
|
||||
@Composable
|
||||
fun Calendar(
|
||||
modifier: Modifier = Modifier,
|
||||
initialDate: LocalDate = LocalDate.now(),
|
||||
onDateSelected: (LocalDate) -> Unit,
|
||||
selectedDate: LocalDate? = null,
|
||||
onClearDate: (() -> Unit)? = null
|
||||
) {
|
||||
val baseMonth = remember(selectedDate) {
|
||||
selectedDate?.withDayOfMonth(1) ?: initialDate.withDayOfMonth(1)
|
||||
}
|
||||
|
||||
val pagerState = rememberPagerState(initialPage = START_PAGE_OFFSET) {
|
||||
START_PAGE_OFFSET * 2
|
||||
}
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val currentMonth by remember {
|
||||
derivedStateOf {
|
||||
val pageOffset = pagerState.currentPage - START_PAGE_OFFSET
|
||||
baseMonth.plusMonths(pageOffset.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
GlassCard(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = MaterialTheme.colorScheme.surfaceContainer
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 12.dp)
|
||||
.animateContentSize()
|
||||
) {
|
||||
MonthNavigation(
|
||||
currentMonth = currentMonth,
|
||||
selectedDate = selectedDate,
|
||||
onPreviousClick = {
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(pagerState.currentPage - 1)
|
||||
}
|
||||
},
|
||||
onNextClick = {
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(pagerState.currentPage + 1)
|
||||
}
|
||||
},
|
||||
onClearClick = onClearDate
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
DayOfWeekHeader()
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 320.dp),
|
||||
verticalAlignment = Alignment.Top
|
||||
) { page ->
|
||||
val pageOffset = page - START_PAGE_OFFSET
|
||||
val monthForPage = baseMonth.plusMonths(pageOffset.toLong())
|
||||
|
||||
MonthPageContent(
|
||||
month = monthForPage,
|
||||
selectedDate = selectedDate,
|
||||
onDateClick = onDateSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MonthPageContent(
|
||||
month: LocalDate,
|
||||
selectedDate: LocalDate?,
|
||||
onDateClick: (LocalDate) -> Unit
|
||||
) {
|
||||
val datesInMonth = getDatesInMonth(month)
|
||||
val firstDayOfWeek = month.dayOfWeek.value % 7
|
||||
val offset = firstDayOfWeek
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(7),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
userScrollEnabled = false,
|
||||
horizontalArrangement = Arrangement.SpaceAround
|
||||
) {
|
||||
items(offset) {
|
||||
Spacer(modifier = Modifier.size(36.dp))
|
||||
}
|
||||
|
||||
items(datesInMonth) { date ->
|
||||
DateItem(
|
||||
date = date,
|
||||
isSelected = date == selectedDate,
|
||||
isToday = date == LocalDate.now(),
|
||||
isCurrentMonth = date.month == month.month,
|
||||
onClick = { onDateClick(date) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MonthNavigation(
|
||||
currentMonth: LocalDate,
|
||||
selectedDate: LocalDate?,
|
||||
onPreviousClick: () -> Unit,
|
||||
onNextClick: () -> Unit,
|
||||
onClearClick: (() -> Unit)?
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = currentMonth.year.toString(),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
AnimatedTextLine(
|
||||
text = currentMonth.format(DateTimeFormatter.ofPattern("MMMM", Locale.getDefault())),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
|
||||
if (selectedDate != null && onClearClick != null) {
|
||||
FilledTonalIconButton(onClick = onClearClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Clear Date Selection",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = onPreviousClick) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Default.ArrowBack,
|
||||
contentDescription = "Previous Month",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onNextClick) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Default.ArrowForward,
|
||||
contentDescription = "Next Month",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DayOfWeekHeader() {
|
||||
val dayNames = listOf(
|
||||
DayOfWeek.SUNDAY, DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY,
|
||||
DayOfWeek.THURSDAY, DayOfWeek.FRIDAY, DayOfWeek.SATURDAY
|
||||
).map {
|
||||
it.getDisplayName(TextStyle.SHORT, Locale.getDefault())
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceAround
|
||||
) {
|
||||
dayNames.forEach { dayName ->
|
||||
Text(
|
||||
text = dayName,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.width(36.dp),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun DateItem(
|
||||
date: LocalDate,
|
||||
isSelected: Boolean,
|
||||
isToday: Boolean,
|
||||
isCurrentMonth: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val buttonColors = when {
|
||||
isSelected -> ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
isCurrentMonth -> ButtonDefaults.textButtonColors(
|
||||
contentColor = if (isToday) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
else -> ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
|
||||
val borderStroke = if (isToday && !isSelected) {
|
||||
BorderStroke(1.dp, MaterialTheme.colorScheme.primary)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.padding(4.dp),
|
||||
shape = CircleShape,
|
||||
colors = buttonColors,
|
||||
border = borderStroke,
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
elevation = ButtonDefaults.buttonElevation(defaultElevation = 0.dp)
|
||||
) {
|
||||
Text(
|
||||
text = date.dayOfMonth.toString(),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Medium),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getDatesInMonth(month: LocalDate): List<LocalDate> {
|
||||
val firstDayOfMonth = month.withDayOfMonth(1)
|
||||
val lengthOfMonth = month.lengthOfMonth()
|
||||
return (0 until lengthOfMonth).map { firstDayOfMonth.plusDays(it.toLong()) }
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -38,7 +39,7 @@ fun SplicedColumnGroup(
|
||||
|
||||
GlassCard(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
|
||||
@@ -3,12 +3,12 @@ package io.legado.app.ui.widget.components.card
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardColors
|
||||
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
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||
|
||||
@@ -17,22 +17,20 @@ fun GlassCard(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (() -> Unit)? = null,
|
||||
shape: Shape = CardDefaults.shape,
|
||||
color: Color = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
colors: CardColors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = ThemeConfig.containerOpacity / 100f),
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
),
|
||||
elevation: CardElevation = CardDefaults.cardElevation(),
|
||||
border: BorderStroke? = null,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
val opacity = ThemeConfig.containerOpacity
|
||||
val cardColors = CardDefaults.cardColors(
|
||||
containerColor = color.copy(alpha = opacity / 100f)
|
||||
)
|
||||
|
||||
if (onClick != null) {
|
||||
Card(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
shape = shape,
|
||||
colors = cardColors,
|
||||
colors = colors,
|
||||
elevation = elevation,
|
||||
border = border,
|
||||
content = content
|
||||
@@ -41,10 +39,10 @@ fun GlassCard(
|
||||
Card(
|
||||
modifier = modifier,
|
||||
shape = shape,
|
||||
colors = cardColors,
|
||||
colors = colors,
|
||||
elevation = elevation,
|
||||
border = border,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ fun SelectionItemCard(
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = containerColor,
|
||||
colors = CardDefaults.cardColors(containerColor = containerColor),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = elevation)
|
||||
) {
|
||||
Row(
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
@@ -19,7 +20,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.ui.widget.components.AnimatedText
|
||||
import io.legado.app.ui.widget.components.AnimatedTextLine
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
@@ -36,6 +37,7 @@ fun TextCard(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
icon: ImageVector? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
backgroundColor: Color = colorScheme.primaryContainer,
|
||||
contentColor: Color = colorScheme.onPrimaryContainer,
|
||||
cornerRadius: Dp = 8.dp,
|
||||
@@ -49,7 +51,8 @@ fun TextCard(
|
||||
GlassCard(
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(cornerRadius),
|
||||
color = backgroundColor
|
||||
colors = CardDefaults.cardColors(containerColor = backgroundColor),
|
||||
onClick = onClick
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(
|
||||
@@ -69,7 +72,7 @@ fun TextCard(
|
||||
Spacer(modifier = Modifier.width(spacing))
|
||||
}
|
||||
|
||||
AnimatedText(
|
||||
AnimatedTextLine(
|
||||
text = text,
|
||||
style = textStyle,
|
||||
color = contentColor,
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
@@ -62,7 +63,9 @@ fun SettingItem(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
shape = shape,
|
||||
color = color ?: MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = color ?: MaterialTheme.colorScheme.surfaceContainerLow
|
||||
),
|
||||
) {
|
||||
Column {
|
||||
ListItem(
|
||||
|
||||
Reference in New Issue
Block a user