Просмотр исходного кода

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

HapeLee 8 месяцев назад
Родитель
Сommit
ca067578a1

+ 0 - 86
app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesFragment.kt

@@ -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)
-            }
-        }
-    }
-}

+ 95 - 51
app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesScreen.kt

@@ -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 = {

+ 42 - 2
app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesViewModel.kt

@@ -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)

+ 1 - 1
app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceScreen.kt

@@ -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) },

+ 2 - 1
app/src/main/java/io/legado/app/ui/widget/components/SourceIcon.kt

@@ -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()
             )
         }

+ 26 - 16
app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt

@@ -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()
         )
     }
-}
+}