rss收藏增加分组管理 (#4236)

* rss收藏增加分组和标题编辑选项

* 优化变量命名

* 订阅收藏按分组展示

* 优化:订阅收藏分组展示

* 优化

* 优化rss收藏分组管理

* 优化数据库版本控制
This commit is contained in:
老牛
2024-09-26 14:50:17 +08:00
committed by GitHub
parent 72af4d737a
commit a762ba4587
14 changed files with 2328 additions and 55 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -63,7 +63,7 @@ val appDb by lazy {
}
@Database(
version = 71,
version = 72,
exportSchema = true,
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
@@ -99,6 +99,7 @@ val appDb by lazy {
AutoMigration(from = 68, to = 69),
AutoMigration(from = 69, to = 70),
AutoMigration(from = 70, to = 71),
AutoMigration(from = 71, to = 72),
]
)
abstract class AppDatabase : RoomDatabase() {

View File

@@ -13,7 +13,7 @@ interface RssArticleDao {
@Query(
"""select t1.link, t1.sort, t1.origin, t1.`order`, t1.title, t1.content,
t1.description, t1.image, t1.pubDate, t1.variable, ifNull(t2.read, 0) as read
t1.description, t1.image, t1.`group`, t1.pubDate, t1.variable, ifNull(t2.read, 0) as read
from rssArticles as t1 left join rssReadRecords as t2
on t1.link = t2.record where origin = :origin and sort = :sort
order by `order` desc"""

View File

@@ -10,6 +10,12 @@ interface RssStarDao {
@get:Query("select * from rssStars order by starTime desc")
val all: List<RssStar>
@Query("select `group` from rssStars group by `group` order by `group`")
fun groupList(): Flow<List<String>>
@Query("select * from rssStars where `group` = :group order by starTime desc")
fun getByGroup(group: String): Flow<List<RssStar>>
@Query("select * from rssStars where origin = :origin and link = :link")
fun get(origin: String, link: String): RssStar?

View File

@@ -1,12 +1,12 @@
package io.legado.app.data.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject
import kotlinx.parcelize.IgnoredOnParcel
@Entity(
tableName = "rssArticles",
primaryKeys = ["origin", "link"]
@@ -21,6 +21,8 @@ data class RssArticle(
var description: String? = null,
var content: String? = null,
var image: String? = null,
@ColumnInfo(defaultValue = "默认分组")
var group: String = "默认分组",
var read: Boolean = false,
override var variable: String? = null
) : BaseRssArticle {
@@ -49,6 +51,7 @@ data class RssArticle(
description = description,
content = content,
image = image,
group = group,
variable = variable
)
}

View File

@@ -1,5 +1,6 @@
package io.legado.app.data.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import io.legado.app.utils.GSON
@@ -21,6 +22,8 @@ data class RssStar(
var description: String? = null,
var content: String? = null,
var image: String? = null,
@ColumnInfo(defaultValue = "默认分组")
var group: String = "默认分组",
override var variable: String? = null
) : BaseRssArticle {
@@ -40,6 +43,7 @@ data class RssStar(
description = description,
content = content,
image = image,
group = group,
variable = variable
)
}

View File

@@ -1,62 +1,84 @@
@file:Suppress("DEPRECATION")
package io.legado.app.ui.rss.favorites
import android.os.Bundle
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentStatePagerAdapter
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.base.BaseActivity
import io.legado.app.constant.AppLog
import io.legado.app.data.appDb
import io.legado.app.data.entities.RssStar
import io.legado.app.databinding.ActivityRssFavoritesBinding
import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.rss.read.ReadRssActivity
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.startActivity
import io.legado.app.utils.gone
import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.visible
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
/**
* 收藏夹
*/
class RssFavoritesActivity : BaseActivity<ActivityRssFavoritesBinding>(),
RssFavoritesAdapter.CallBack {
class RssFavoritesActivity : BaseActivity<ActivityRssFavoritesBinding>(){
override val binding by viewBinding(ActivityRssFavoritesBinding::inflate)
private val adapter by lazy { RssFavoritesAdapter(this, this) }
private val adapter by lazy { TabFragmentPageAdapter() }
private var groupList = mutableListOf<String>()
private var rssStarFlowJob: Job? = null
override fun onActivityCreated(savedInstanceState: Bundle?) {
initView()
initData()
binding.viewPager.adapter = adapter
binding.tabLayout.setupWithViewPager(binding.viewPager)
binding.tabLayout.setSelectedTabIndicatorColor(accentColor)
upFragments()
}
private fun initView() {
binding.refreshLayout.setColorSchemeColors(accentColor)
binding.recyclerView.let {
it.layoutManager = LinearLayoutManager(this)
it.addItemDecoration(VerticalDivider(this))
it.adapter = adapter
}
}
private fun initData() {
lifecycleScope.launch {
appDb.rssStarDao.liveAll().catch {
AppLog.put("订阅收藏夹界面获取数据失败\n${it.localizedMessage}", it)
}.flowOn(IO).conflate().collect {
adapter.setItems(it)
private fun upFragments() {
rssStarFlowJob?.cancel()
rssStarFlowJob = lifecycleScope.launch {
appDb.rssStarDao.groupList().catch {
AppLog.put("订阅分组数据获取失败\n${it.localizedMessage}", it)
}.flowOn(IO).collect {
groupList.clear()
groupList.addAll(it)
if (groupList.size == 1) {
binding.tabLayout.gone()
} else {
binding.tabLayout.visible()
}
adapter.notifyDataSetChanged()
}
}
}
override fun readRss(rssStar: RssStar) {
startActivity<ReadRssActivity> {
putExtra("title", rssStar.title)
putExtra("origin", rssStar.origin)
putExtra("link", rssStar.link)
private inner class TabFragmentPageAdapter :
FragmentStatePagerAdapter(supportFragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun getItemPosition(`object`: Any): Int {
return POSITION_NONE
}
override fun getPageTitle(position: Int): CharSequence {
return groupList[position]
}
override fun getItem(position: Int): Fragment {
val group = groupList[position]
return RssFavoritesFragment(group)
}
override fun getCount(): Int {
return groupList.size
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val fragment = super.instantiateItem(container, position) as Fragment
return fragment
}
}
}

View File

@@ -0,0 +1,81 @@
package io.legado.app.ui.rss.favorites
import android.os.Bundle
import android.text.TextUtils
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.data.entities.RssArticle
import io.legado.app.databinding.DialogRssfavoritesBinding
import io.legado.app.lib.theme.primaryColor
import io.legado.app.utils.setLayout
import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.launch
class RssFavoritesDialog() : BaseDialogFragment(R.layout.dialog_rssfavorites, true) {
constructor(rssArticle: RssArticle) : this() {
arguments = Bundle().apply {
putString("title", rssArticle.title)
putString("group", rssArticle.group)
}
}
private val binding by viewBinding(DialogRssfavoritesBinding::bind)
override fun onStart() {
super.onStart()
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
binding.toolBar.setBackgroundColor(primaryColor)
val arguments = arguments ?: let {
dismiss()
return
}
var title = arguments.getString("title") ?: "默认名称"
var group = arguments.getString("group") ?: "默认分组"
binding.run {
editTitle.setText(title)
editGroup.setText(group)
tvCancel.setOnClickListener {
dismiss()
}
tvOk.setOnClickListener {
val editTitle = editTitle.text.toString()
if(!TextUtils.isEmpty(editTitle)){
title = editTitle
}
val editGroup = editGroup.text.toString()
if(!TextUtils.isEmpty(editGroup)){
group = editGroup
}
lifecycleScope.launch {
callback?.updateFavorite(title, group)
dismiss()
}
}
tvFooterLeft.setOnClickListener {
lifecycleScope.launch {
callback?.deleteFavorite()
dismiss()
}
}
}
}
val callback get() = (parentFragment as? Callback) ?: (activity as? Callback)
interface Callback {
fun updateFavorite(title: String, group: String)
fun deleteFavorite()
}
}

View File

@@ -0,0 +1,77 @@
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.theme.primaryColor
import io.legado.app.ui.rss.read.ReadRssActivity
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.setEdgeEffectColor
import io.legado.app.utils.startActivity
import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job
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)
}
private var articlesFlowJob: Job? = null
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
initView()
}
private fun initView() = binding.run {
refreshLayout.setEnabled(false)
recyclerView.setEdgeEffectColor(primaryColor)
recyclerView.layoutManager = run {
recyclerView.addItemDecoration(VerticalDivider(requireContext()))
LinearLayoutManager(requireContext())
}
recyclerView.adapter = adapter
loadArticles()
}
private fun loadArticles() {
articlesFlowJob?.cancel()
articlesFlowJob = lifecycleScope.launch {
val group = arguments?.getString("group") ?: "默认分组"
appDb.rssStarDao.getByGroup(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)
}
}
}

View File

@@ -0,0 +1,9 @@
package io.legado.app.ui.rss.favorites
import android.app.Application
import io.legado.app.base.BaseViewModel
class RssFavoritesViewModel(application: Application) : BaseViewModel(application) {
}

View File

@@ -42,6 +42,7 @@ import io.legado.app.model.Download
import io.legado.app.ui.association.OnLineImportActivity
import io.legado.app.ui.file.HandleFileContract
import io.legado.app.ui.login.SourceLoginActivity
import io.legado.app.ui.rss.favorites.RssFavoritesDialog
import io.legado.app.utils.ACache
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.get
@@ -53,6 +54,7 @@ import io.legado.app.utils.openUrl
import io.legado.app.utils.setDarkeningAllowed
import io.legado.app.utils.setTintMutate
import io.legado.app.utils.share
import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.splitNotBlank
import io.legado.app.utils.startActivity
import io.legado.app.utils.textArray
@@ -70,7 +72,7 @@ import java.util.regex.PatternSyntaxException
/**
* rss阅读界面
*/
class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>() {
class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>(), RssFavoritesDialog.Callback {
override val binding by viewBinding(ActivityRssReadBinding::inflate)
override val viewModel by viewModels<ReadRssViewModel>()
@@ -151,8 +153,12 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
R.id.menu_rss_refresh -> viewModel.refresh {
binding.webView.reload()
}
R.id.menu_rss_star -> viewModel.favorite()
R.id.menu_rss_star -> {
viewModel.addFavorite()
viewModel.rssArticle?.let {
showDialogFragment(RssFavoritesDialog(it))
}
}
R.id.menu_share_it -> {
binding.webView.url?.let {
share(it)
@@ -174,6 +180,16 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
return super.onCompatOptionsItemSelected(item)
}
override fun updateFavorite(title: String, group: String) {
viewModel.rssArticle?.title = title
viewModel.rssArticle?.group = group
viewModel.updateFavorite()
}
override fun deleteFavorite() {
viewModel.delFavorite()
}
@JavascriptInterface
fun isNightTheme(): Boolean {
return AppConfig.isNightTheme
@@ -188,7 +204,7 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
}
}
@SuppressLint("SetJavaScriptEnabled")
@SuppressLint("SetJavaScriptEnabled", "JavascriptInterface")
private fun initWebView() {
binding.progressBar.fontColor = accentColor
binding.webView.webChromeClient = CustomWebChromeClient()

View File

@@ -135,6 +135,39 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application), J
}
}
fun addFavorite() {
execute {
rssStar ?: rssArticle?.toStar()?.let {
appDb.rssStarDao.insert(it)
rssStar = it
}
}.onSuccess {
upStarMenuData.postValue(true)
}
}
fun updateFavorite() {
execute {
rssArticle?.toStar()?.let {
appDb.rssStarDao.update(it)
rssStar = it
}
}.onSuccess {
upStarMenuData.postValue(true)
}
}
fun delFavorite() {
execute {
rssStar?.let {
appDb.rssStarDao.delete(it.origin, it.link)
rssStar = null
}
}.onSuccess {
upStarMenuData.postValue(true)
}
}
fun saveImage(webPic: String?, uri: Uri) {
webPic ?: return
execute {

View File

@@ -1,30 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<io.legado.app.ui.widget.TitleBar
android:id="@+id/title_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/favorites"
app:layout_constraintTop_toTopOf="parent" />
app:title="@string/favorites" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refresh_layout"
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/title_bar"
app:layout_constraintVertical_bias="0.0">
android:layout_height="wrap_content"
app:tabMode="scrollable" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="SpeakableTextPresentCheck" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<LinearLayout
android:id="@+id/vw_bg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/shape_card_view"
android:orientation="vertical"
tools:ignore="UselessParent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/tool_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarStyle"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:title="@string/favorite"
app:titleTextAppearance="@style/ToolbarTitle" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:overScrollMode="ifContentScrolls">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<io.legado.app.ui.widget.text.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="3dp">
<io.legado.app.lib.theme.view.ThemeEditText
android:id="@+id/edit_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/title"
tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
</io.legado.app.ui.widget.text.TextInputLayout>
<io.legado.app.ui.widget.text.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="3dp">
<io.legado.app.lib.theme.view.ThemeEditText
android:id="@+id/edit_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/group_name"
tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
</io.legado.app.ui.widget.text.TextInputLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<com.google.android.flexbox.FlexboxLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="12dp"
android:paddingRight="12dp"
app:flexWrap="wrap"
app:justifyContent="space_between">
<io.legado.app.ui.widget.text.AccentTextView
android:id="@+id/tv_footer_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:text="@string/delete"
tools:ignore="RtlHardcoded" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<io.legado.app.ui.widget.text.AccentTextView
android:id="@+id/tv_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:text="@string/cancel"
tools:ignore="RtlHardcoded" />
<io.legado.app.ui.widget.text.AccentTextView
android:id="@+id/tv_ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:text="@string/ok"
tools:ignore="RtlHardcoded" />
</LinearLayout>
</com.google.android.flexbox.FlexboxLayout>
</LinearLayout>
</FrameLayout>