From d6f0ca01b6452a32d20bafbcb202cf621d07bc5d Mon Sep 17 00:00:00 2001 From: HapeLee <1321903405@qq.com> Date: Fri, 13 Mar 2026 01:04:53 +0800 Subject: [PATCH] =?UTF-8?q?[=E6=96=B0=E5=A2=9E]=20=E7=94=B1Compose?= =?UTF-8?q?=E9=87=8D=E5=86=99=E7=9A=84=E5=8F=91=E7=8E=B0=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=EF=BC=8C=E5=8F=AF=E8=83=BD=E5=AD=98=E5=9C=A8bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/io/legado/app/di/appModule.kt | 4 + .../app/ui/config/themeConfig/ThemeConfig.kt | 7 + .../app/ui/main/explore/ExploreFragment.kt | 258 +-------- .../app/ui/main/explore/ExploreScreen.kt | 522 ++++++++++++++++++ .../app/ui/main/explore/ExploreViewModel.kt | 137 ++++- .../io/legado/app/ui/main/rss/RssFragment.kt | 90 +-- .../io/legado/app/ui/main/rss/RssScreen.kt | 108 +++- .../app/ui/widget/components/Calender.kt | 299 ---------- .../widget/components/SplicedColumnGroup.kt | 3 +- .../ui/widget/components/card/GlassCard.kt | 18 +- .../components/card/SelectionItemCard.kt | 2 +- .../app/ui/widget/components/card/TextCard.kt | 9 +- .../components/settingItem/SettingItem.kt | 5 +- 13 files changed, 814 insertions(+), 648 deletions(-) create mode 100644 app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt delete mode 100644 app/src/main/java/io/legado/app/ui/widget/components/Calender.kt diff --git a/app/src/main/java/io/legado/app/di/appModule.kt b/app/src/main/java/io/legado/app/di/appModule.kt index 709d7dd61..54846f952 100644 --- a/app/src/main/java/io/legado/app/di/appModule.kt +++ b/app/src/main/java/io/legado/app/di/appModule.kt @@ -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( diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt index e554753d2..b1672b7c3 100644 --- a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt +++ b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt @@ -28,4 +28,11 @@ object ThemeConfig { !bgImageLight.isNullOrEmpty() } } + + /* + fun hasImageBg(isDark: Boolean): Boolean { + return if (isDark) bgImageDark.isNullOrBlank() else bgImageLight.isNullOrBlank() + } + */ + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt index 84b3cfde1..a02a9bc96 100644 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt @@ -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(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() - 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() - 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 { - putExtra("exploreName", title) - putExtra("sourceUrl", sourceUrl) - putExtra("exploreUrl", exploreUrl) - } - } - - override fun editSource(sourceUrl: String) { - startActivity { - 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 { - 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 } } diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt new file mode 100644 index 000000000..13fb62f72 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt @@ -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(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 { + putExtra("sourceUrl", item.bookSourceUrl) + } + }, + onSearch = { + context.startActivity { + putExtra("searchScope", SearchScope(item).toString()) + } + }, + onLogin = { + context.startActivity { + 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 { + 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(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, + maxSpan: Int +): List>> { + val rows = mutableListOf>>() + var currentRow = mutableListOf>() + 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 + ) +} diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt index 41e2e3230..35218b34c 100644 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt @@ -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) { } } -} \ No newline at end of file + data class ExploreUiState( + override val items: List = emptyList(), + override val selectedIds: Set = emptySet(), + override val searchKey: String = "", + override val isSearch: Boolean = false, + override val isLoading: Boolean = false, + val groups: List = emptyList(), + val selectedGroup: String = "", + val expandedId: String? = null, + val exploreKinds: List = emptyList(), + val loadingKinds: Boolean = false + ) : ListUiState + +} diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt index 2169850d0..e9298904c 100644 --- a/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt @@ -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(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() + override val position: Int + get() = arguments?.getInt("position") ?: 0 override fun onCreateView( inflater: LayoutInflater, @@ -44,66 +30,14 @@ class RssFragment() : VMBaseFragment(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() }, - onOpenConfig = { startActivity() }, - onOpenRuleSub = { startActivity() }, - 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 { - putExtra("title", rssSource.sourceName) - putExtra("origin", url) - } - } else { - context?.openUrl(url) - } - } - } else { - startActivity { - putExtra("url", rssSource.sourceUrl) - } - } - } - - private fun edit(rssSource: RssSource) { - startActivity { - putExtra("sourceUrl", rssSource.sourceUrl) - } - } - - private fun login(rssSource: RssSource) { - startActivity { - 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) - } - } - } } diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt index 72bb0a764..f06425d5e 100644 --- a/app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt +++ b/app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt @@ -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(null) } + + val openRss: (RssSource) -> Unit = { rssSource -> + if (rssSource.singleUrl) { + viewModel.getSingleUrl(rssSource) { url -> + if (url.startsWith("http", true)) { + context.startActivity { + putExtra("title", rssSource.sourceName) + putExtra("origin", url) + } + } else { + context.openUrl(url) + } + } + } else { + context.startActivity { + putExtra("url", rssSource.sourceUrl) + } + } + } + + val edit: (RssSource) -> Unit = { rssSource -> + context.startActivity { + putExtra("sourceUrl", rssSource.sourceUrl) + } + } + + val login: (RssSource) -> Unit = { rssSource -> + context.startActivity { + 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() }, + imageVector = Icons.Default.Subscriptions, contentDescription = stringResource(R.string.rule_subscription) ) TopBarActionButton( - onClick = onOpenStar, - imageVector = Icons.Outlined.Star, + onClick = { context.startActivity() }, + imageVector = Icons.Default.Star, contentDescription = stringResource(R.string.favorite) ) }, dropDownMenuContent = { dismiss -> RoundDropdownMenuItem( - onClick = onOpenConfig, - leadingIcon = { MenuItemIcon(Icons.Outlined.Settings) }, + onClick = { + context.startActivity() + 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) diff --git a/app/src/main/java/io/legado/app/ui/widget/components/Calender.kt b/app/src/main/java/io/legado/app/ui/widget/components/Calender.kt deleted file mode 100644 index f5f4371c0..000000000 --- a/app/src/main/java/io/legado/app/ui/widget/components/Calender.kt +++ /dev/null @@ -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 { - val firstDayOfMonth = month.withDayOfMonth(1) - val lengthOfMonth = month.lengthOfMonth() - return (0 until lengthOfMonth).map { firstDayOfMonth.plusDays(it.toLong()) } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/components/SplicedColumnGroup.kt b/app/src/main/java/io/legado/app/ui/widget/components/SplicedColumnGroup.kt index 873db348d..ce775270d 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/SplicedColumnGroup.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/SplicedColumnGroup.kt @@ -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( diff --git a/app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt b/app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt index 01add0890..35b6e43e0 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt @@ -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 ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt b/app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt index 08019d6ea..c6375454c 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt @@ -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( diff --git a/app/src/main/java/io/legado/app/ui/widget/components/card/TextCard.kt b/app/src/main/java/io/legado/app/ui/widget/components/card/TextCard.kt index 90f92e03b..5ddd471da 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/card/TextCard.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/card/TextCard.kt @@ -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, diff --git a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SettingItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SettingItem.kt index c161e19c7..fd98e5f9e 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SettingItem.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SettingItem.kt @@ -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(