[修复] 订阅收藏页图片解析错误问题 #700

This commit is contained in:
HapeLee
2026-03-11 00:20:14 +08:00
parent 901ff29efd
commit ca067578a1
6 changed files with 166 additions and 157 deletions
@@ -1,86 +0,0 @@
package io.legado.app.ui.rss.favorites
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.R
import io.legado.app.base.VMBaseFragment
import io.legado.app.constant.AppLog
import io.legado.app.data.appDb
import io.legado.app.data.entities.RssStar
import io.legado.app.databinding.FragmentRssArticlesBinding
import io.legado.app.lib.dialogs.alert
//import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.rss.read.ReadRssActivity
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyNavigationBarPadding
import io.legado.app.utils.startActivity
import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
class RssFavoritesFragment() : VMBaseFragment<RssFavoritesViewModel>(R.layout.fragment_rss_articles),
RssFavoritesAdapter.CallBack {
constructor(group: String) : this() {
arguments = Bundle().apply {
putString("group", group)
}
}
private val binding by viewBinding(FragmentRssArticlesBinding::bind)
override val viewModel by viewModels<RssFavoritesViewModel>()
private val adapter: RssFavoritesAdapter by lazy {
RssFavoritesAdapter(requireContext(), this@RssFavoritesFragment)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
initView()
loadArticles()
}
private fun initView() = binding.run {
refreshLayout.isEnabled = false
// recyclerView.setEdgeEffectColor(primaryColor)
recyclerView.layoutManager = run {
recyclerView.addItemDecoration(VerticalDivider(requireContext()))
LinearLayoutManager(requireContext())
}
recyclerView.adapter = adapter
recyclerView.applyNavigationBarPadding()
}
private fun loadArticles() {
lifecycleScope.launch {
val group = arguments?.getString("group") ?: "默认分组"
appDb.rssStarDao.flowByGroup(group).catch {
AppLog.put("订阅文章界面获取数据失败\n${it.localizedMessage}", it)
}.flowOn(IO).collect {
adapter.setItems(it)
}
}
}
override fun readRss(rssStar: RssStar) {
startActivity<ReadRssActivity> {
putExtra("title", rssStar.title)
putExtra("origin", rssStar.origin)
putExtra("link", rssStar.link)
}
}
override fun delStar(rssStar: RssStar) {
alert(R.string.draw) {
setMessage(getString(R.string.sure_del) + "\n<" + rssStar.title + ">")
noButton()
yesButton {
appDb.rssStarDao.delete(rssStar.origin, rssStar.link)
}
}
}
}
@@ -11,36 +11,37 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material.icons.automirrored.outlined.Label import androidx.compose.material.icons.automirrored.outlined.Label
import androidx.compose.material.icons.filled.DeleteForever import androidx.compose.material.icons.filled.DeleteForever
import androidx.compose.material.icons.filled.DeleteSweep import androidx.compose.material.icons.filled.DeleteSweep
import androidx.compose.material.icons.filled.Group import androidx.compose.material.icons.filled.Group
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Tab
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import coil.request.ImageRequest
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.entities.RssStar
import io.legado.app.ui.rss.read.ReadRssActivity import io.legado.app.ui.rss.read.ReadRssActivity
import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.EmptyMessageView import io.legado.app.ui.widget.components.EmptyMessageView
import io.legado.app.ui.widget.components.SourceIcon
import io.legado.app.ui.widget.components.button.SmallIconButton
import io.legado.app.ui.widget.components.card.SelectionItemCard import io.legado.app.ui.widget.components.card.SelectionItemCard
import io.legado.app.ui.widget.components.dialog.TextListInputDialog
import io.legado.app.ui.widget.components.divider.PillDivider import io.legado.app.ui.widget.components.divider.PillDivider
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.rules.RuleListScaffold import io.legado.app.ui.widget.components.rules.RuleListScaffold
@@ -57,14 +58,55 @@ fun RssFavoritesScreen(
val groups by viewModel.groups.collectAsState() val groups by viewModel.groups.collectAsState()
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(groups) { var showAddToGroupDialog by remember { mutableStateOf(false) }
if (state.currentGroup.isEmpty() && groups.isNotEmpty()) { var showRemoveFromGroupDialog by remember { mutableStateOf(false) }
viewModel.onGroupChange(groups.first()) var showSetGroupDialog by remember { mutableStateOf<RssStar?>(null) }
}
if (showAddToGroupDialog) {
TextListInputDialog(
title = stringResource(R.string.add_group),
hint = stringResource(R.string.group_name),
suggestions = groups,
onDismissRequest = { showAddToGroupDialog = false },
onConfirm = {
viewModel.selectionAddToGroups(state.selectedIds, it)
showAddToGroupDialog = false
viewModel.clearSelection()
}
)
}
if (showRemoveFromGroupDialog) {
TextListInputDialog(
title = stringResource(R.string.remove_group),
hint = stringResource(R.string.group_name),
suggestions = groups,
onDismissRequest = { showRemoveFromGroupDialog = false },
onConfirm = {
viewModel.selectionRemoveFromGroups(state.selectedIds, it)
showRemoveFromGroupDialog = false
viewModel.clearSelection()
}
)
}
showSetGroupDialog?.let { rssStar ->
TextListInputDialog(
title = stringResource(R.string.change_group),
hint = stringResource(R.string.group_name),
initialValue = rssStar.group,
suggestions = groups,
onDismissRequest = { showSetGroupDialog = null },
onConfirm = {
viewModel.updateGroup(rssStar, it)
showSetGroupDialog = null
}
)
} }
RuleListScaffold( RuleListScaffold(
title = stringResource(R.string.favorite), title = stringResource(R.string.favorite),
subtitle = state.currentGroup.ifEmpty { stringResource(R.string.all) },
state = state, state = state,
onBackClick = onBackClick, onBackClick = onBackClick,
onSearchToggle = viewModel::onSearchToggle, onSearchToggle = viewModel::onSearchToggle,
@@ -74,7 +116,16 @@ fun RssFavoritesScreen(
onSelectInvert = viewModel::selectInvert, onSelectInvert = viewModel::selectInvert,
onDeleteSelected = { viewModel.deleteSelected() }, onDeleteSelected = { viewModel.deleteSelected() },
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
selectionSecondaryActions = emptyList(), selectionSecondaryActions = listOf(
ActionItem(
text = stringResource(R.string.add_group),
onClick = { showAddToGroupDialog = true }
),
ActionItem(
text = stringResource(R.string.remove_group),
onClick = { showRemoveFromGroupDialog = true }
)
),
topBarActions = {}, topBarActions = {},
dropDownMenuContent = { dismiss -> dropDownMenuContent = { dismiss ->
RoundDropdownMenuItem( RoundDropdownMenuItem(
@@ -128,7 +179,7 @@ fun RssFavoritesScreen(
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.DeleteForever, null, modifier = Modifier.size(18.dp)) Icon(Icons.Default.DeleteForever, null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(12.dp)) Spacer(Modifier.width(12.dp))
Text(stringResource(R.string.delete_all)) Text(stringResource(R.string.all))
} }
}, },
onClick = { onClick = {
@@ -136,23 +187,6 @@ fun RssFavoritesScreen(
dismiss() dismiss()
} }
) )
},
bottomContent = {
if (groups.size > 1) {
PrimaryScrollableTabRow(
selectedTabIndex = groups.indexOf(state.currentGroup).coerceAtLeast(0),
edgePadding = 16.dp,
divider = {}
) {
groups.forEach { group ->
Tab(
selected = state.currentGroup == group,
onClick = { viewModel.onGroupChange(group) },
text = { Text(group) }
)
}
}
}
} }
) { paddingValues -> ) { paddingValues ->
if (state.items.isEmpty()) { if (state.items.isEmpty()) {
@@ -178,38 +212,48 @@ fun RssFavoritesScreen(
val isSelected = state.selectedIds.contains(id) val isSelected = state.selectedIds.contains(id)
SelectionItemCard( SelectionItemCard(
title = rssStar.title, title = rssStar.title,
subtitle = rssStar.pubDate, subtitle = if (rssStar.group.isNotBlank()) {
"${rssStar.group}${rssStar.pubDate ?: ""}"
} else {
rssStar.pubDate
},
isSelected = isSelected, isSelected = isSelected,
inSelectionMode = state.selectedIds.isNotEmpty(), inSelectionMode = state.selectedIds.isNotEmpty(),
onToggleSelection = { onToggleSelection = {
if (state.selectedIds.isNotEmpty()) { viewModel.toggleSelection(rssStar)
viewModel.toggleSelection(rssStar) },
} else { leadingContent = if (!rssStar.image.isNullOrBlank()) {
{
SourceIcon(
path = rssStar.image,
sourceOrigin = rssStar.origin,
contentScale = ContentScale.Crop,
modifier = Modifier.size(54.dp)
)
}
} else null,
trailingAction = {
val openAction = {
context.startActivity<ReadRssActivity> { context.startActivity<ReadRssActivity> {
putExtra("title", rssStar.title) putExtra("title", rssStar.title)
putExtra("origin", rssStar.origin) putExtra("origin", rssStar.origin)
putExtra("link", rssStar.link) putExtra("link", rssStar.link)
} }
} }
}, SmallIconButton(
trailingAction = { onClick = openAction,
if (!rssStar.image.isNullOrBlank()) { icon = Icons.AutoMirrored.Filled.OpenInNew,
AsyncImage( contentDescription = "Open"
model = ImageRequest.Builder(LocalContext.current) )
.data(rssStar.image)
.setHeader("sourceOrigin", rssStar.origin)
.crossfade(true)
.build(),
contentDescription = null,
modifier = Modifier
.size(width = 80.dp, height = 50.dp)
.padding(start = 8.dp)
.clip(MaterialTheme.shapes.small),
contentScale = ContentScale.Crop
)
}
}, },
dropdownContent = { dismiss -> dropdownContent = { dismiss ->
RoundDropdownMenuItem(
text = { Text(stringResource(R.string.change_group)) },
onClick = {
showSetGroupDialog = rssStar
dismiss()
}
)
RoundDropdownMenuItem( RoundDropdownMenuItem(
text = { Text(stringResource(R.string.delete)) }, text = { Text(stringResource(R.string.delete)) },
onClick = { onClick = {
@@ -37,8 +37,6 @@ class RssFavoritesViewModel(application: Application) : BaseViewModel(applicatio
_selectedIds, _selectedIds,
_currentGroup.flatMapLatest { group -> _currentGroup.flatMapLatest { group ->
if (group.isEmpty()) { if (group.isEmpty()) {
// If group is empty, we might want to wait for the first group from 'groups' flow
// Or just show nothing/default. The Activity/Screen should set the first group.
appDb.rssStarDao.liveAll() appDb.rssStarDao.liveAll()
} else { } else {
appDb.rssStarDao.flowByGroup(group) appDb.rssStarDao.flowByGroup(group)
@@ -73,6 +71,10 @@ class RssFavoritesViewModel(application: Application) : BaseViewModel(applicatio
_selectedIds.value = emptySet() _selectedIds.value = emptySet()
} }
fun setSelection(ids: Set<String>) {
_selectedIds.value = ids
}
fun toggleSelection(rssStar: RssStar) { fun toggleSelection(rssStar: RssStar) {
val id = "${rssStar.origin}|${rssStar.link}" val id = "${rssStar.origin}|${rssStar.link}"
_selectedIds.update { _selectedIds.update {
@@ -108,6 +110,44 @@ class RssFavoritesViewModel(application: Application) : BaseViewModel(applicatio
} }
} }
fun selectionAddToGroups(selectedIds: Set<String>, groupName: String) {
viewModelScope.launch(IO) {
state.value.items.forEach {
val id = "${it.origin}|${it.link}"
if (selectedIds.contains(id)) {
val groups = it.group.split(",").toMutableList()
if (!groups.contains(groupName)) {
groups.add(groupName)
appDb.rssStarDao.update(it.copy(group = groups.filter { g -> g.isNotBlank() }
.joinToString(",")))
}
}
}
}
}
fun selectionRemoveFromGroups(selectedIds: Set<String>, groupName: String) {
viewModelScope.launch(IO) {
state.value.items.forEach {
val id = "${it.origin}|${it.link}"
if (selectedIds.contains(id)) {
val groups = it.group.split(",").toMutableList()
if (groups.contains(groupName)) {
groups.remove(groupName)
appDb.rssStarDao.update(it.copy(group = groups.filter { g -> g.isNotBlank() }
.joinToString(",")))
}
}
}
}
}
fun updateGroup(rssStar: RssStar, group: String) {
viewModelScope.launch(IO) {
appDb.rssStarDao.update(rssStar.copy(group = group))
}
}
fun deleteGroup(group: String) { fun deleteGroup(group: String) {
viewModelScope.launch(IO) { viewModelScope.launch(IO) {
appDb.rssStarDao.deleteByGroup(group) appDb.rssStarDao.deleteByGroup(group)
@@ -242,7 +242,7 @@ fun RssSourceScreen(
RuleListScaffold( RuleListScaffold(
title = stringResource(R.string.rss_source), title = stringResource(R.string.rss_source),
subtitle = uiState.groupFilterName, subtitle = uiState.groupFilterName ?: stringResource(R.string.all),
state = uiState, state = uiState,
onBackClick = { onBackClick() }, onBackClick = { onBackClick() },
onSearchToggle = { active -> viewModel.setSearchMode(active) }, onSearchToggle = { active -> viewModel.setSearchMode(active) },
@@ -31,6 +31,7 @@ fun SourceIcon(
modifier: Modifier = Modifier.size(32.dp), modifier: Modifier = Modifier.size(32.dp),
sourceOrigin: String? = null, sourceOrigin: String? = null,
loadOnlyWifi: Boolean = false, loadOnlyWifi: Boolean = false,
contentScale: ContentScale = ContentScale.Fit,
imageLoader: ImageLoader = koinInject(), imageLoader: ImageLoader = koinInject(),
placeholderIcon: @Composable () -> Unit = { placeholderIcon: @Composable () -> Unit = {
Icon( Icon(
@@ -59,7 +60,7 @@ fun SourceIcon(
.build(), .build(),
imageLoader = imageLoader, imageLoader = imageLoader,
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Fit, // 不裁切 contentScale = contentScale, // 不裁切
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
} }
@@ -1,15 +1,12 @@
package io.legado.app.ui.widget.components.card package io.legado.app.ui.widget.components.card
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -24,7 +21,6 @@ import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -46,6 +42,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.SmallIconButton
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.ReorderableLazyListState import sh.calvin.reorderable.ReorderableLazyListState
@@ -59,6 +56,7 @@ fun SelectionItemCard(
inSelectionMode: Boolean = false, inSelectionMode: Boolean = false,
elevation: Dp = 0.dp, elevation: Dp = 0.dp,
onToggleSelection: () -> Unit = {}, onToggleSelection: () -> Unit = {},
leadingContent: @Composable (() -> Unit)? = null,
onEnabledChange: ((Boolean) -> Unit)? = null, onEnabledChange: ((Boolean) -> Unit)? = null,
onClickEdit: (() -> Unit)? = null, onClickEdit: (() -> Unit)? = null,
trailingAction: @Composable (RowScope.() -> Unit)? = null, trailingAction: @Composable (RowScope.() -> Unit)? = null,
@@ -87,20 +85,30 @@ fun SelectionItemCard(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 4.dp)
.animateContentSize(), .animateContentSize(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = inSelectionMode, visible = inSelectionMode || leadingContent != null
enter = fadeIn() + expandHorizontally(),
exit = fadeOut() + shrinkHorizontally()
) { ) {
Checkbox( Box(
checked = isSelected, modifier = Modifier.padding(start = 12.dp),
onCheckedChange = null, contentAlignment = Alignment.Center
modifier = Modifier.padding(start = 12.dp) ) {
) AnimatedContent(
targetState = inSelectionMode,
label = "LeadingContent"
) { selectionMode ->
if (selectionMode) {
Checkbox(
checked = isSelected,
onCheckedChange = null
)
} else {
leadingContent?.invoke()
}
}
}
} }
ListItem( ListItem(
@@ -159,7 +167,7 @@ fun SelectionItemCard(
icon = Icons.Default.MoreVert, icon = Icons.Default.MoreVert,
contentDescription = "More" contentDescription = "More"
) )
DropdownMenu( RoundDropdownMenu(
expanded = showMenu, expanded = showMenu,
onDismissRequest = { showMenu = false } onDismissRequest = { showMenu = false }
) { ) {
@@ -185,6 +193,7 @@ fun LazyItemScope.ReorderableSelectionItem(
inSelectionMode: Boolean = false, inSelectionMode: Boolean = false,
canReorder: Boolean = true, canReorder: Boolean = true,
onToggleSelection: () -> Unit = {}, onToggleSelection: () -> Unit = {},
leadingContent: @Composable (() -> Unit)? = null,
onEnabledChange: ((Boolean) -> Unit)? = null, onEnabledChange: ((Boolean) -> Unit)? = null,
onClickEdit: (() -> Unit)? = null, onClickEdit: (() -> Unit)? = null,
trailingAction: @Composable (RowScope.() -> Unit)? = null, trailingAction: @Composable (RowScope.() -> Unit)? = null,
@@ -206,6 +215,7 @@ fun LazyItemScope.ReorderableSelectionItem(
inSelectionMode = inSelectionMode, inSelectionMode = inSelectionMode,
elevation = elevation, elevation = elevation,
onToggleSelection = onToggleSelection, onToggleSelection = onToggleSelection,
leadingContent = leadingContent,
onEnabledChange = onEnabledChange, onEnabledChange = onEnabledChange,
onClickEdit = onClickEdit, onClickEdit = onClickEdit,
trailingAction = trailingAction, trailingAction = trailingAction,
@@ -227,4 +237,4 @@ fun LazyItemScope.ReorderableSelectionItem(
.animateItem() .animateItem()
) )
} }
} }