[修复] 订阅收藏页图片解析错误问题 #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.items
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.filled.DeleteForever
import androidx.compose.material.icons.filled.DeleteSweep
import androidx.compose.material.icons.filled.Group
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import coil.request.ImageRequest
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.widget.components.ActionItem
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.dialog.TextListInputDialog
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.rules.RuleListScaffold
@@ -57,14 +58,55 @@ fun RssFavoritesScreen(
val groups by viewModel.groups.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(groups) {
if (state.currentGroup.isEmpty() && groups.isNotEmpty()) {
viewModel.onGroupChange(groups.first())
}
var showAddToGroupDialog by remember { mutableStateOf(false) }
var showRemoveFromGroupDialog by remember { mutableStateOf(false) }
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(
title = stringResource(R.string.favorite),
subtitle = state.currentGroup.ifEmpty { stringResource(R.string.all) },
state = state,
onBackClick = onBackClick,
onSearchToggle = viewModel::onSearchToggle,
@@ -74,7 +116,16 @@ fun RssFavoritesScreen(
onSelectInvert = viewModel::selectInvert,
onDeleteSelected = { viewModel.deleteSelected() },
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 = {},
dropDownMenuContent = { dismiss ->
RoundDropdownMenuItem(
@@ -128,7 +179,7 @@ fun RssFavoritesScreen(
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.DeleteForever, null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(12.dp))
Text(stringResource(R.string.delete_all))
Text(stringResource(R.string.all))
}
},
onClick = {
@@ -136,23 +187,6 @@ fun RssFavoritesScreen(
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 ->
if (state.items.isEmpty()) {
@@ -178,38 +212,48 @@ fun RssFavoritesScreen(
val isSelected = state.selectedIds.contains(id)
SelectionItemCard(
title = rssStar.title,
subtitle = rssStar.pubDate,
subtitle = if (rssStar.group.isNotBlank()) {
"${rssStar.group}${rssStar.pubDate ?: ""}"
} else {
rssStar.pubDate
},
isSelected = isSelected,
inSelectionMode = state.selectedIds.isNotEmpty(),
onToggleSelection = {
if (state.selectedIds.isNotEmpty()) {
viewModel.toggleSelection(rssStar)
} else {
viewModel.toggleSelection(rssStar)
},
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> {
putExtra("title", rssStar.title)
putExtra("origin", rssStar.origin)
putExtra("link", rssStar.link)
}
}
},
trailingAction = {
if (!rssStar.image.isNullOrBlank()) {
AsyncImage(
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
)
}
SmallIconButton(
onClick = openAction,
icon = Icons.AutoMirrored.Filled.OpenInNew,
contentDescription = "Open"
)
},
dropdownContent = { dismiss ->
RoundDropdownMenuItem(
text = { Text(stringResource(R.string.change_group)) },
onClick = {
showSetGroupDialog = rssStar
dismiss()
}
)
RoundDropdownMenuItem(
text = { Text(stringResource(R.string.delete)) },
onClick = {
@@ -37,8 +37,6 @@ class RssFavoritesViewModel(application: Application) : BaseViewModel(applicatio
_selectedIds,
_currentGroup.flatMapLatest { group ->
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()
} else {
appDb.rssStarDao.flowByGroup(group)
@@ -73,6 +71,10 @@ class RssFavoritesViewModel(application: Application) : BaseViewModel(applicatio
_selectedIds.value = emptySet()
}
fun setSelection(ids: Set<String>) {
_selectedIds.value = ids
}
fun toggleSelection(rssStar: RssStar) {
val id = "${rssStar.origin}|${rssStar.link}"
_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) {
viewModelScope.launch(IO) {
appDb.rssStarDao.deleteByGroup(group)
@@ -242,7 +242,7 @@ fun RssSourceScreen(
RuleListScaffold(
title = stringResource(R.string.rss_source),
subtitle = uiState.groupFilterName,
subtitle = uiState.groupFilterName ?: stringResource(R.string.all),
state = uiState,
onBackClick = { onBackClick() },
onSearchToggle = { active -> viewModel.setSearchMode(active) },
@@ -31,6 +31,7 @@ fun SourceIcon(
modifier: Modifier = Modifier.size(32.dp),
sourceOrigin: String? = null,
loadOnlyWifi: Boolean = false,
contentScale: ContentScale = ContentScale.Fit,
imageLoader: ImageLoader = koinInject(),
placeholderIcon: @Composable () -> Unit = {
Icon(
@@ -59,7 +60,7 @@ fun SourceIcon(
.build(),
imageLoader = imageLoader,
contentDescription = null,
contentScale = ContentScale.Fit, // 不裁切
contentScale = contentScale, // 不裁切
modifier = Modifier.fillMaxSize()
)
}
@@ -1,15 +1,12 @@
package io.legado.app.ui.widget.components.card
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateDpAsState
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.layout.Arrangement
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.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
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.zIndex
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.ReorderableLazyListState
@@ -59,6 +56,7 @@ fun SelectionItemCard(
inSelectionMode: Boolean = false,
elevation: Dp = 0.dp,
onToggleSelection: () -> Unit = {},
leadingContent: @Composable (() -> Unit)? = null,
onEnabledChange: ((Boolean) -> Unit)? = null,
onClickEdit: (() -> Unit)? = null,
trailingAction: @Composable (RowScope.() -> Unit)? = null,
@@ -87,20 +85,30 @@ fun SelectionItemCard(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp)
.animateContentSize(),
verticalAlignment = Alignment.CenterVertically
) {
AnimatedVisibility(
visible = inSelectionMode,
enter = fadeIn() + expandHorizontally(),
exit = fadeOut() + shrinkHorizontally()
visible = inSelectionMode || leadingContent != null
) {
Checkbox(
checked = isSelected,
onCheckedChange = null,
modifier = Modifier.padding(start = 12.dp)
)
Box(
modifier = Modifier.padding(start = 12.dp),
contentAlignment = Alignment.Center
) {
AnimatedContent(
targetState = inSelectionMode,
label = "LeadingContent"
) { selectionMode ->
if (selectionMode) {
Checkbox(
checked = isSelected,
onCheckedChange = null
)
} else {
leadingContent?.invoke()
}
}
}
}
ListItem(
@@ -159,7 +167,7 @@ fun SelectionItemCard(
icon = Icons.Default.MoreVert,
contentDescription = "More"
)
DropdownMenu(
RoundDropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
@@ -185,6 +193,7 @@ fun LazyItemScope.ReorderableSelectionItem(
inSelectionMode: Boolean = false,
canReorder: Boolean = true,
onToggleSelection: () -> Unit = {},
leadingContent: @Composable (() -> Unit)? = null,
onEnabledChange: ((Boolean) -> Unit)? = null,
onClickEdit: (() -> Unit)? = null,
trailingAction: @Composable (RowScope.() -> Unit)? = null,
@@ -206,6 +215,7 @@ fun LazyItemScope.ReorderableSelectionItem(
inSelectionMode = inSelectionMode,
elevation = elevation,
onToggleSelection = onToggleSelection,
leadingContent = leadingContent,
onEnabledChange = onEnabledChange,
onClickEdit = onClickEdit,
trailingAction = trailingAction,
@@ -227,4 +237,4 @@ fun LazyItemScope.ReorderableSelectionItem(
.animateItem()
)
}
}
}