[修复] 订阅收藏页图片解析错误问题 #700
This commit is contained in:
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user