Rename
This commit is contained in:
1 parent
b86d90021c
commit
6c0757c91b
972 files changed
+6885
-7321
No files matched your search
@@ -0,0 +1,16 @@
|
||||
package io.legato.kazusa.ui.rss.article
|
||||
|
||||
import android.content.Context
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import io.legato.kazusa.base.adapter.RecyclerAdapter
|
||||
import io.legato.kazusa.data.entities.RssArticle
|
||||
|
||||
|
||||
abstract class BaseRssArticlesAdapter<VB : ViewBinding>(context: Context, val callBack: CallBack) :
|
||||
RecyclerAdapter<RssArticle, VB>(context) {
|
||||
|
||||
interface CallBack {
|
||||
val isGridLayout: Boolean
|
||||
fun readRss(rssArticle: RssArticle)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package io.legato.kazusa.ui.rss.article
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.BaseDialogFragment
|
||||
import io.legato.kazusa.base.adapter.ItemViewHolder
|
||||
import io.legato.kazusa.base.adapter.RecyclerAdapter
|
||||
import io.legato.kazusa.data.entities.RssReadRecord
|
||||
import io.legato.kazusa.databinding.DialogRecyclerViewBinding
|
||||
import io.legato.kazusa.databinding.ItemRssReadRecordBinding
|
||||
import io.legato.kazusa.lib.dialogs.alert
|
||||
//import io.legado.app.lib.theme.primaryColor
|
||||
import io.legato.kazusa.utils.setLayout
|
||||
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
|
||||
|
||||
class ReadRecordDialog : BaseDialogFragment(R.layout.dialog_recycler_view),
|
||||
Toolbar.OnMenuItemClickListener {
|
||||
|
||||
private val viewModel by viewModels<RssSortViewModel>()
|
||||
private val binding by viewBinding(DialogRecyclerViewBinding::bind)
|
||||
private val adapter by lazy {
|
||||
ReadRecordAdapter(requireContext())
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
setLayout(0.9f, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.run {
|
||||
//toolBar.setBackgroundColor(primaryColor)
|
||||
toolBar.setTitle(R.string.read_record)
|
||||
toolBar.inflateMenu(R.menu.rss_read_record)
|
||||
toolBar.setOnMenuItemClickListener(this@ReadRecordDialog)
|
||||
recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
recyclerView.adapter = adapter
|
||||
}
|
||||
adapter.setItems(viewModel.getRecords())
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||
when (item?.itemId) {
|
||||
R.id.menu_clear -> {
|
||||
alert(R.string.draw) {
|
||||
val countRead = viewModel.countRecords()
|
||||
setMessage(getString(R.string.sure_del) + "\n" + countRead + " " + getString(R.string.read_record))
|
||||
noButton()
|
||||
yesButton {
|
||||
viewModel.deleteAllRecord()
|
||||
adapter.clearItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
inner class ReadRecordAdapter(context: Context) :
|
||||
RecyclerAdapter<RssReadRecord, ItemRssReadRecordBinding>(context) {
|
||||
|
||||
override fun getViewBinding(parent: ViewGroup): ItemRssReadRecordBinding {
|
||||
return ItemRssReadRecordBinding.inflate(inflater, parent, false)
|
||||
}
|
||||
|
||||
override fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemRssReadRecordBinding,
|
||||
item: RssReadRecord,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
binding.textTitle.text = item.title
|
||||
binding.textRecord.text = item.record
|
||||
}
|
||||
|
||||
override fun registerListener(holder: ItemViewHolder, binding: ItemRssReadRecordBinding) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package io.legato.kazusa.ui.rss.article
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.ViewGroup
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.adapter.ItemViewHolder
|
||||
import io.legato.kazusa.data.entities.RssArticle
|
||||
import io.legato.kazusa.databinding.ItemRssArticleBinding
|
||||
import io.legato.kazusa.help.glide.ImageLoader
|
||||
import io.legato.kazusa.help.glide.OkHttpModelLoader
|
||||
import io.legato.kazusa.utils.getCompatColor
|
||||
import io.legato.kazusa.utils.gone
|
||||
import io.legato.kazusa.utils.visible
|
||||
|
||||
|
||||
class RssArticlesAdapter(context: Context, callBack: CallBack) :
|
||||
BaseRssArticlesAdapter<ItemRssArticleBinding>(context, callBack) {
|
||||
|
||||
override fun getViewBinding(parent: ViewGroup): ItemRssArticleBinding {
|
||||
return ItemRssArticleBinding.inflate(inflater, parent, false)
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
override fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemRssArticleBinding,
|
||||
item: RssArticle,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
binding.run {
|
||||
tvTitle.text = item.title
|
||||
tvPubDate.text = item.pubDate
|
||||
if (item.image.isNullOrBlank() && !callBack.isGridLayout) {
|
||||
imageView.gone()
|
||||
} else {
|
||||
val options =
|
||||
RequestOptions().set(OkHttpModelLoader.sourceOriginOption, item.origin)
|
||||
ImageLoader.load(context, item.image).apply(options).apply {
|
||||
if (callBack.isGridLayout) {
|
||||
placeholder(R.drawable.image_rss_article)
|
||||
} else {
|
||||
addListener(object : RequestListener<Drawable> {
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
imageView.gone()
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
model: Any,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
imageView.visible()
|
||||
return false
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}.into(imageView)
|
||||
}
|
||||
if (item.read) {
|
||||
tvTitle.setTextColor(context.getCompatColor(R.color.tv_text_summary))
|
||||
} else {
|
||||
tvTitle.setTextColor(context.getCompatColor(R.color.primaryText))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerListener(holder: ItemViewHolder, binding: ItemRssArticleBinding) {
|
||||
holder.itemView.setOnClickListener {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
callBack.readRss(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package io.legato.kazusa.ui.rss.article
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.ViewGroup
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.adapter.ItemViewHolder
|
||||
import io.legato.kazusa.data.entities.RssArticle
|
||||
import io.legato.kazusa.databinding.ItemRssArticle1Binding
|
||||
import io.legato.kazusa.help.glide.ImageLoader
|
||||
import io.legato.kazusa.help.glide.OkHttpModelLoader
|
||||
import io.legato.kazusa.utils.gone
|
||||
import io.legato.kazusa.utils.visible
|
||||
|
||||
|
||||
class RssArticlesAdapter1(context: Context, callBack: CallBack) :
|
||||
BaseRssArticlesAdapter<ItemRssArticle1Binding>(context, callBack) {
|
||||
|
||||
override fun getViewBinding(parent: ViewGroup): ItemRssArticle1Binding {
|
||||
return ItemRssArticle1Binding.inflate(inflater, parent, false)
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
override fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemRssArticle1Binding,
|
||||
item: RssArticle,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
binding.run {
|
||||
tvTitle.text = item.title
|
||||
tvPubDate.text = item.pubDate
|
||||
if (item.image.isNullOrBlank() && !callBack.isGridLayout) {
|
||||
imageView.gone()
|
||||
} else {
|
||||
val options =
|
||||
RequestOptions().set(OkHttpModelLoader.sourceOriginOption, item.origin)
|
||||
ImageLoader.load(context, item.image).apply(options).apply {
|
||||
if (callBack.isGridLayout) {
|
||||
placeholder(R.drawable.image_rss_article)
|
||||
} else {
|
||||
addListener(object : RequestListener<Drawable> {
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
imageView.gone()
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
model: Any,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
imageView.visible()
|
||||
return false
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}.into(imageView)
|
||||
}
|
||||
// if (item.read) {
|
||||
// tvTitle.setTextColor(context.getCompatColor(R.color.tv_text_summary))
|
||||
// } else {
|
||||
// tvTitle.setTextColor(context.getCompatColor(R.color.primaryText))
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerListener(holder: ItemViewHolder, binding: ItemRssArticle1Binding) {
|
||||
holder.itemView.setOnClickListener {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
callBack.readRss(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package io.legato.kazusa.ui.rss.article
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.ViewGroup
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.adapter.ItemViewHolder
|
||||
import io.legato.kazusa.data.entities.RssArticle
|
||||
import io.legato.kazusa.databinding.ItemRssArticle2Binding
|
||||
import io.legato.kazusa.help.glide.ImageLoader
|
||||
import io.legato.kazusa.help.glide.OkHttpModelLoader
|
||||
import io.legato.kazusa.utils.getCompatColor
|
||||
import io.legato.kazusa.utils.gone
|
||||
import io.legato.kazusa.utils.visible
|
||||
|
||||
|
||||
class RssArticlesAdapter2(context: Context, callBack: CallBack) :
|
||||
BaseRssArticlesAdapter<ItemRssArticle2Binding>(context, callBack) {
|
||||
|
||||
override fun getViewBinding(parent: ViewGroup): ItemRssArticle2Binding {
|
||||
return ItemRssArticle2Binding.inflate(inflater, parent, false)
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
override fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemRssArticle2Binding,
|
||||
item: RssArticle,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
binding.run {
|
||||
tvTitle.text = item.title
|
||||
tvPubDate.text = item.pubDate
|
||||
if (item.image.isNullOrBlank() && !callBack.isGridLayout) {
|
||||
imageView.gone()
|
||||
} else {
|
||||
val options =
|
||||
RequestOptions().set(OkHttpModelLoader.sourceOriginOption, item.origin)
|
||||
ImageLoader.load(context, item.image).apply(options).apply {
|
||||
if (callBack.isGridLayout) {
|
||||
placeholder(R.drawable.image_rss_article)
|
||||
} else {
|
||||
addListener(object : RequestListener<Drawable> {
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
imageView.gone()
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
model: Any,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
imageView.visible()
|
||||
return false
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}.into(imageView)
|
||||
}
|
||||
if (item.read) {
|
||||
tvTitle.setTextColor(context.getCompatColor(R.color.tv_text_summary))
|
||||
} else {
|
||||
tvTitle.setTextColor(context.getCompatColor(R.color.primaryText))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerListener(holder: ItemViewHolder, binding: ItemRssArticle2Binding) {
|
||||
holder.itemView.setOnClickListener {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
callBack.readRss(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package io.legato.kazusa.ui.rss.article
|
||||
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.VMBaseFragment
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.data.appDb
|
||||
import io.legato.kazusa.data.entities.RssArticle
|
||||
import io.legato.kazusa.databinding.FragmentRssArticlesBinding
|
||||
import io.legato.kazusa.databinding.ViewLoadMoreBinding
|
||||
//import io.legado.app.lib.theme.accentColor
|
||||
//import io.legado.app.lib.theme.primaryColor
|
||||
import io.legato.kazusa.ui.rss.read.ReadRssActivity
|
||||
import io.legato.kazusa.ui.widget.recycler.LoadMoreView
|
||||
import io.legato.kazusa.ui.widget.recycler.VerticalDivider
|
||||
import io.legato.kazusa.utils.applyNavigationBarPadding
|
||||
import io.legato.kazusa.utils.startActivity
|
||||
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RssArticlesFragment() : VMBaseFragment<RssArticlesViewModel>(R.layout.fragment_rss_articles),
|
||||
BaseRssArticlesAdapter.CallBack {
|
||||
|
||||
constructor(sortName: String, sortUrl: String) : this() {
|
||||
arguments = Bundle().apply {
|
||||
putString("sortName", sortName)
|
||||
putString("sortUrl", sortUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private val binding by viewBinding(FragmentRssArticlesBinding::bind)
|
||||
private val activityViewModel by activityViewModels<RssSortViewModel>()
|
||||
override val viewModel by viewModels<RssArticlesViewModel>()
|
||||
private val adapter: BaseRssArticlesAdapter<*> by lazy {
|
||||
when (activityViewModel.rssSource?.articleStyle) {
|
||||
1 -> RssArticlesAdapter1(requireContext(), this@RssArticlesFragment)
|
||||
2 -> RssArticlesAdapter2(requireContext(), this@RssArticlesFragment)
|
||||
else -> RssArticlesAdapter(requireContext(), this@RssArticlesFragment)
|
||||
}
|
||||
}
|
||||
private val loadMoreView: LoadMoreView by lazy {
|
||||
LoadMoreView(requireContext())
|
||||
}
|
||||
private var articlesFlowJob: Job? = null
|
||||
override val isGridLayout: Boolean
|
||||
get() = activityViewModel.isGridLayout
|
||||
|
||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
||||
viewModel.init(arguments)
|
||||
initView()
|
||||
initData()
|
||||
}
|
||||
|
||||
private fun initView() = binding.run {
|
||||
// refreshLayout.setColorSchemeColors(accentColor)
|
||||
// recyclerView.setEdgeEffectColor(primaryColor)
|
||||
recyclerView.applyNavigationBarPadding()
|
||||
loadMoreView.setOnClickListener {
|
||||
if (!loadMoreView.isLoading) {
|
||||
scrollToBottom(true)
|
||||
}
|
||||
}
|
||||
recyclerView.layoutManager = if (activityViewModel.isGridLayout) {
|
||||
recyclerView.setPadding(8, 0, 8, 0)
|
||||
GridLayoutManager(requireContext(), 2)
|
||||
} else {
|
||||
recyclerView.addItemDecoration(VerticalDivider(requireContext()))
|
||||
LinearLayoutManager(requireContext())
|
||||
}
|
||||
recyclerView.adapter = adapter
|
||||
adapter.addFooterView {
|
||||
ViewLoadMoreBinding.bind(loadMoreView)
|
||||
}
|
||||
refreshLayout.setOnRefreshListener {
|
||||
loadArticles()
|
||||
}
|
||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
if (!recyclerView.canScrollVertically(1)) {
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
})
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
refreshLayout.isRefreshing = true
|
||||
loadArticles()
|
||||
this@launch.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initData() {
|
||||
val rssUrl = activityViewModel.url ?: return
|
||||
articlesFlowJob?.cancel()
|
||||
articlesFlowJob = viewLifecycleOwner.lifecycleScope.launch {
|
||||
appDb.rssArticleDao.flowByOriginSort(rssUrl, viewModel.sortName).catch {
|
||||
AppLog.put("订阅文章界面获取数据失败\n${it.localizedMessage}", it)
|
||||
}.flowOn(IO).collect {
|
||||
adapter.setItems(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadArticles() {
|
||||
activityViewModel.rssSource?.let {
|
||||
viewModel.loadArticles(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToBottom(forceLoad: Boolean = false) {
|
||||
if (viewModel.isLoading) return
|
||||
if ((loadMoreView.hasMore && adapter.getActualItemCount() > 0) || forceLoad) {
|
||||
loadMoreView.hasMore()
|
||||
activityViewModel.rssSource?.let {
|
||||
viewModel.loadMore(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeLiveBus() {
|
||||
viewModel.loadErrorLiveData.observe(viewLifecycleOwner) {
|
||||
loadMoreView.error(it)
|
||||
}
|
||||
viewModel.loadFinallyLiveData.observe(viewLifecycleOwner) { hasMore ->
|
||||
binding.refreshLayout.isRefreshing = false
|
||||
if (!hasMore) {
|
||||
loadMoreView.noMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun readRss(rssArticle: RssArticle) {
|
||||
activityViewModel.read(rssArticle)
|
||||
startActivity<ReadRssActivity> {
|
||||
putExtra("title", rssArticle.title)
|
||||
putExtra("origin", rssArticle.origin)
|
||||
putExtra("link", rssArticle.link)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package io.legato.kazusa.ui.rss.article
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.legato.kazusa.base.BaseViewModel
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.data.appDb
|
||||
import io.legato.kazusa.data.entities.RssArticle
|
||||
import io.legato.kazusa.data.entities.RssSource
|
||||
import io.legato.kazusa.model.rss.Rss
|
||||
import io.legato.kazusa.utils.stackTraceStr
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
|
||||
|
||||
class RssArticlesViewModel(application: Application) : BaseViewModel(application) {
|
||||
val loadFinallyLiveData = MutableLiveData<Boolean>()
|
||||
val loadErrorLiveData = MutableLiveData<String>()
|
||||
var isLoading = true
|
||||
var order = System.currentTimeMillis()
|
||||
private var nextPageUrl: String? = null
|
||||
var sortName: String = ""
|
||||
var sortUrl: String = ""
|
||||
var page = 1
|
||||
|
||||
fun init(bundle: Bundle?) {
|
||||
bundle?.let {
|
||||
sortName = it.getString("sortName") ?: ""
|
||||
sortUrl = it.getString("sortUrl") ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
fun loadArticles(rssSource: RssSource) {
|
||||
isLoading = true
|
||||
page = 1
|
||||
order = System.currentTimeMillis()
|
||||
Rss.getArticles(viewModelScope, sortName, sortUrl, rssSource, page).onSuccess(IO) {
|
||||
nextPageUrl = it.second
|
||||
val articles = it.first
|
||||
articles.forEach { rssArticle ->
|
||||
rssArticle.order = order--
|
||||
}
|
||||
appDb.rssArticleDao.insert(*articles.toTypedArray())
|
||||
if (!rssSource.ruleNextPage.isNullOrEmpty()) {
|
||||
appDb.rssArticleDao.clearOld(rssSource.sourceUrl, sortName, order)
|
||||
}
|
||||
val hasMore = articles.isNotEmpty() && !rssSource.ruleNextPage.isNullOrEmpty()
|
||||
loadFinallyLiveData.postValue(hasMore)
|
||||
isLoading = false
|
||||
}.onError {
|
||||
loadFinallyLiveData.postValue(false)
|
||||
AppLog.put("rss获取内容失败", it)
|
||||
loadErrorLiveData.postValue(it.stackTraceStr)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMore(rssSource: RssSource) {
|
||||
isLoading = true
|
||||
page++
|
||||
val pageUrl = nextPageUrl
|
||||
if (pageUrl.isNullOrEmpty()) {
|
||||
loadFinallyLiveData.postValue(false)
|
||||
return
|
||||
}
|
||||
Rss.getArticles(viewModelScope, sortName, pageUrl, rssSource, page).onSuccess(IO) {
|
||||
nextPageUrl = it.second
|
||||
loadMoreSuccess(it.first)
|
||||
isLoading = false
|
||||
}.onError {
|
||||
loadFinallyLiveData.postValue(false)
|
||||
AppLog.put("rss获取内容失败", it)
|
||||
loadErrorLiveData.postValue(it.stackTraceStr)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadMoreSuccess(articles: MutableList<RssArticle>) {
|
||||
if (articles.isEmpty()) {
|
||||
loadFinallyLiveData.postValue(false)
|
||||
return
|
||||
}
|
||||
val firstArticle = articles.first()
|
||||
val dbFirstArticle = appDb.rssArticleDao.get(firstArticle.origin, firstArticle.link)
|
||||
val lastArticle = articles.last()
|
||||
val dbLastArticle = appDb.rssArticleDao.get(lastArticle.origin, lastArticle.link)
|
||||
if (dbFirstArticle != null && dbLastArticle != null) {
|
||||
loadFinallyLiveData.postValue(false)
|
||||
} else {
|
||||
articles.forEach {
|
||||
it.order = order--
|
||||
}
|
||||
appDb.rssArticleDao.append(*articles.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package io.legato.kazusa.ui.rss.article
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.viewModels
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.VMBaseActivity
|
||||
import io.legato.kazusa.databinding.ActivityRssArtivlesBinding
|
||||
import io.legato.kazusa.help.source.sortUrls
|
||||
//import io.legado.app.lib.theme.accentColor
|
||||
import io.legato.kazusa.ui.login.SourceLoginActivity
|
||||
import io.legato.kazusa.ui.rss.source.edit.RssSourceEditActivity
|
||||
import io.legato.kazusa.ui.widget.dialog.VariableDialog
|
||||
import io.legato.kazusa.utils.*
|
||||
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewModel>(),
|
||||
VariableDialog.Callback {
|
||||
|
||||
override val binding by viewBinding(ActivityRssArtivlesBinding::inflate)
|
||||
override val viewModel by viewModels<RssSortViewModel>()
|
||||
private val adapter by lazy { TabFragmentPageAdapter() }
|
||||
private val sortList = mutableListOf<Pair<String, String>>()
|
||||
private val fragmentMap = hashMapOf<String, Fragment>()
|
||||
private val editSourceResult = registerForActivityResult(
|
||||
StartActivityContract(RssSourceEditActivity::class.java)
|
||||
) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
viewModel.initData(intent) {
|
||||
upFragments()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||
//binding.tabLayout.setSelectedTabIndicatorColor(accentColor)
|
||||
viewModel.titleLiveData.observe(this) {
|
||||
binding.titleBar.title = it
|
||||
}
|
||||
viewModel.initData(intent) {
|
||||
upFragments()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.rss_articles, menu)
|
||||
return super.onCompatCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
|
||||
menu.findItem(R.id.menu_login)?.isVisible =
|
||||
!viewModel.rssSource?.loginUrl.isNullOrBlank()
|
||||
return super.onMenuOpened(featureId, menu)
|
||||
}
|
||||
|
||||
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_login -> startActivity<SourceLoginActivity> {
|
||||
putExtra("type", "rssSource")
|
||||
putExtra("key", viewModel.rssSource?.sourceUrl)
|
||||
}
|
||||
|
||||
R.id.menu_refresh_sort -> viewModel.clearSortCache { upFragments() }
|
||||
R.id.menu_set_source_variable -> setSourceVariable()
|
||||
R.id.menu_edit_source -> viewModel.rssSource?.sourceUrl?.let {
|
||||
editSourceResult.launch {
|
||||
putExtra("sourceUrl", it)
|
||||
}
|
||||
}
|
||||
|
||||
R.id.menu_clear -> {
|
||||
viewModel.url?.let {
|
||||
viewModel.clearArticles()
|
||||
}
|
||||
}
|
||||
|
||||
R.id.menu_switch_layout -> {
|
||||
viewModel.switchLayout()
|
||||
upFragments()
|
||||
}
|
||||
|
||||
R.id.menu_read_record -> {
|
||||
showDialogFragment<ReadRecordDialog>()
|
||||
}
|
||||
}
|
||||
return super.onCompatOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun upFragments() {
|
||||
lifecycleScope.launch {
|
||||
viewModel.rssSource?.sortUrls()?.let {
|
||||
sortList.clear()
|
||||
sortList.addAll(it)
|
||||
}
|
||||
if (sortList.size == 1) {
|
||||
binding.tabLayout.gone()
|
||||
} else {
|
||||
binding.tabLayout.visible()
|
||||
}
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSourceVariable() {
|
||||
lifecycleScope.launch {
|
||||
val source = viewModel.rssSource
|
||||
if (source == null) {
|
||||
toastOnUi("源不存在")
|
||||
return@launch
|
||||
}
|
||||
val comment =
|
||||
source.getDisplayVariableComment("源变量可在js中通过source.getVariable()获取")
|
||||
val variable = withContext(Dispatchers.IO) { source.getVariable() }
|
||||
showDialogFragment(
|
||||
VariableDialog(
|
||||
getString(R.string.set_source_variable),
|
||||
source.getKey(),
|
||||
variable,
|
||||
comment
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setVariable(key: String, variable: String?) {
|
||||
viewModel.rssSource?.setVariable(variable)
|
||||
}
|
||||
|
||||
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 sortList[position].first
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): Fragment {
|
||||
val sort = sortList[position]
|
||||
return RssArticlesFragment(sort.first, sort.second)
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return sortList.size
|
||||
}
|
||||
|
||||
override fun instantiateItem(container: ViewGroup, position: Int): Any {
|
||||
val fragment = super.instantiateItem(container, position) as Fragment
|
||||
fragmentMap[sortList[position].first] = fragment
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package io.legato.kazusa.ui.rss.article
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.legato.kazusa.base.BaseViewModel
|
||||
import io.legato.kazusa.data.appDb
|
||||
import io.legato.kazusa.data.entities.RssArticle
|
||||
import io.legato.kazusa.data.entities.RssReadRecord
|
||||
import io.legato.kazusa.data.entities.RssSource
|
||||
import io.legato.kazusa.help.source.removeSortCache
|
||||
|
||||
|
||||
class RssSortViewModel(application: Application) : BaseViewModel(application) {
|
||||
var url: String? = null
|
||||
var rssSource: RssSource? = null
|
||||
val titleLiveData = MutableLiveData<String>()
|
||||
var order = System.currentTimeMillis()
|
||||
val isGridLayout get() = rssSource?.articleStyle == 2
|
||||
|
||||
fun initData(intent: Intent, finally: () -> Unit) {
|
||||
execute {
|
||||
url = intent.getStringExtra("url")
|
||||
url?.let { url ->
|
||||
rssSource = appDb.rssSourceDao.getByKey(url)
|
||||
rssSource?.let {
|
||||
titleLiveData.postValue(it.sourceName)
|
||||
} ?: let {
|
||||
rssSource = RssSource(sourceUrl = url)
|
||||
}
|
||||
}
|
||||
}.onFinally {
|
||||
finally()
|
||||
}
|
||||
}
|
||||
|
||||
fun switchLayout() {
|
||||
rssSource?.let {
|
||||
if (it.articleStyle < 2) {
|
||||
it.articleStyle += 1
|
||||
} else {
|
||||
it.articleStyle = 0
|
||||
}
|
||||
execute {
|
||||
appDb.rssSourceDao.update(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun read(rssArticle: RssArticle) {
|
||||
execute {
|
||||
val rssReadRecord = RssReadRecord(
|
||||
record = rssArticle.link,
|
||||
title = rssArticle.title,
|
||||
readTime = System.currentTimeMillis()
|
||||
)
|
||||
appDb.rssReadRecordDao.insertRecord(rssReadRecord)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearArticles() {
|
||||
execute {
|
||||
url?.let {
|
||||
appDb.rssArticleDao.delete(it)
|
||||
}
|
||||
order = System.currentTimeMillis()
|
||||
}.onSuccess {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSortCache(onFinally: () -> Unit) {
|
||||
execute {
|
||||
rssSource?.removeSortCache()
|
||||
}.onFinally {
|
||||
onFinally.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun getRecords(): List<RssReadRecord> {
|
||||
return appDb.rssReadRecordDao.getRecords()
|
||||
}
|
||||
|
||||
fun countRecords() : Int {
|
||||
return appDb.rssReadRecordDao.countRecords
|
||||
}
|
||||
|
||||
fun deleteAllRecord() {
|
||||
execute {
|
||||
appDb.rssReadRecordDao.deleteAllRecord()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package io.legato.kazusa.ui.rss.favorites
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.SubMenu
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.BaseActivity
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.data.appDb
|
||||
import io.legato.kazusa.databinding.ActivityRssFavoritesBinding
|
||||
import io.legato.kazusa.lib.dialogs.alert
|
||||
//import io.legado.app.lib.theme.accentColor
|
||||
import io.legato.kazusa.utils.gone
|
||||
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
|
||||
import io.legato.kazusa.utils.visible
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 收藏夹
|
||||
*/
|
||||
class RssFavoritesActivity : BaseActivity<ActivityRssFavoritesBinding>() {
|
||||
|
||||
override val binding by viewBinding(ActivityRssFavoritesBinding::inflate)
|
||||
private val adapter by lazy { TabFragmentPageAdapter() }
|
||||
private var groupList = mutableListOf<String>()
|
||||
private var groupsMenu: SubMenu? = null
|
||||
private var currentGroup = ""
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
initView()
|
||||
upFragments()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
//从ReadRssActivity退出时,判断是否需要重新定位tabLayout选中项
|
||||
if (currentGroup.isNotEmpty() && groupList.isNotEmpty()){
|
||||
var item = groupList.indexOf(currentGroup)
|
||||
val currentItem = binding.viewPager.currentItem
|
||||
//如果坐标没有变化,则结束
|
||||
if(item == currentItem){
|
||||
return
|
||||
}
|
||||
if (item == -1){
|
||||
item = currentItem
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
delay(100)
|
||||
binding.tabLayout.getTabAt(item)?.select()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
|
||||
override fun onPageScrolled(
|
||||
position: Int,
|
||||
positionOffset: Float,
|
||||
positionOffsetPixels: Int
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onPageSelected(position: Int) {
|
||||
currentGroup = groupList[position]
|
||||
}
|
||||
|
||||
override fun onPageScrollStateChanged(state: Int) {}
|
||||
|
||||
})
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||
//binding.tabLayout.setSelectedTabIndicatorColor(accentColor)
|
||||
}
|
||||
|
||||
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.rss_favorites, menu)
|
||||
groupsMenu = menu.findItem(R.id.menu_group)?.subMenu
|
||||
upGroupsMenu()
|
||||
return super.onCompatCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
private fun upGroupsMenu() = groupsMenu?.let { subMenu ->
|
||||
subMenu.removeGroup(R.id.menu_group)
|
||||
groupList.forEachIndexed { index, it ->
|
||||
subMenu.add(R.id.menu_group, Menu.NONE, index, it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.groupId == R.id.menu_group) {
|
||||
binding.viewPager.setCurrentItem(item.order)
|
||||
} else {
|
||||
when (item.itemId) {
|
||||
R.id.menu_del_group -> deleteGroup()
|
||||
R.id.menu_del_all -> deleteAll()
|
||||
}
|
||||
}
|
||||
return super.onCompatOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun upFragments() {
|
||||
lifecycleScope.launch {
|
||||
appDb.rssStarDao.flowGroups().catch {
|
||||
AppLog.put("订阅分组数据获取失败\n${it.localizedMessage}", it)
|
||||
}.distinctUntilChanged().flowOn(IO).collect {
|
||||
groupList.clear()
|
||||
groupList.addAll(it)
|
||||
if (groupList.size == 1) {
|
||||
binding.tabLayout.gone()
|
||||
} else {
|
||||
binding.tabLayout.visible()
|
||||
}
|
||||
if (groupsMenu != null) {
|
||||
upGroupsMenu()
|
||||
}
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteGroup() {
|
||||
alert(R.string.draw) {
|
||||
val item = binding.viewPager.currentItem
|
||||
val group = groupList[item]
|
||||
setMessage(getString(R.string.sure_del) + "\n<" + group + ">" + getString(R.string.group))
|
||||
noButton()
|
||||
yesButton {
|
||||
appDb.rssStarDao.deleteByGroup(group)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteAll() {
|
||||
alert(R.string.draw) {
|
||||
setMessage(getString(R.string.sure_del) + "\n<" + getString(R.string.all) + ">" + getString(R.string.favorite))
|
||||
noButton()
|
||||
yesButton {
|
||||
appDb.rssStarDao.deleteAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package io.legato.kazusa.ui.rss.favorites
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.ViewGroup
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import io.legato.kazusa.base.adapter.ItemViewHolder
|
||||
import io.legato.kazusa.base.adapter.RecyclerAdapter
|
||||
import io.legato.kazusa.data.entities.RssStar
|
||||
import io.legato.kazusa.databinding.ItemRssArticleBinding
|
||||
import io.legato.kazusa.help.glide.ImageLoader
|
||||
import io.legato.kazusa.help.glide.OkHttpModelLoader
|
||||
import io.legato.kazusa.utils.gone
|
||||
import io.legato.kazusa.utils.visible
|
||||
|
||||
|
||||
class RssFavoritesAdapter(context: Context, val callBack: CallBack) :
|
||||
RecyclerAdapter<RssStar, ItemRssArticleBinding>(context) {
|
||||
|
||||
override fun getViewBinding(parent: ViewGroup): ItemRssArticleBinding {
|
||||
return ItemRssArticleBinding.inflate(inflater, parent, false)
|
||||
}
|
||||
|
||||
override fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemRssArticleBinding,
|
||||
item: RssStar,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
binding.run {
|
||||
tvTitle.text = item.title
|
||||
tvPubDate.text = item.pubDate
|
||||
if (item.image.isNullOrBlank()) {
|
||||
imageView.gone()
|
||||
} else {
|
||||
val options =
|
||||
RequestOptions().set(OkHttpModelLoader.sourceOriginOption, item.origin)
|
||||
ImageLoader.load(context, item.image)
|
||||
.apply(options)
|
||||
.addListener(object : RequestListener<Drawable> {
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
imageView.gone()
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
model: Any,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
imageView.visible()
|
||||
return false
|
||||
}
|
||||
|
||||
})
|
||||
.into(imageView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerListener(holder: ItemViewHolder, binding: ItemRssArticleBinding) {
|
||||
holder.itemView.setOnClickListener {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
callBack.readRss(it)
|
||||
}
|
||||
}
|
||||
holder.itemView.setOnLongClickListener {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
callBack.delStar(it)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
interface CallBack {
|
||||
fun readRss(rssStar: RssStar)
|
||||
|
||||
fun delStar(rssStar: RssStar)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package io.legato.kazusa.ui.rss.favorites
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.BaseDialogFragment
|
||||
import io.legato.kazusa.data.entities.RssArticle
|
||||
import io.legato.kazusa.databinding.DialogRssFavoriteConfigBinding
|
||||
//import io.legado.app.lib.theme.primaryColor
|
||||
import io.legato.kazusa.utils.setLayout
|
||||
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
|
||||
|
||||
class RssFavoritesDialog() : BaseDialogFragment(R.layout.dialog_rss_favorite_config, true) {
|
||||
|
||||
constructor(rssArticle: RssArticle) : this() {
|
||||
arguments = Bundle().apply {
|
||||
putString("title", rssArticle.title)
|
||||
putString("group", rssArticle.group)
|
||||
}
|
||||
}
|
||||
|
||||
private val binding by viewBinding(DialogRssFavoriteConfigBinding::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 (editTitle.isNotBlank()) {
|
||||
title = editTitle
|
||||
}
|
||||
val editGroup = editGroup.text.toString()
|
||||
if (editGroup.isNotBlank()) {
|
||||
group = editGroup
|
||||
}
|
||||
callback?.updateFavorite(title, group)
|
||||
dismiss()
|
||||
}
|
||||
tvFooterLeft.setOnClickListener {
|
||||
callback?.deleteFavorite()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val callback get() = (parentFragment as? Callback) ?: (activity as? Callback)
|
||||
|
||||
interface Callback {
|
||||
|
||||
fun updateFavorite(title: String?, group: String?)
|
||||
|
||||
fun deleteFavorite()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package io.legato.kazusa.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.legato.kazusa.R
|
||||
import io.legato.kazusa.base.VMBaseFragment
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.data.appDb
|
||||
import io.legato.kazusa.data.entities.RssStar
|
||||
import io.legato.kazusa.databinding.FragmentRssArticlesBinding
|
||||
import io.legato.kazusa.lib.dialogs.alert
|
||||
//import io.legado.app.lib.theme.primaryColor
|
||||
import io.legato.kazusa.ui.rss.read.ReadRssActivity
|
||||
import io.legato.kazusa.ui.widget.recycler.VerticalDivider
|
||||
import io.legato.kazusa.utils.applyNavigationBarPadding
|
||||
import io.legato.kazusa.utils.startActivity
|
||||
import io.legato.kazusa.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.legato.kazusa.ui.rss.favorites
|
||||
|
||||
import android.app.Application
|
||||
import io.legato.kazusa.base.BaseViewModel
|
||||
|
||||
|
||||
class RssFavoritesViewModel(application: Application) : BaseViewModel(application) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
package io.legato.kazusa.ui.rss.read
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.net.http.SslError
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.webkit.URLUtil
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.size
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.script.rhino.runScriptWithContext
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.VMBaseActivity
|
||||
import io.legato.kazusa.constant.AppConst
|
||||
import io.legato.kazusa.constant.AppConst.imagePathKey
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.data.entities.RssSource
|
||||
import io.legato.kazusa.databinding.ActivityRssReadBinding
|
||||
import io.legato.kazusa.help.config.AppConfig
|
||||
import io.legato.kazusa.help.http.CookieManager
|
||||
import io.legato.kazusa.lib.dialogs.SelectItem
|
||||
import io.legato.kazusa.lib.dialogs.selector
|
||||
//import io.legado.app.lib.theme.accentColor
|
||||
//import io.legado.app.lib.theme.primaryTextColor
|
||||
import io.legato.kazusa.model.Download
|
||||
import io.legato.kazusa.ui.association.OnLineImportActivity
|
||||
import io.legato.kazusa.ui.file.HandleFileContract
|
||||
import io.legato.kazusa.ui.login.SourceLoginActivity
|
||||
import io.legato.kazusa.ui.rss.favorites.RssFavoritesDialog
|
||||
import io.legato.kazusa.utils.ACache
|
||||
import io.legato.kazusa.utils.NetworkUtils
|
||||
import io.legato.kazusa.utils.gone
|
||||
import io.legato.kazusa.utils.invisible
|
||||
import io.legato.kazusa.utils.isTrue
|
||||
import io.legato.kazusa.utils.keepScreenOn
|
||||
import io.legato.kazusa.utils.longSnackbar
|
||||
import io.legato.kazusa.utils.openUrl
|
||||
import io.legato.kazusa.utils.setDarkeningAllowed
|
||||
import io.legato.kazusa.utils.setOnApplyWindowInsetsListenerCompat
|
||||
import io.legato.kazusa.utils.share
|
||||
import io.legato.kazusa.utils.showDialogFragment
|
||||
import io.legato.kazusa.utils.splitNotBlank
|
||||
import io.legato.kazusa.utils.startActivity
|
||||
import io.legato.kazusa.utils.textArray
|
||||
import io.legato.kazusa.utils.toastOnUi
|
||||
import io.legato.kazusa.utils.toggleSystemBar
|
||||
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
|
||||
import io.legato.kazusa.utils.visible
|
||||
import kotlinx.coroutines.launch
|
||||
import org.apache.commons.text.StringEscapeUtils
|
||||
import org.jsoup.Jsoup
|
||||
import splitties.views.bottomPadding
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.net.URLDecoder
|
||||
import java.util.regex.PatternSyntaxException
|
||||
|
||||
/**
|
||||
* rss阅读界面
|
||||
*/
|
||||
class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>(),
|
||||
RssFavoritesDialog.Callback {
|
||||
|
||||
override val binding by viewBinding(ActivityRssReadBinding::inflate)
|
||||
override val viewModel by viewModels<ReadRssViewModel>()
|
||||
|
||||
private var starMenuItem: MenuItem? = null
|
||||
private var ttsMenuItem: MenuItem? = null
|
||||
private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null
|
||||
private val selectImageDir = registerForActivityResult(HandleFileContract()) {
|
||||
it.uri?.let { uri ->
|
||||
ACache.get().put(imagePathKey, uri.toString())
|
||||
viewModel.saveImage(it.value, uri)
|
||||
}
|
||||
}
|
||||
private val rssJsExtensions by lazy { RssJsExtensions(this) }
|
||||
|
||||
fun getSource(): RssSource? {
|
||||
return viewModel.rssSource
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
viewModel.upStarMenuData.observe(this) { upStarMenu() }
|
||||
viewModel.upTtsMenuData.observe(this) { upTtsMenu(it) }
|
||||
binding.titleBar.title = intent.getStringExtra("title")
|
||||
initView()
|
||||
initWebView()
|
||||
initLiveData()
|
||||
viewModel.initData(intent)
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
if (binding.customWebView.size > 0) {
|
||||
customWebViewCallback?.onCustomViewHidden()
|
||||
return@addCallback
|
||||
} else if (binding.webView.canGoBack()
|
||||
&& binding.webView.copyBackForwardList().size > 1
|
||||
) {
|
||||
binding.webView.goBack()
|
||||
return@addCallback
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("SwitchIntDef")
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
when (newConfig.orientation) {
|
||||
Configuration.ORIENTATION_LANDSCAPE -> {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
}
|
||||
|
||||
Configuration.ORIENTATION_PORTRAIT -> {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.rss_read, menu)
|
||||
return super.onCompatCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
starMenuItem = menu.findItem(R.id.menu_rss_star)
|
||||
ttsMenuItem = menu.findItem(R.id.menu_aloud)
|
||||
upStarMenu()
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
|
||||
menu.findItem(R.id.menu_login)?.isVisible = !viewModel.rssSource?.loginUrl.isNullOrBlank()
|
||||
return super.onMenuOpened(featureId, menu)
|
||||
}
|
||||
|
||||
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_rss_refresh -> viewModel.refresh {
|
||||
binding.webView.reload()
|
||||
}
|
||||
|
||||
R.id.menu_rss_star -> {
|
||||
viewModel.addFavorite()
|
||||
viewModel.rssArticle?.let {
|
||||
showDialogFragment(RssFavoritesDialog(it))
|
||||
}
|
||||
}
|
||||
|
||||
R.id.menu_share_it -> {
|
||||
binding.webView.url?.let {
|
||||
share(it)
|
||||
} ?: viewModel.rssArticle?.let {
|
||||
share(it.link)
|
||||
} ?: toastOnUi(R.string.null_url)
|
||||
}
|
||||
|
||||
R.id.menu_aloud -> readAloud()
|
||||
R.id.menu_login -> startActivity<SourceLoginActivity> {
|
||||
putExtra("type", "rssSource")
|
||||
putExtra("key", viewModel.rssSource?.sourceUrl)
|
||||
}
|
||||
|
||||
R.id.menu_browser_open -> binding.webView.url?.let {
|
||||
openUrl(it)
|
||||
} ?: toastOnUi("url null")
|
||||
}
|
||||
return super.onCompatOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun updateFavorite(title: String?, group: String?) {
|
||||
viewModel.rssArticle?.let {
|
||||
if (title != null) {
|
||||
it.title = title
|
||||
}
|
||||
if (group != null) {
|
||||
it.group = group
|
||||
}
|
||||
}
|
||||
viewModel.updateFavorite()
|
||||
}
|
||||
|
||||
override fun deleteFavorite() {
|
||||
viewModel.delFavorite()
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun isNightTheme(): Boolean {
|
||||
return AppConfig.isNightTheme
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
binding.root.setOnApplyWindowInsetsListenerCompat { view, windowInsets ->
|
||||
val typeMask = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime()
|
||||
val insets = windowInsets.getInsets(typeMask)
|
||||
view.bottomPadding = insets.bottom
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled", "JavascriptInterface")
|
||||
private fun initWebView() {
|
||||
//binding.progressBar.fontColor = accentColor
|
||||
binding.webView.webChromeClient = CustomWebChromeClient()
|
||||
binding.webView.webViewClient = CustomWebViewClient()
|
||||
binding.webView.settings.apply {
|
||||
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
||||
domStorageEnabled = true
|
||||
allowContentAccess = true
|
||||
builtInZoomControls = true
|
||||
displayZoomControls = false
|
||||
setDarkeningAllowed(AppConfig.isNightTheme)
|
||||
}
|
||||
binding.webView.addJavascriptInterface(this, "thisActivity")
|
||||
binding.webView.setOnLongClickListener {
|
||||
val hitTestResult = binding.webView.hitTestResult
|
||||
if (hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
|
||||
hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
|
||||
) {
|
||||
hitTestResult.extra?.let { webPic ->
|
||||
selector(
|
||||
arrayListOf(
|
||||
SelectItem(getString(R.string.action_save), "save"),
|
||||
SelectItem(getString(R.string.select_folder), "selectFolder")
|
||||
)
|
||||
) { _, charSequence, _ ->
|
||||
when (charSequence.value) {
|
||||
"save" -> saveImage(webPic)
|
||||
"selectFolder" -> selectSaveFolder(null)
|
||||
}
|
||||
}
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
}
|
||||
return@setOnLongClickListener false
|
||||
}
|
||||
binding.webView.setDownloadListener { url, _, contentDisposition, _, _ ->
|
||||
var fileName = URLUtil.guessFileName(url, contentDisposition, null)
|
||||
fileName = URLDecoder.decode(fileName, "UTF-8")
|
||||
binding.llView.longSnackbar(fileName, getString(R.string.action_download)) {
|
||||
Download.start(this, url, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun saveImage(webPic: String) {
|
||||
val path = ACache.get().getAsString(imagePathKey)
|
||||
if (path.isNullOrEmpty()) {
|
||||
selectSaveFolder(webPic)
|
||||
} else {
|
||||
viewModel.saveImage(webPic, Uri.parse(path))
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectSaveFolder(webPic: String?) {
|
||||
val default = arrayListOf<SelectItem<Int>>()
|
||||
val path = ACache.get().getAsString(imagePathKey)
|
||||
if (!path.isNullOrEmpty()) {
|
||||
default.add(SelectItem(path, -1))
|
||||
}
|
||||
selectImageDir.launch {
|
||||
otherActions = default
|
||||
value = webPic
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun initLiveData() {
|
||||
viewModel.contentLiveData.observe(this) { content ->
|
||||
viewModel.rssArticle?.let {
|
||||
upJavaScriptEnable()
|
||||
val url = NetworkUtils.getAbsoluteURL(it.origin, it.link)
|
||||
val html = viewModel.clHtml(content)
|
||||
binding.webView.settings.userAgentString =
|
||||
viewModel.headerMap[AppConst.UA_NAME] ?: AppConfig.userAgent
|
||||
if (viewModel.rssSource?.loadWithBaseUrl == true) {
|
||||
binding.webView
|
||||
.loadDataWithBaseURL(url, html, "text/html", "utf-8", url)//不想用baseUrl进else
|
||||
} else {
|
||||
binding.webView
|
||||
.loadDataWithBaseURL(null, html, "text/html;charset=utf-8", "utf-8", url)
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModel.urlLiveData.observe(this) {
|
||||
upJavaScriptEnable()
|
||||
CookieManager.applyToWebView(it.url)
|
||||
binding.webView.settings.userAgentString = it.getUserAgent()
|
||||
binding.webView.loadUrl(it.url, it.headerMap)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun upJavaScriptEnable() {
|
||||
if (viewModel.rssSource?.enableJs == true) {
|
||||
binding.webView.settings.javaScriptEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun upStarMenu() {
|
||||
starMenuItem?.isVisible = viewModel.rssArticle != null
|
||||
if (viewModel.rssStar != null) {
|
||||
starMenuItem?.setIcon(R.drawable.ic_star)
|
||||
starMenuItem?.setTitle(R.string.in_favorites)
|
||||
} else {
|
||||
starMenuItem?.setIcon(R.drawable.ic_star_border)
|
||||
starMenuItem?.setTitle(R.string.out_favorites)
|
||||
}
|
||||
//starMenuItem?.icon?.setTintMutate(primaryTextColor)
|
||||
}
|
||||
|
||||
private fun upTtsMenu(isPlaying: Boolean) {
|
||||
lifecycleScope.launch {
|
||||
if (isPlaying) {
|
||||
ttsMenuItem?.setIcon(R.drawable.ic_stop_black_24dp)
|
||||
ttsMenuItem?.setTitle(R.string.aloud_stop)
|
||||
} else {
|
||||
ttsMenuItem?.setIcon(R.drawable.ic_volume_up)
|
||||
ttsMenuItem?.setTitle(R.string.read_aloud)
|
||||
}
|
||||
//ttsMenuItem?.icon?.setTintMutate(primaryTextColor)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun readAloud() {
|
||||
if (viewModel.tts?.isSpeaking == true) {
|
||||
viewModel.tts?.stop()
|
||||
upTtsMenu(false)
|
||||
} else {
|
||||
binding.webView.settings.javaScriptEnabled = true
|
||||
binding.webView.evaluateJavascript("document.documentElement.outerHTML") {
|
||||
val html = StringEscapeUtils.unescapeJson(it)
|
||||
.replace("^\"|\"$".toRegex(), "")
|
||||
viewModel.readAloud(
|
||||
Jsoup.parse(html)
|
||||
.textArray()
|
||||
.joinToString("\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
binding.webView.destroy()
|
||||
}
|
||||
|
||||
inner class CustomWebChromeClient : WebChromeClient() {
|
||||
|
||||
override fun onProgressChanged(view: WebView?, newProgress: Int) {
|
||||
super.onProgressChanged(view, newProgress)
|
||||
binding.progressBar.setDurProgress(newProgress)
|
||||
binding.progressBar.gone(newProgress == 100)
|
||||
}
|
||||
|
||||
override fun onShowCustomView(view: View?, callback: CustomViewCallback?) {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
|
||||
binding.llView.invisible()
|
||||
binding.customWebView.addView(view)
|
||||
customWebViewCallback = callback
|
||||
keepScreenOn(true)
|
||||
toggleSystemBar(false)
|
||||
}
|
||||
|
||||
override fun onHideCustomView() {
|
||||
binding.customWebView.removeAllViews()
|
||||
binding.llView.visible()
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
keepScreenOn(false)
|
||||
toggleSystemBar(true)
|
||||
}
|
||||
}
|
||||
|
||||
inner class CustomWebViewClient : WebViewClient() {
|
||||
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): Boolean {
|
||||
return shouldOverrideUrlLoading(request.url)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION", "KotlinRedundantDiagnosticSuppress")
|
||||
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
||||
return shouldOverrideUrlLoading(Uri.parse(url))
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果有黑名单,黑名单匹配返回空白,
|
||||
* 没有黑名单再判断白名单,在白名单中的才通过,
|
||||
* 都没有不做处理
|
||||
*/
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): WebResourceResponse? {
|
||||
val url = request.url.toString()
|
||||
val source = viewModel.rssSource ?: return super.shouldInterceptRequest(view, request)
|
||||
val blacklist = source.contentBlacklist?.splitNotBlank(",")
|
||||
if (!blacklist.isNullOrEmpty()) {
|
||||
blacklist.forEach {
|
||||
try {
|
||||
if (url.startsWith(it) || url.matches(it.toRegex())) {
|
||||
return createEmptyResource()
|
||||
}
|
||||
} catch (e: PatternSyntaxException) {
|
||||
AppLog.put("黑名单规则正则语法错误 源名称:${source.sourceName} 正则:$it", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val whitelist = source.contentWhitelist?.splitNotBlank(",")
|
||||
if (!whitelist.isNullOrEmpty()) {
|
||||
whitelist.forEach {
|
||||
try {
|
||||
if (url.startsWith(it) || url.matches(it.toRegex())) {
|
||||
return super.shouldInterceptRequest(view, request)
|
||||
}
|
||||
} catch (e: PatternSyntaxException) {
|
||||
val msg = "白名单规则正则语法错误 源名称:${source.sourceName} 正则:$it"
|
||||
AppLog.put(msg, e)
|
||||
}
|
||||
}
|
||||
return createEmptyResource()
|
||||
}
|
||||
}
|
||||
return super.shouldInterceptRequest(view, request)
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
view.title?.let { title ->
|
||||
if (title != url && title != view.url && title.isNotBlank() && url != "about:blank") {
|
||||
binding.titleBar.title = title
|
||||
} else {
|
||||
binding.titleBar.title = intent.getStringExtra("title")
|
||||
}
|
||||
}
|
||||
viewModel.rssSource?.injectJs?.let {
|
||||
if (it.isNotBlank()) {
|
||||
view.evaluateJavascript(it, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createEmptyResource(): WebResourceResponse {
|
||||
return WebResourceResponse(
|
||||
"text/plain",
|
||||
"utf-8",
|
||||
ByteArrayInputStream("".toByteArray())
|
||||
)
|
||||
}
|
||||
|
||||
private fun shouldOverrideUrlLoading(url: Uri): Boolean {
|
||||
val source = viewModel.rssSource
|
||||
val js = source?.shouldOverrideUrlLoading
|
||||
if (!js.isNullOrBlank()) {
|
||||
val t = SystemClock.uptimeMillis()
|
||||
val result = kotlin.runCatching {
|
||||
runScriptWithContext(lifecycleScope.coroutineContext) {
|
||||
source.evalJS(js) {
|
||||
put("java", rssJsExtensions)
|
||||
put("url", url.toString())
|
||||
}.toString()
|
||||
}
|
||||
}.onFailure {
|
||||
AppLog.put("${source.getTag()}: url跳转拦截js出错", it)
|
||||
}.getOrNull()
|
||||
if (SystemClock.uptimeMillis() - t > 30) {
|
||||
AppLog.put("${source.getTag()}: url跳转拦截js执行耗时过长")
|
||||
}
|
||||
if (result.isTrue()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
when (url.scheme) {
|
||||
"http", "https", "jsbridge" -> {
|
||||
return false
|
||||
}
|
||||
|
||||
"legado", "yuedu" -> {
|
||||
startActivity<OnLineImportActivity> {
|
||||
data = url
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
else -> {
|
||||
binding.root.longSnackbar(R.string.jump_to_another_app, R.string.confirm) {
|
||||
openUrl(url)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("WebViewClientOnReceivedSslError")
|
||||
override fun onReceivedSslError(
|
||||
view: WebView?,
|
||||
handler: SslErrorHandler?,
|
||||
error: SslError?
|
||||
) {
|
||||
handler?.proceed()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package io.legato.kazusa.ui.rss.read
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import android.webkit.URLUtil
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.script.rhino.runScriptWithContext
|
||||
import io.legato.kazusa.base.BaseViewModel
|
||||
import io.legato.kazusa.constant.AppConst
|
||||
import io.legato.kazusa.data.appDb
|
||||
import io.legato.kazusa.data.entities.RssArticle
|
||||
import io.legato.kazusa.data.entities.RssSource
|
||||
import io.legato.kazusa.data.entities.RssStar
|
||||
import io.legato.kazusa.exception.NoStackTraceException
|
||||
import io.legato.kazusa.help.TTS
|
||||
import io.legato.kazusa.help.http.newCallResponseBody
|
||||
import io.legato.kazusa.help.http.okHttpClient
|
||||
import io.legato.kazusa.model.analyzeRule.AnalyzeUrl
|
||||
import io.legato.kazusa.model.rss.Rss
|
||||
import io.legato.kazusa.utils.toastOnUi
|
||||
import io.legato.kazusa.utils.writeBytes
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import splitties.init.appCtx
|
||||
import java.util.Date
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
|
||||
class ReadRssViewModel(application: Application) : BaseViewModel(application) {
|
||||
var rssSource: RssSource? = null
|
||||
var rssArticle: RssArticle? = null
|
||||
var tts: TTS? = null
|
||||
val contentLiveData = MutableLiveData<String>()
|
||||
val urlLiveData = MutableLiveData<AnalyzeUrl>()
|
||||
var rssStar: RssStar? = null
|
||||
val upTtsMenuData = MutableLiveData<Boolean>()
|
||||
val upStarMenuData = MutableLiveData<Boolean>()
|
||||
var headerMap: Map<String, String> = emptyMap()
|
||||
|
||||
fun initData(intent: Intent) {
|
||||
execute {
|
||||
val origin = intent.getStringExtra("origin") ?: return@execute
|
||||
val link = intent.getStringExtra("link")
|
||||
rssSource = appDb.rssSourceDao.getByKey(origin)
|
||||
headerMap = runScriptWithContext {
|
||||
rssSource?.getHeaderMap() ?: emptyMap()
|
||||
}
|
||||
if (link != null) {
|
||||
rssStar = appDb.rssStarDao.get(origin, link)
|
||||
rssArticle = rssStar?.toRssArticle() ?: appDb.rssArticleDao.get(origin, link)
|
||||
val rssArticle = rssArticle ?: return@execute
|
||||
if (!rssArticle.description.isNullOrBlank()) {
|
||||
contentLiveData.postValue(rssArticle.description!!)
|
||||
} else {
|
||||
rssSource?.let {
|
||||
val ruleContent = it.ruleContent
|
||||
if (!ruleContent.isNullOrBlank()) {
|
||||
loadContent(rssArticle, ruleContent)
|
||||
} else {
|
||||
loadUrl(rssArticle.link, rssArticle.origin)
|
||||
}
|
||||
} ?: loadUrl(rssArticle.link, rssArticle.origin)
|
||||
}
|
||||
} else {
|
||||
val ruleContent = rssSource?.ruleContent
|
||||
if (ruleContent.isNullOrBlank()) {
|
||||
loadUrl(origin, origin)
|
||||
} else {
|
||||
val rssArticle = RssArticle()
|
||||
rssArticle.origin = origin
|
||||
rssArticle.link = origin
|
||||
rssArticle.title = rssSource!!.sourceName
|
||||
loadContent(rssArticle, ruleContent)
|
||||
}
|
||||
}
|
||||
}.onFinally {
|
||||
upStarMenuData.postValue(true)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadUrl(url: String, baseUrl: String) {
|
||||
val analyzeUrl = AnalyzeUrl(
|
||||
mUrl = url,
|
||||
baseUrl = baseUrl,
|
||||
source = rssSource,
|
||||
coroutineContext = coroutineContext,
|
||||
hasLoginHeader = false
|
||||
)
|
||||
urlLiveData.postValue(analyzeUrl)
|
||||
}
|
||||
|
||||
private fun loadContent(rssArticle: RssArticle, ruleContent: String) {
|
||||
val source = rssSource ?: return
|
||||
Rss.getContent(viewModelScope, rssArticle, ruleContent, source)
|
||||
.onSuccess(IO) { body ->
|
||||
rssArticle.description = body
|
||||
appDb.rssArticleDao.insert(rssArticle)
|
||||
rssStar?.let {
|
||||
it.description = body
|
||||
appDb.rssStarDao.insert(it)
|
||||
}
|
||||
contentLiveData.postValue(body)
|
||||
}.onError {
|
||||
contentLiveData.postValue("加载正文失败\n${it.stackTraceToString()}")
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh(finish: () -> Unit) {
|
||||
rssArticle?.let { rssArticle ->
|
||||
rssSource?.let {
|
||||
val ruleContent = it.ruleContent
|
||||
if (!ruleContent.isNullOrBlank()) {
|
||||
loadContent(rssArticle, ruleContent)
|
||||
} else {
|
||||
finish.invoke()
|
||||
}
|
||||
} ?: let {
|
||||
appCtx.toastOnUi("订阅源不存在")
|
||||
finish.invoke()
|
||||
}
|
||||
} ?: finish.invoke()
|
||||
}
|
||||
|
||||
fun favorite() {
|
||||
execute {
|
||||
rssStar?.let {
|
||||
appDb.rssStarDao.delete(it.origin, it.link)
|
||||
rssStar = null
|
||||
} ?: rssArticle?.toStar()?.let {
|
||||
appDb.rssStarDao.insert(it)
|
||||
rssStar = it
|
||||
}
|
||||
}.onSuccess {
|
||||
upStarMenuData.postValue(true)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
val fileName = "${AppConst.fileNameFormat.format(Date(System.currentTimeMillis()))}.jpg"
|
||||
val byteArray = webData2bitmap(webPic) ?: throw NoStackTraceException("NULL")
|
||||
uri.writeBytes(context, fileName, byteArray)
|
||||
}.onError {
|
||||
context.toastOnUi("保存图片失败:${it.localizedMessage}")
|
||||
}.onSuccess {
|
||||
context.toastOnUi("保存成功")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun webData2bitmap(data: String): ByteArray? {
|
||||
return if (URLUtil.isValidUrl(data)) {
|
||||
okHttpClient.newCallResponseBody {
|
||||
url(data)
|
||||
}.bytes()
|
||||
} else {
|
||||
Base64.decode(data.split(",").toTypedArray()[1], Base64.DEFAULT)
|
||||
}
|
||||
}
|
||||
|
||||
fun clHtml(content: String): String {
|
||||
return when {
|
||||
!rssSource?.style.isNullOrEmpty() -> {
|
||||
"""
|
||||
<style>
|
||||
${rssSource?.style}
|
||||
</style>
|
||||
$content
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
content.contains("<style>".toRegex()) -> {
|
||||
content
|
||||
}
|
||||
|
||||
else -> {
|
||||
"""
|
||||
<style>
|
||||
img{max-width:100% !important; width:auto; height:auto;}
|
||||
video{object-fit:fill; max-width:100% !important; width:auto; height:auto;}
|
||||
body{word-wrap:break-word; height:auto;max-width: 100%; width:auto;}
|
||||
</style>
|
||||
$content
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun readAloud(text: String) {
|
||||
if (tts == null) {
|
||||
tts = TTS().apply {
|
||||
setSpeakStateListener(object : TTS.SpeakStateListener {
|
||||
override fun onStart() {
|
||||
upTtsMenuData.postValue(true)
|
||||
}
|
||||
|
||||
override fun onDone() {
|
||||
upTtsMenuData.postValue(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
tts?.speak(text)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
tts?.clearTts()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package io.legato.kazusa.ui.rss.read
|
||||
|
||||
import io.legato.kazusa.data.entities.BaseSource
|
||||
import io.legato.kazusa.help.JsExtensions
|
||||
import io.legato.kazusa.ui.association.AddToBookshelfDialog
|
||||
import io.legato.kazusa.ui.book.search.SearchActivity
|
||||
import io.legato.kazusa.utils.showDialogFragment
|
||||
|
||||
@Suppress("unused")
|
||||
class RssJsExtensions(private val activity: ReadRssActivity) : JsExtensions {
|
||||
|
||||
override fun getSource(): BaseSource? {
|
||||
return activity.getSource()
|
||||
}
|
||||
|
||||
fun searchBook(key: String) {
|
||||
SearchActivity.start(activity, key)
|
||||
}
|
||||
|
||||
fun addBook(bookUrl: String) {
|
||||
activity.showDialogFragment(AddToBookshelfDialog(bookUrl))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.legato.kazusa.ui.rss.read
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.webkit.WebView
|
||||
|
||||
class VisibleWebView(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : WebView(context, attrs) {
|
||||
|
||||
override fun onWindowVisibilityChanged(visibility: Int) {
|
||||
super.onWindowVisibilityChanged(View.VISIBLE)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package io.legato.kazusa.ui.rss.source.debug
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.SearchView
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.VMBaseActivity
|
||||
import io.legato.kazusa.databinding.ActivitySourceDebugBinding
|
||||
//import io.legado.app.lib.theme.accentColor
|
||||
//import io.legado.app.lib.theme.primaryColor
|
||||
import io.legato.kazusa.ui.widget.dialog.TextDialog
|
||||
import io.legato.kazusa.utils.applyNavigationBarPadding
|
||||
import io.legato.kazusa.utils.gone
|
||||
import io.legato.kazusa.utils.showDialogFragment
|
||||
import io.legato.kazusa.utils.toastOnUi
|
||||
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class RssSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, RssSourceDebugModel>() {
|
||||
|
||||
override val binding by viewBinding(ActivitySourceDebugBinding::inflate)
|
||||
override val viewModel by viewModels<RssSourceDebugModel>()
|
||||
|
||||
private val adapter by lazy { RssSourceDebugAdapter(this) }
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
initRecyclerView()
|
||||
initSearchView()
|
||||
viewModel.observe { state, msg ->
|
||||
lifecycleScope.launch {
|
||||
adapter.addItem(msg)
|
||||
if (state == -1 || state == 1000) {
|
||||
binding.rotateLoading.gone()
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModel.initData(intent.getStringExtra("key")) {
|
||||
startDebug()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.rss_source_debug, menu)
|
||||
return super.onCompatCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_list_src -> showDialogFragment(TextDialog("Html", viewModel.listSrc))
|
||||
R.id.menu_content_src -> showDialogFragment(TextDialog("Html", viewModel.contentSrc))
|
||||
}
|
||||
return super.onCompatOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun initRecyclerView() {
|
||||
// binding.recyclerView.setEdgeEffectColor(primaryColor)
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.recyclerView.applyNavigationBarPadding()
|
||||
// binding.rotateLoading.loadingColor = accentColor
|
||||
}
|
||||
|
||||
private fun initSearchView() {
|
||||
binding.titleBar.findViewById<SearchView>(R.id.search_view).gone()
|
||||
}
|
||||
|
||||
private fun startDebug() {
|
||||
adapter.clearItems()
|
||||
viewModel.rssSource?.let {
|
||||
binding.rotateLoading.visible()
|
||||
viewModel.startDebug(it)
|
||||
} ?: toastOnUi(R.string.error_no_source)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package io.legato.kazusa.ui.rss.source.debug
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.adapter.ItemViewHolder
|
||||
import io.legato.kazusa.base.adapter.RecyclerAdapter
|
||||
import io.legato.kazusa.databinding.ItemLogBinding
|
||||
|
||||
class RssSourceDebugAdapter(context: Context) :
|
||||
RecyclerAdapter<String, ItemLogBinding>(context) {
|
||||
|
||||
override fun getViewBinding(parent: ViewGroup): ItemLogBinding {
|
||||
return ItemLogBinding.inflate(inflater, parent, false)
|
||||
}
|
||||
|
||||
override fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemLogBinding,
|
||||
item: String,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
binding.apply {
|
||||
if (textView.getTag(R.id.tag1) == null) {
|
||||
val listener = object : View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(v: View) {
|
||||
textView.isCursorVisible = false
|
||||
textView.isCursorVisible = true
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(v: View) {}
|
||||
}
|
||||
textView.addOnAttachStateChangeListener(listener)
|
||||
textView.setTag(R.id.tag1, listener)
|
||||
}
|
||||
textView.text = item
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerListener(holder: ItemViewHolder, binding: ItemLogBinding) {
|
||||
//nothing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package io.legato.kazusa.ui.rss.source.debug
|
||||
|
||||
import android.app.Application
|
||||
import io.legato.kazusa.base.BaseViewModel
|
||||
import io.legato.kazusa.data.appDb
|
||||
import io.legato.kazusa.data.entities.RssSource
|
||||
import io.legato.kazusa.model.Debug
|
||||
|
||||
class RssSourceDebugModel(application: Application) : BaseViewModel(application),
|
||||
Debug.Callback {
|
||||
var rssSource: RssSource? = null
|
||||
private var callback: ((Int, String) -> Unit)? = null
|
||||
var listSrc: String? = null
|
||||
var contentSrc: String? = null
|
||||
|
||||
fun initData(sourceUrl: String?, finally: () -> Unit) {
|
||||
sourceUrl?.let {
|
||||
execute {
|
||||
rssSource = appDb.rssSourceDao.getByKey(sourceUrl)
|
||||
}.onFinally {
|
||||
finally()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun observe(callback: (Int, String) -> Unit) {
|
||||
this.callback = callback
|
||||
}
|
||||
|
||||
fun startDebug(source: RssSource) {
|
||||
execute {
|
||||
Debug.callback = this@RssSourceDebugModel
|
||||
Debug.startDebug(this, source)
|
||||
}
|
||||
}
|
||||
|
||||
override fun printLog(state: Int, msg: String) {
|
||||
when (state) {
|
||||
10 -> listSrc = msg
|
||||
20 -> contentSrc = msg
|
||||
else -> callback?.invoke(state, msg)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
Debug.cancelDebug(true)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
package io.legato.kazusa.ui.rss.source.edit
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.EditText
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.VMBaseActivity
|
||||
import io.legato.kazusa.data.entities.RssSource
|
||||
import io.legato.kazusa.databinding.ActivityRssSourceEditBinding
|
||||
import io.legato.kazusa.help.config.LocalConfig
|
||||
import io.legato.kazusa.lib.dialogs.SelectItem
|
||||
import io.legato.kazusa.lib.dialogs.alert
|
||||
//import io.legado.app.lib.theme.accentColor
|
||||
//import io.legado.app.lib.theme.backgroundColor
|
||||
//import io.legado.app.lib.theme.primaryColor
|
||||
import io.legato.kazusa.ui.file.HandleFileContract
|
||||
import io.legato.kazusa.ui.login.SourceLoginActivity
|
||||
import io.legato.kazusa.ui.qrcode.QrCodeResult
|
||||
import io.legato.kazusa.ui.rss.source.debug.RssSourceDebugActivity
|
||||
import io.legato.kazusa.ui.widget.dialog.UrlOptionDialog
|
||||
import io.legato.kazusa.ui.widget.dialog.VariableDialog
|
||||
import io.legato.kazusa.ui.widget.keyboard.KeyboardToolPop
|
||||
import io.legato.kazusa.ui.widget.text.EditEntity
|
||||
import io.legato.kazusa.utils.GSON
|
||||
import io.legato.kazusa.utils.imeHeight
|
||||
import io.legato.kazusa.utils.isContentScheme
|
||||
import io.legato.kazusa.utils.isTrue
|
||||
import io.legato.kazusa.utils.launch
|
||||
import io.legato.kazusa.utils.navigationBarHeight
|
||||
import io.legato.kazusa.utils.sendToClip
|
||||
import io.legato.kazusa.utils.setOnApplyWindowInsetsListenerCompat
|
||||
import io.legato.kazusa.utils.share
|
||||
import io.legato.kazusa.utils.shareWithQr
|
||||
import io.legato.kazusa.utils.showDialogFragment
|
||||
import io.legato.kazusa.utils.showHelp
|
||||
import io.legato.kazusa.utils.startActivity
|
||||
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import splitties.views.bottomPadding
|
||||
|
||||
class RssSourceEditActivity :
|
||||
VMBaseActivity<ActivityRssSourceEditBinding, RssSourceEditViewModel>(),
|
||||
KeyboardToolPop.CallBack,
|
||||
VariableDialog.Callback {
|
||||
|
||||
override val binding by viewBinding(ActivityRssSourceEditBinding::inflate)
|
||||
override val viewModel by viewModels<RssSourceEditViewModel>()
|
||||
private val softKeyboardTool by lazy {
|
||||
KeyboardToolPop(this, lifecycleScope, binding.root, this)
|
||||
}
|
||||
private val adapter by lazy { RssSourceEditAdapter() }
|
||||
private val sourceEntities: ArrayList<EditEntity> = ArrayList()
|
||||
private val listEntities: ArrayList<EditEntity> = ArrayList()
|
||||
private val webViewEntities: ArrayList<EditEntity> = ArrayList()
|
||||
private val selectDoc = registerForActivityResult(HandleFileContract()) {
|
||||
it.uri?.let { uri ->
|
||||
if (uri.isContentScheme()) {
|
||||
sendText(uri.toString())
|
||||
} else {
|
||||
sendText(uri.path.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
|
||||
it?.let {
|
||||
viewModel.importSource(it) { source: RssSource ->
|
||||
upSourceView(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
softKeyboardTool.attachToWindow(window)
|
||||
initView()
|
||||
viewModel.initData(intent) {
|
||||
upSourceView(viewModel.rssSource)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
super.onPostCreate(savedInstanceState)
|
||||
if (!LocalConfig.ruleHelpVersionIsLast) {
|
||||
showHelp("ruleHelp")
|
||||
}
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
val source = getRssSource()
|
||||
if (!source.equal(viewModel.rssSource ?: RssSource())) {
|
||||
alert(R.string.exit) {
|
||||
setMessage(R.string.exit_no_save)
|
||||
positiveButton(R.string.yes)
|
||||
negativeButton(R.string.no) {
|
||||
super.finish()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
softKeyboardTool.dismiss()
|
||||
}
|
||||
|
||||
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.source_edit, menu)
|
||||
return super.onCompatCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
|
||||
menu.findItem(R.id.menu_login)?.isVisible = !viewModel.rssSource?.loginUrl.isNullOrBlank()
|
||||
menu.findItem(R.id.menu_auto_complete)?.isChecked = viewModel.autoComplete
|
||||
return super.onMenuOpened(featureId, menu)
|
||||
}
|
||||
|
||||
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_save -> viewModel.save(getRssSource()) {
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
|
||||
R.id.menu_debug_source -> viewModel.save(getRssSource()) { source ->
|
||||
startActivity<RssSourceDebugActivity> {
|
||||
putExtra("key", source.sourceUrl)
|
||||
}
|
||||
}
|
||||
|
||||
R.id.menu_login -> viewModel.save(getRssSource()) {
|
||||
startActivity<SourceLoginActivity> {
|
||||
putExtra("type", "rssSource")
|
||||
putExtra("key", it.sourceUrl)
|
||||
}
|
||||
}
|
||||
|
||||
R.id.menu_set_source_variable -> setSourceVariable()
|
||||
R.id.menu_clear_cookie -> viewModel.clearCookie(getRssSource().sourceUrl)
|
||||
R.id.menu_auto_complete -> viewModel.autoComplete = !viewModel.autoComplete
|
||||
R.id.menu_copy_source -> sendToClip(GSON.toJson(getRssSource()))
|
||||
R.id.menu_qr_code_camera -> qrCodeResult.launch()
|
||||
R.id.menu_paste_source -> viewModel.pasteSource { upSourceView(it) }
|
||||
R.id.menu_share_str -> share(GSON.toJson(getRssSource()))
|
||||
R.id.menu_share_qr -> shareWithQr(
|
||||
GSON.toJson(getRssSource()),
|
||||
getString(R.string.share_rss_source),
|
||||
ErrorCorrectionLevel.L
|
||||
)
|
||||
|
||||
R.id.menu_help -> showHelp("ruleHelp")
|
||||
}
|
||||
return super.onCompatOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
binding.tabLayout.addTab(binding.tabLayout.newTab().apply {
|
||||
setText(R.string.source_tab_base)
|
||||
})
|
||||
binding.tabLayout.addTab(binding.tabLayout.newTab().apply {
|
||||
setText(R.string.source_tab_list)
|
||||
})
|
||||
binding.tabLayout.addTab(binding.tabLayout.newTab().apply {
|
||||
text = "WEB_VIEW"
|
||||
})
|
||||
// binding.recyclerView.setEdgeEffectColor(primaryColor)
|
||||
binding.recyclerView.adapter = adapter
|
||||
// binding.tabLayout.setBackgroundColor(backgroundColor)
|
||||
// binding.tabLayout.setSelectedTabIndicatorColor(accentColor)
|
||||
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
setEditEntities(tab?.position)
|
||||
}
|
||||
})
|
||||
binding.recyclerView.setOnApplyWindowInsetsListenerCompat { view, windowInsets ->
|
||||
val navigationBarHeight = windowInsets.navigationBarHeight
|
||||
val imeHeight = windowInsets.imeHeight
|
||||
view.bottomPadding = if (imeHeight == 0) navigationBarHeight else 0
|
||||
softKeyboardTool.initialPadding = imeHeight
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
|
||||
private fun setEditEntities(tabPosition: Int?) {
|
||||
when (tabPosition) {
|
||||
1 -> adapter.editEntities = listEntities
|
||||
2 -> adapter.editEntities = webViewEntities
|
||||
else -> adapter.editEntities = sourceEntities
|
||||
}
|
||||
binding.recyclerView.scrollToPosition(0)
|
||||
}
|
||||
|
||||
private fun upSourceView(rssSource: RssSource?) {
|
||||
val rs = rssSource ?: RssSource()
|
||||
rs.let {
|
||||
binding.cbIsEnable.isChecked = rs.enabled
|
||||
binding.cbSingleUrl.isChecked = rs.singleUrl
|
||||
binding.cbIsEnableCookie.isChecked = rs.enabledCookieJar == true
|
||||
}
|
||||
sourceEntities.clear()
|
||||
sourceEntities.apply {
|
||||
add(EditEntity("sourceName", rs.sourceName, R.string.source_name))
|
||||
add(EditEntity("sourceUrl", rs.sourceUrl, R.string.source_url))
|
||||
add(EditEntity("sourceIcon", rs.sourceIcon, R.string.source_icon))
|
||||
add(EditEntity("sourceGroup", rs.sourceGroup, R.string.source_group))
|
||||
add(EditEntity("sourceComment", rs.sourceComment, R.string.comment))
|
||||
add(EditEntity("sortUrl", rs.sortUrl, R.string.sort_url))
|
||||
add(EditEntity("loginUrl", rs.loginUrl, R.string.login_url))
|
||||
add(EditEntity("loginUi", rs.loginUi, R.string.login_ui))
|
||||
add(EditEntity("loginCheckJs", rs.loginCheckJs, R.string.login_check_js))
|
||||
add(EditEntity("coverDecodeJs", rs.coverDecodeJs, R.string.cover_decode_js))
|
||||
add(EditEntity("header", rs.header, R.string.source_http_header))
|
||||
add(EditEntity("variableComment", rs.variableComment, R.string.variable_comment))
|
||||
add(EditEntity("concurrentRate", rs.concurrentRate, R.string.concurrent_rate))
|
||||
add(EditEntity("jsLib", rs.jsLib, "jsLib"))
|
||||
}
|
||||
listEntities.clear()
|
||||
listEntities.apply {
|
||||
add(EditEntity("ruleArticles", rs.ruleArticles, R.string.r_articles))
|
||||
add(EditEntity("ruleNextPage", rs.ruleNextPage, R.string.r_next))
|
||||
add(EditEntity("ruleTitle", rs.ruleTitle, R.string.r_title))
|
||||
add(EditEntity("rulePubDate", rs.rulePubDate, R.string.r_date))
|
||||
add(EditEntity("ruleDescription", rs.ruleDescription, R.string.r_description))
|
||||
add(EditEntity("ruleImage", rs.ruleImage, R.string.r_image))
|
||||
add(EditEntity("ruleLink", rs.ruleLink, R.string.r_link))
|
||||
}
|
||||
webViewEntities.clear()
|
||||
webViewEntities.apply {
|
||||
add(
|
||||
EditEntity(
|
||||
"enableJs",
|
||||
rs.enableJs.toString(),
|
||||
R.string.enable_js,
|
||||
EditEntity.ViewType.checkBox
|
||||
)
|
||||
)
|
||||
add(
|
||||
EditEntity(
|
||||
"loadWithBaseUrl",
|
||||
rs.loadWithBaseUrl.toString(),
|
||||
R.string.load_with_base_url,
|
||||
EditEntity.ViewType.checkBox
|
||||
)
|
||||
)
|
||||
add(EditEntity("ruleContent", rs.ruleContent, R.string.r_content))
|
||||
add(EditEntity("style", rs.style, R.string.r_style))
|
||||
add(EditEntity("injectJs", rs.injectJs, R.string.r_inject_js))
|
||||
add(EditEntity("contentWhitelist", rs.contentWhitelist, R.string.c_whitelist))
|
||||
add(EditEntity("contentBlacklist", rs.contentBlacklist, R.string.c_blacklist))
|
||||
add(
|
||||
EditEntity(
|
||||
"shouldOverrideUrlLoading",
|
||||
rs.shouldOverrideUrlLoading,
|
||||
"url跳转拦截(js, 返回true拦截,js变量url,可以通过js打开url,比如调用阅读搜索,添加书架等,简化规则写法,不用webView js注入)"
|
||||
)
|
||||
)
|
||||
}
|
||||
binding.tabLayout.selectTab(binding.tabLayout.getTabAt(0))
|
||||
setEditEntities(0)
|
||||
}
|
||||
|
||||
private fun getRssSource(): RssSource {
|
||||
val source = viewModel.rssSource?.copy() ?: RssSource()
|
||||
source.enabled = binding.cbIsEnable.isChecked
|
||||
source.singleUrl = binding.cbSingleUrl.isChecked
|
||||
source.enabledCookieJar = binding.cbIsEnableCookie.isChecked
|
||||
sourceEntities.forEach {
|
||||
it.value = it.value?.takeIf { s -> s.isNotBlank() }
|
||||
when (it.key) {
|
||||
"sourceName" -> source.sourceName = it.value ?: ""
|
||||
"sourceUrl" -> source.sourceUrl = it.value ?: ""
|
||||
"sourceIcon" -> source.sourceIcon = it.value ?: ""
|
||||
"sourceGroup" -> source.sourceGroup = it.value
|
||||
"sourceComment" -> source.sourceComment = it.value
|
||||
"loginUrl" -> source.loginUrl = it.value
|
||||
"loginUi" -> source.loginUi = it.value
|
||||
"loginCheckJs" -> source.loginCheckJs = it.value
|
||||
"coverDecodeJs" -> source.coverDecodeJs = it.value
|
||||
"header" -> source.header = it.value
|
||||
"variableComment" -> source.variableComment = it.value
|
||||
"concurrentRate" -> source.concurrentRate = it.value
|
||||
"sortUrl" -> source.sortUrl = it.value
|
||||
"jsLib" -> source.jsLib = it.value
|
||||
}
|
||||
}
|
||||
listEntities.forEach {
|
||||
it.value = it.value?.takeIf { s -> s.isNotBlank() }
|
||||
when (it.key) {
|
||||
"ruleArticles" -> source.ruleArticles = it.value
|
||||
"ruleNextPage" -> source.ruleNextPage =
|
||||
viewModel.ruleComplete(it.value, source.ruleArticles, 2)
|
||||
|
||||
"ruleTitle" -> source.ruleTitle =
|
||||
viewModel.ruleComplete(it.value, source.ruleArticles)
|
||||
|
||||
"rulePubDate" -> source.rulePubDate =
|
||||
viewModel.ruleComplete(it.value, source.ruleArticles)
|
||||
|
||||
"ruleDescription" -> source.ruleDescription =
|
||||
viewModel.ruleComplete(it.value, source.ruleArticles)
|
||||
|
||||
"ruleImage" -> source.ruleImage =
|
||||
viewModel.ruleComplete(it.value, source.ruleArticles, 3)
|
||||
|
||||
"ruleLink" -> source.ruleLink =
|
||||
viewModel.ruleComplete(it.value, source.ruleArticles)
|
||||
}
|
||||
}
|
||||
webViewEntities.forEach {
|
||||
it.value = it.value?.takeIf { s -> s.isNotBlank() }
|
||||
when (it.key) {
|
||||
"enableJs" -> source.enableJs = it.value.isTrue()
|
||||
"loadWithBaseUrl" -> source.loadWithBaseUrl = it.value.isTrue()
|
||||
"ruleContent" -> source.ruleContent =
|
||||
viewModel.ruleComplete(it.value, source.ruleArticles)
|
||||
|
||||
"style" -> source.style = it.value
|
||||
"injectJs" -> source.injectJs = it.value
|
||||
"contentWhitelist" -> source.contentWhitelist = it.value
|
||||
"contentBlacklist" -> source.contentBlacklist = it.value
|
||||
"shouldOverrideUrlLoading" -> source.shouldOverrideUrlLoading = it.value
|
||||
}
|
||||
}
|
||||
return source
|
||||
}
|
||||
|
||||
private fun setSourceVariable() {
|
||||
viewModel.save(getRssSource()) { source ->
|
||||
lifecycleScope.launch {
|
||||
val comment =
|
||||
source.getDisplayVariableComment("源变量可在js中通过source.getVariable()获取")
|
||||
val variable = withContext(Dispatchers.IO) { source.getVariable() }
|
||||
showDialogFragment(
|
||||
VariableDialog(
|
||||
getString(R.string.set_source_variable),
|
||||
source.getKey(),
|
||||
variable,
|
||||
comment
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setVariable(key: String, variable: String?) {
|
||||
viewModel.rssSource?.setVariable(variable)
|
||||
}
|
||||
|
||||
override fun helpActions(): List<SelectItem<String>> {
|
||||
return arrayListOf(
|
||||
SelectItem("插入URL参数", "urlOption"),
|
||||
SelectItem("订阅源教程", "ruleHelp"),
|
||||
SelectItem("js教程", "jsHelp"),
|
||||
SelectItem("正则教程", "regexHelp"),
|
||||
SelectItem("选择文件", "selectFile"),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onHelpActionSelect(action: String) {
|
||||
when (action) {
|
||||
"urlOption" -> UrlOptionDialog(this) {
|
||||
sendText(it)
|
||||
}.show()
|
||||
|
||||
"ruleHelp" -> showHelp("ruleHelp")
|
||||
"jsHelp" -> showHelp("jsHelp")
|
||||
"regexHelp" -> showHelp("regexHelp")
|
||||
"selectFile" -> selectDoc.launch {
|
||||
mode = HandleFileContract.FILE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendText(text: String) {
|
||||
if (text.isBlank()) return
|
||||
val view = window.decorView.findFocus()
|
||||
if (view is EditText) {
|
||||
val start = view.selectionStart
|
||||
val end = view.selectionEnd
|
||||
val edit = view.editableText//获取EditText的文字
|
||||
if (start < 0 || start >= edit.length) {
|
||||
edit.append(text)
|
||||
} else {
|
||||
edit.replace(start, end, text)//光标所在位置插入文字
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package io.legato.kazusa.ui.rss.source.edit
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.databinding.ItemSourceEditBinding
|
||||
import io.legato.kazusa.databinding.ItemSourceEditCheckBoxBinding
|
||||
import io.legato.kazusa.help.config.AppConfig
|
||||
import io.legato.kazusa.ui.widget.code.addJsPattern
|
||||
import io.legato.kazusa.ui.widget.code.addJsonPattern
|
||||
import io.legato.kazusa.ui.widget.code.addLegadoPattern
|
||||
import io.legato.kazusa.ui.widget.text.EditEntity
|
||||
import io.legato.kazusa.utils.isTrue
|
||||
|
||||
class RssSourceEditAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
val editEntityMaxLine = AppConfig.sourceEditMaxLine
|
||||
|
||||
var editEntities: ArrayList<EditEntity> = ArrayList()
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val item = editEntities[position]
|
||||
return item.viewType
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
EditEntity.ViewType.checkBox -> {
|
||||
val binding = ItemSourceEditCheckBoxBinding
|
||||
.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
CheckBoxViewHolder(binding)
|
||||
}
|
||||
else -> {
|
||||
val binding = ItemSourceEditBinding
|
||||
.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
binding.editText.addLegadoPattern()
|
||||
binding.editText.addJsonPattern()
|
||||
binding.editText.addJsPattern()
|
||||
EditTextViewHolder(binding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is CheckBoxViewHolder -> holder.bind(editEntities[position])
|
||||
is EditTextViewHolder -> holder.bind(editEntities[position])
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return editEntities.size
|
||||
}
|
||||
|
||||
inner class EditTextViewHolder(val binding: ItemSourceEditBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(editEntity: EditEntity) = binding.run {
|
||||
editText.maxLines = editEntityMaxLine
|
||||
if (editText.getTag(R.id.tag1) == null) {
|
||||
val listener = object : View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(v: View) {
|
||||
editText.isCursorVisible = false
|
||||
editText.isCursorVisible = true
|
||||
editText.isFocusable = true
|
||||
editText.isFocusableInTouchMode = true
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(v: View) {
|
||||
|
||||
}
|
||||
}
|
||||
editText.addOnAttachStateChangeListener(listener)
|
||||
editText.setTag(R.id.tag1, listener)
|
||||
}
|
||||
editText.getTag(R.id.tag2)?.let {
|
||||
if (it is TextWatcher) {
|
||||
editText.removeTextChangedListener(it)
|
||||
}
|
||||
}
|
||||
editText.setText(editEntity.value)
|
||||
textInputLayout.hint = editEntity.hint
|
||||
val textWatcher = object : TextWatcher {
|
||||
override fun beforeTextChanged(
|
||||
s: CharSequence,
|
||||
start: Int,
|
||||
count: Int,
|
||||
after: Int
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
|
||||
}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
editEntity.value = (s?.toString())
|
||||
}
|
||||
}
|
||||
editText.addTextChangedListener(textWatcher)
|
||||
editText.setTag(R.id.tag2, textWatcher)
|
||||
}
|
||||
}
|
||||
|
||||
inner class CheckBoxViewHolder(val binding: ItemSourceEditCheckBoxBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(editEntity: EditEntity) = binding.run {
|
||||
checkBox.setOnCheckedChangeListener(null)
|
||||
checkBox.text = editEntity.hint
|
||||
checkBox.isChecked = editEntity.value.isTrue()
|
||||
checkBox.setOnCheckedChangeListener { _, isChecked ->
|
||||
editEntity.value = isChecked.toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package io.legato.kazusa.ui.rss.source.edit
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.BaseViewModel
|
||||
import io.legato.kazusa.data.appDb
|
||||
import io.legato.kazusa.data.entities.RssSource
|
||||
import io.legato.kazusa.exception.NoStackTraceException
|
||||
import io.legato.kazusa.help.AppCacheManager
|
||||
import io.legato.kazusa.help.RuleComplete
|
||||
import io.legato.kazusa.help.http.CookieStore
|
||||
import io.legato.kazusa.utils.GSON
|
||||
import io.legato.kazusa.utils.fromJsonObject
|
||||
import io.legato.kazusa.utils.getClipText
|
||||
import io.legato.kazusa.utils.printOnDebug
|
||||
import io.legato.kazusa.utils.stackTraceStr
|
||||
import io.legato.kazusa.utils.toastOnUi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
|
||||
class RssSourceEditViewModel(application: Application) : BaseViewModel(application) {
|
||||
var autoComplete = false
|
||||
var rssSource: RssSource? = null
|
||||
|
||||
fun initData(intent: Intent, onFinally: () -> Unit) {
|
||||
execute {
|
||||
val key = intent.getStringExtra("sourceUrl")
|
||||
if (key != null) {
|
||||
appDb.rssSourceDao.getByKey(key)?.let {
|
||||
rssSource = it
|
||||
}
|
||||
}
|
||||
}.onFinally {
|
||||
onFinally()
|
||||
}
|
||||
}
|
||||
|
||||
fun save(source: RssSource, success: ((RssSource) -> Unit)) {
|
||||
execute {
|
||||
if (source.sourceName.isBlank() || source.sourceName.isBlank()) {
|
||||
throw NoStackTraceException(context.getString(R.string.non_null_name_url))
|
||||
}
|
||||
rssSource?.let {
|
||||
appDb.rssSourceDao.delete(it)
|
||||
//更新收藏的源地址
|
||||
if (it.sourceUrl != source.sourceUrl) {
|
||||
appDb.rssStarDao.updateOrigin(source.sourceUrl, it.sourceUrl)
|
||||
appDb.rssArticleDao.updateOrigin(source.sourceUrl, it.sourceUrl)
|
||||
appDb.cacheDao.deleteSourceVariables(it.sourceUrl)
|
||||
AppCacheManager.clearSourceVariables()
|
||||
}
|
||||
}
|
||||
appDb.rssSourceDao.insert(source)
|
||||
rssSource = source
|
||||
source
|
||||
}.onSuccess {
|
||||
success(it)
|
||||
}.onError {
|
||||
context.toastOnUi(it.localizedMessage)
|
||||
it.printOnDebug()
|
||||
}
|
||||
}
|
||||
|
||||
fun pasteSource(onSuccess: (source: RssSource) -> Unit) {
|
||||
execute(context = Dispatchers.Main) {
|
||||
var source: RssSource? = null
|
||||
context.getClipText()?.let { json ->
|
||||
source = GSON.fromJsonObject<RssSource>(json).getOrThrow()
|
||||
}
|
||||
source
|
||||
}.onError {
|
||||
context.toastOnUi(it.localizedMessage)
|
||||
}.onSuccess {
|
||||
if (it != null) {
|
||||
onSuccess(it)
|
||||
} else {
|
||||
context.toastOnUi("格式不对")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun importSource(text: String, finally: (source: RssSource) -> Unit) {
|
||||
execute {
|
||||
val text1 = text.trim()
|
||||
GSON.fromJsonObject<RssSource>(text1).getOrThrow().let {
|
||||
finally.invoke(it)
|
||||
}
|
||||
}.onError {
|
||||
context.toastOnUi(it.stackTraceStr)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCookie(url: String) {
|
||||
execute {
|
||||
CookieStore.removeCookie(url)
|
||||
}
|
||||
}
|
||||
|
||||
fun ruleComplete(rule: String?, preRule: String? = null, type: Int = 1): String? {
|
||||
if (autoComplete) {
|
||||
return RuleComplete.autoComplete(rule, preRule, type)
|
||||
}
|
||||
return rule
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package io.legato.kazusa.ui.rss.source.manage
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.BaseBottomSheetDialogFragment
|
||||
import io.legato.kazusa.base.adapter.ItemViewHolder
|
||||
import io.legato.kazusa.base.adapter.RecyclerAdapter
|
||||
import io.legato.kazusa.data.appDb
|
||||
import io.legato.kazusa.databinding.DialogEditTextBinding
|
||||
import io.legato.kazusa.databinding.DialogRecyclerViewBinding
|
||||
import io.legato.kazusa.databinding.ItemGroupManageBinding
|
||||
import io.legato.kazusa.lib.dialogs.alert
|
||||
//import io.legado.app.lib.theme.accentColor
|
||||
//import io.legado.app.lib.theme.backgroundColor
|
||||
//import io.legado.app.lib.theme.primaryColor
|
||||
import io.legato.kazusa.ui.widget.recycler.VerticalDivider
|
||||
import io.legato.kazusa.utils.applyTint
|
||||
import io.legato.kazusa.utils.requestInputMethod
|
||||
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
|
||||
import io.legato.kazusa.utils.visible
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class GroupManageDialog : BaseBottomSheetDialogFragment(R.layout.dialog_recycler_view),
|
||||
Toolbar.OnMenuItemClickListener {
|
||||
|
||||
private val viewModel: RssSourceViewModel by activityViewModels()
|
||||
private val binding by viewBinding(DialogRecyclerViewBinding::bind)
|
||||
private val adapter by lazy { GroupAdapter(requireContext()) }
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
}
|
||||
|
||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) = binding.run {
|
||||
//toolBar.setBackgroundColor(primaryColor)
|
||||
toolBar.title = getString(R.string.group_manage)
|
||||
toolBar.inflateMenu(R.menu.group_manage)
|
||||
toolBar.menu.applyTint(requireContext())
|
||||
toolBar.setOnMenuItemClickListener(this@GroupManageDialog)
|
||||
recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
recyclerView.addItemDecoration(VerticalDivider(requireContext()))
|
||||
recyclerView.adapter = adapter
|
||||
//tvOk.setTextColor(requireContext().accentColor)
|
||||
tvOk.visible()
|
||||
tvOk.setOnClickListener {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
initData()
|
||||
}
|
||||
|
||||
private fun initData() {
|
||||
lifecycleScope.launch {
|
||||
appDb.rssSourceDao.flowGroups().conflate().collect {
|
||||
adapter.setItems(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||
when (item?.itemId) {
|
||||
R.id.menu_add -> addGroup()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun addGroup() {
|
||||
alert(title = getString(R.string.add_group)) {
|
||||
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
|
||||
editView.setHint(R.string.group_name)
|
||||
}
|
||||
customView { alertBinding.root }
|
||||
okButton {
|
||||
alertBinding.editView.text?.toString()?.let {
|
||||
if (it.isNotBlank()) {
|
||||
viewModel.addGroup(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
cancelButton()
|
||||
}.requestInputMethod()
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun editGroup(group: String) {
|
||||
alert(title = getString(R.string.group_edit)) {
|
||||
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
|
||||
editView.setHint(R.string.group_name)
|
||||
editView.setText(group)
|
||||
}
|
||||
customView { alertBinding.root }
|
||||
okButton {
|
||||
viewModel.upGroup(group, alertBinding.editView.text?.toString())
|
||||
}
|
||||
cancelButton()
|
||||
}.requestInputMethod()
|
||||
}
|
||||
|
||||
private inner class GroupAdapter(context: Context) :
|
||||
RecyclerAdapter<String, ItemGroupManageBinding>(context) {
|
||||
|
||||
override fun getViewBinding(parent: ViewGroup): ItemGroupManageBinding {
|
||||
return ItemGroupManageBinding.inflate(inflater, parent, false)
|
||||
}
|
||||
|
||||
override fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemGroupManageBinding,
|
||||
item: String,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
binding.run {
|
||||
//root.setBackgroundColor(context.backgroundColor)
|
||||
tvGroup.text = item
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerListener(holder: ItemViewHolder, binding: ItemGroupManageBinding) {
|
||||
binding.apply {
|
||||
tvEdit.setOnClickListener {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
editGroup(it)
|
||||
}
|
||||
}
|
||||
|
||||
tvDel.setOnClickListener {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
viewModel.delGroup(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
package io.legato.kazusa.ui.rss.source.manage
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.SubMenu
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.VMBaseActivity
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.data.appDb
|
||||
import io.legato.kazusa.data.entities.RssSource
|
||||
import io.legato.kazusa.databinding.ActivityRssSourceBinding
|
||||
import io.legato.kazusa.databinding.DialogEditTextBinding
|
||||
import io.legato.kazusa.help.DirectLinkUpload
|
||||
import io.legato.kazusa.lib.dialogs.alert
|
||||
//import io.legado.app.lib.theme.primaryColor
|
||||
//import io.legado.app.lib.theme.primaryTextColor
|
||||
import io.legato.kazusa.ui.association.ImportRssSourceDialog
|
||||
import io.legato.kazusa.ui.file.HandleFileContract
|
||||
import io.legato.kazusa.ui.qrcode.QrCodeResult
|
||||
import io.legato.kazusa.ui.rss.source.edit.RssSourceEditActivity
|
||||
import io.legato.kazusa.ui.widget.SelectActionBar
|
||||
import io.legato.kazusa.ui.widget.recycler.DragSelectTouchHelper
|
||||
import io.legato.kazusa.ui.widget.recycler.ItemTouchCallback
|
||||
import io.legato.kazusa.ui.widget.recycler.VerticalDivider
|
||||
import io.legato.kazusa.utils.ACache
|
||||
import io.legato.kazusa.utils.dpToPx
|
||||
import io.legato.kazusa.utils.hideSoftInput
|
||||
import io.legato.kazusa.utils.isAbsUrl
|
||||
import io.legato.kazusa.utils.launch
|
||||
import io.legato.kazusa.utils.readText
|
||||
import io.legato.kazusa.utils.sendToClip
|
||||
import io.legato.kazusa.utils.share
|
||||
import io.legato.kazusa.utils.shouldHideSoftInput
|
||||
import io.legato.kazusa.utils.showDialogFragment
|
||||
import io.legato.kazusa.utils.showHelp
|
||||
import io.legato.kazusa.utils.splitNotBlank
|
||||
import io.legato.kazusa.utils.startActivity
|
||||
import io.legato.kazusa.utils.toastOnUi
|
||||
import io.legato.kazusa.utils.transaction
|
||||
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 订阅源管理
|
||||
*/
|
||||
class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceViewModel>(),
|
||||
PopupMenu.OnMenuItemClickListener,
|
||||
SelectActionBar.CallBack,
|
||||
RssSourceAdapter.CallBack {
|
||||
|
||||
override val binding by viewBinding(ActivityRssSourceBinding::inflate)
|
||||
override val viewModel by viewModels<RssSourceViewModel>()
|
||||
private val importRecordKey = "rssSourceRecordKey"
|
||||
private val adapter by lazy { RssSourceAdapter(this, this) }
|
||||
private val searchView: SearchView by lazy {
|
||||
binding.titleBar.findViewById(R.id.search_view)
|
||||
}
|
||||
private var sourceFlowJob: Job? = null
|
||||
private var groups = hashSetOf<String>()
|
||||
private var groupMenu: SubMenu? = null
|
||||
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
|
||||
it ?: return@registerForActivityResult
|
||||
showDialogFragment(
|
||||
ImportRssSourceDialog(it)
|
||||
)
|
||||
}
|
||||
private val importDoc = registerForActivityResult(HandleFileContract()) {
|
||||
kotlin.runCatching {
|
||||
it.uri?.readText(this)?.let {
|
||||
showDialogFragment(
|
||||
ImportRssSourceDialog(it)
|
||||
)
|
||||
}
|
||||
}.onFailure {
|
||||
toastOnUi("readTextError:${it.localizedMessage}")
|
||||
}
|
||||
}
|
||||
private val exportResult = registerForActivityResult(HandleFileContract()) {
|
||||
it.uri?.let { uri ->
|
||||
alert(R.string.export_success) {
|
||||
if (uri.toString().isAbsUrl()) {
|
||||
setMessage(DirectLinkUpload.getSummary())
|
||||
}
|
||||
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
|
||||
editView.hint = getString(R.string.path)
|
||||
editView.setText(uri.toString())
|
||||
}
|
||||
customView { alertBinding.root }
|
||||
okButton {
|
||||
sendToClip(uri.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
initRecyclerView()
|
||||
initSearchView()
|
||||
initGroupFlow()
|
||||
upSourceFlow()
|
||||
initSelectActionBar()
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
||||
if (ev.action == MotionEvent.ACTION_DOWN) {
|
||||
currentFocus?.let {
|
||||
if (it.shouldHideSoftInput(ev)) {
|
||||
it.post {
|
||||
it.clearFocus()
|
||||
it.hideSoftInput()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.rss_source, menu)
|
||||
return super.onCompatCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
groupMenu = menu.findItem(R.id.menu_group)?.subMenu
|
||||
upGroupMenu()
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_add -> startActivity<RssSourceEditActivity>()
|
||||
R.id.menu_import_local -> importDoc.launch {
|
||||
mode = HandleFileContract.FILE
|
||||
allowExtensions = arrayOf("txt", "json")
|
||||
}
|
||||
|
||||
R.id.menu_import_onLine -> showImportDialog()
|
||||
R.id.menu_import_qr -> qrCodeResult.launch()
|
||||
R.id.menu_group_manage -> showDialogFragment<GroupManageDialog>()
|
||||
R.id.menu_import_default -> viewModel.importDefault()
|
||||
R.id.menu_enabled_group -> {
|
||||
searchView.setQuery(getString(R.string.enabled), true)
|
||||
}
|
||||
|
||||
R.id.menu_disabled_group -> {
|
||||
searchView.setQuery(getString(R.string.disabled), true)
|
||||
}
|
||||
|
||||
R.id.menu_group_login -> {
|
||||
searchView.setQuery(getString(R.string.need_login), true)
|
||||
}
|
||||
|
||||
R.id.menu_group_null -> {
|
||||
searchView.setQuery(getString(R.string.no_group), true)
|
||||
}
|
||||
|
||||
R.id.menu_help -> showHelp("SourceMRssHelp")
|
||||
else -> if (item.groupId == R.id.source_group) {
|
||||
searchView.setQuery("group:${item.title}", true)
|
||||
}
|
||||
}
|
||||
return super.onCompatOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||
when (item?.itemId) {
|
||||
R.id.menu_enable_selection -> viewModel.enableSelection(adapter.selection)
|
||||
R.id.menu_disable_selection -> viewModel.disableSelection(adapter.selection)
|
||||
R.id.menu_add_group -> selectionAddToGroups()
|
||||
R.id.menu_remove_group -> selectionRemoveFromGroups()
|
||||
R.id.menu_top_sel -> viewModel.topSource(*adapter.selection.toTypedArray())
|
||||
R.id.menu_bottom_sel -> viewModel.bottomSource(*adapter.selection.toTypedArray())
|
||||
R.id.menu_export_selection -> viewModel.saveToFile(adapter.selection) { file ->
|
||||
exportResult.launch {
|
||||
mode = HandleFileContract.EXPORT
|
||||
fileData = HandleFileContract.FileData(
|
||||
"exportRssSource.json", file, "application/json"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
R.id.menu_share_source -> viewModel.saveToFile(adapter.selection) {
|
||||
share(it)
|
||||
}
|
||||
|
||||
R.id.menu_check_selected_interval -> adapter.checkSelectedInterval()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun initRecyclerView() {
|
||||
//binding.recyclerView.setEdgeEffectColor(primaryColor)
|
||||
binding.recyclerView.addItemDecoration(VerticalDivider(this))
|
||||
binding.recyclerView.adapter = adapter
|
||||
// When this page is opened, it is in selection mode
|
||||
val dragSelectTouchHelper: DragSelectTouchHelper =
|
||||
DragSelectTouchHelper(adapter.dragSelectCallback).setSlideArea(16, 50)
|
||||
dragSelectTouchHelper.attachToRecyclerView(binding.recyclerView)
|
||||
dragSelectTouchHelper.activeSlideSelect()
|
||||
// Note: need judge selection first, so add ItemTouchHelper after it.
|
||||
val itemTouchCallback = ItemTouchCallback(adapter)
|
||||
itemTouchCallback.isCanDrag = true
|
||||
ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView)
|
||||
}
|
||||
|
||||
private fun initSearchView() {
|
||||
binding.titleBar.findViewById<SearchView>(R.id.search_view).let {
|
||||
//it.applyTint(primaryTextColor)
|
||||
it.onActionViewExpanded()
|
||||
it.queryHint = getString(R.string.search_rss_source)
|
||||
it.clearFocus()
|
||||
it.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
upSourceFlow(newText)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSelectActionBar() {
|
||||
binding.selectActionBar.setMainActionText(R.string.delete)
|
||||
binding.selectActionBar.inflateMenu(R.menu.rss_source_sel)
|
||||
binding.selectActionBar.setOnMenuItemClickListener(this)
|
||||
binding.selectActionBar.setCallBack(this)
|
||||
}
|
||||
|
||||
private fun initGroupFlow() {
|
||||
lifecycleScope.launch {
|
||||
appDb.rssSourceDao.flowGroups().conflate().collect {
|
||||
groups.clear()
|
||||
groups.addAll(it)
|
||||
upGroupMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun selectionAddToGroups() {
|
||||
alert(titleResource = R.string.add_group) {
|
||||
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
|
||||
editView.setHint(R.string.group_name)
|
||||
editView.setFilterValues(groups.toList())
|
||||
editView.dropDownHeight = 180.dpToPx()
|
||||
}
|
||||
customView { alertBinding.root }
|
||||
okButton {
|
||||
alertBinding.editView.text?.toString()?.let {
|
||||
if (it.isNotEmpty()) {
|
||||
viewModel.selectionAddToGroups(adapter.selection, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
cancelButton()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun selectionRemoveFromGroups() {
|
||||
alert(titleResource = R.string.remove_group) {
|
||||
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
|
||||
editView.setHint(R.string.group_name)
|
||||
editView.setFilterValues(groups.toList())
|
||||
editView.dropDownHeight = 180.dpToPx()
|
||||
}
|
||||
customView { alertBinding.root }
|
||||
okButton {
|
||||
alertBinding.editView.text?.toString()?.let {
|
||||
if (it.isNotEmpty()) {
|
||||
viewModel.selectionRemoveFromGroups(adapter.selection, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
cancelButton()
|
||||
}
|
||||
}
|
||||
|
||||
override fun selectAll(selectAll: Boolean) {
|
||||
if (selectAll) {
|
||||
adapter.selectAll()
|
||||
} else {
|
||||
adapter.revertSelection()
|
||||
}
|
||||
}
|
||||
|
||||
override fun revertSelection() {
|
||||
adapter.revertSelection()
|
||||
}
|
||||
|
||||
override fun onClickSelectBarMainAction() {
|
||||
delSourceDialog()
|
||||
}
|
||||
|
||||
private fun delSourceDialog() {
|
||||
alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
|
||||
yesButton { viewModel.del(*adapter.selection.toTypedArray()) }
|
||||
noButton()
|
||||
}
|
||||
}
|
||||
|
||||
private fun upGroupMenu() = groupMenu?.transaction { menu ->
|
||||
menu.removeGroup(R.id.source_group)
|
||||
groups.forEach {
|
||||
menu.add(R.id.source_group, Menu.NONE, Menu.NONE, it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun upSourceFlow(searchKey: String? = null) {
|
||||
sourceFlowJob?.cancel()
|
||||
sourceFlowJob = lifecycleScope.launch {
|
||||
when {
|
||||
searchKey.isNullOrBlank() -> {
|
||||
appDb.rssSourceDao.flowAll()
|
||||
}
|
||||
|
||||
searchKey == getString(R.string.enabled) -> {
|
||||
appDb.rssSourceDao.flowEnabled()
|
||||
}
|
||||
|
||||
searchKey == getString(R.string.disabled) -> {
|
||||
appDb.rssSourceDao.flowDisabled()
|
||||
}
|
||||
|
||||
searchKey == getString(R.string.need_login) -> {
|
||||
appDb.rssSourceDao.flowLogin()
|
||||
}
|
||||
|
||||
searchKey == getString(R.string.no_group) -> {
|
||||
appDb.rssSourceDao.flowNoGroup()
|
||||
}
|
||||
|
||||
searchKey.startsWith("group:") -> {
|
||||
val key = searchKey.substringAfter("group:")
|
||||
appDb.rssSourceDao.flowGroupSearch(key)
|
||||
}
|
||||
|
||||
else -> {
|
||||
appDb.rssSourceDao.flowSearch(searchKey)
|
||||
}
|
||||
}.catch {
|
||||
AppLog.put("订阅源管理界面更新数据出错", it)
|
||||
}.flowOn(IO).conflate().collect {
|
||||
adapter.setItems(it, adapter.diffItemCallback)
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun upCountView() {
|
||||
binding.selectActionBar.upCountView(
|
||||
adapter.selection.size,
|
||||
adapter.itemCount
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private fun showImportDialog() {
|
||||
val aCache = ACache.get(cacheDir = false)
|
||||
val cacheUrls: MutableList<String> = aCache
|
||||
.getAsString(importRecordKey)
|
||||
?.splitNotBlank(",")
|
||||
?.toMutableList() ?: mutableListOf()
|
||||
alert(titleResource = R.string.import_on_line) {
|
||||
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
|
||||
editView.hint = "url"
|
||||
editView.setFilterValues(cacheUrls)
|
||||
editView.delCallBack = {
|
||||
cacheUrls.remove(it)
|
||||
aCache.put(importRecordKey, cacheUrls.joinToString(","))
|
||||
}
|
||||
}
|
||||
customView { alertBinding.root }
|
||||
okButton {
|
||||
val text = alertBinding.editView.text?.toString()
|
||||
text?.let {
|
||||
if (it.isAbsUrl() && !cacheUrls.contains(it)) {
|
||||
cacheUrls.add(0, it)
|
||||
aCache.put(importRecordKey, cacheUrls.joinToString(","))
|
||||
}
|
||||
showDialogFragment(
|
||||
ImportRssSourceDialog(it)
|
||||
)
|
||||
}
|
||||
}
|
||||
cancelButton()
|
||||
}
|
||||
}
|
||||
|
||||
override fun del(source: RssSource) {
|
||||
alert(R.string.draw) {
|
||||
setMessage(getString(R.string.sure_del) + "\n" + source.sourceName)
|
||||
noButton()
|
||||
yesButton {
|
||||
viewModel.del(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun edit(source: RssSource) {
|
||||
startActivity<RssSourceEditActivity> {
|
||||
putExtra("sourceUrl", source.sourceUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(vararg source: RssSource) {
|
||||
viewModel.update(*source)
|
||||
}
|
||||
|
||||
override fun toTop(source: RssSource) {
|
||||
viewModel.topSource(source)
|
||||
}
|
||||
|
||||
override fun toBottom(source: RssSource) {
|
||||
viewModel.bottomSource(source)
|
||||
}
|
||||
|
||||
override fun upOrder() {
|
||||
viewModel.upOrder()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package io.legato.kazusa.ui.rss.source.manage
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.PopupMenu
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.adapter.ItemViewHolder
|
||||
import io.legato.kazusa.base.adapter.RecyclerAdapter
|
||||
import io.legato.kazusa.data.entities.RssSource
|
||||
import io.legato.kazusa.databinding.ItemRssSourceBinding
|
||||
//import io.legado.app.lib.theme.backgroundColor
|
||||
import io.legato.kazusa.ui.widget.recycler.DragSelectTouchHelper
|
||||
import io.legato.kazusa.ui.widget.recycler.ItemTouchCallback
|
||||
import java.util.Collections
|
||||
|
||||
|
||||
class RssSourceAdapter(context: Context, val callBack: CallBack) :
|
||||
RecyclerAdapter<RssSource, ItemRssSourceBinding>(context),
|
||||
ItemTouchCallback.Callback {
|
||||
|
||||
private val selected = linkedSetOf<RssSource>()
|
||||
|
||||
val selection: List<RssSource>
|
||||
get() {
|
||||
return getItems().filter {
|
||||
selected.contains(it)
|
||||
}
|
||||
}
|
||||
|
||||
val diffItemCallback = object : DiffUtil.ItemCallback<RssSource>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: RssSource, newItem: RssSource): Boolean {
|
||||
return oldItem.sourceUrl == newItem.sourceUrl
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: RssSource, newItem: RssSource): Boolean {
|
||||
return oldItem.sourceName == newItem.sourceName
|
||||
&& oldItem.sourceGroup == newItem.sourceGroup
|
||||
&& oldItem.enabled == newItem.enabled
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItem: RssSource, newItem: RssSource): Any? {
|
||||
val payload = Bundle()
|
||||
if (oldItem.sourceName != newItem.sourceName
|
||||
|| oldItem.sourceGroup != newItem.sourceGroup
|
||||
) {
|
||||
payload.putBoolean("upName", true)
|
||||
}
|
||||
if (oldItem.enabled != newItem.enabled) {
|
||||
payload.putBoolean("enabled", newItem.enabled)
|
||||
}
|
||||
if (payload.isEmpty) {
|
||||
return null
|
||||
}
|
||||
return payload
|
||||
}
|
||||
}
|
||||
|
||||
override fun getViewBinding(parent: ViewGroup): ItemRssSourceBinding {
|
||||
return ItemRssSourceBinding.inflate(inflater, parent, false)
|
||||
}
|
||||
|
||||
override fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemRssSourceBinding,
|
||||
item: RssSource,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
binding.run {
|
||||
if (payloads.isEmpty()) {
|
||||
//root.setBackgroundColor(ColorUtils.withAlpha(context.backgroundColor, 0.5f))
|
||||
cbSource.text = item.getDisplayNameGroup()
|
||||
swtEnabled.isChecked = item.enabled
|
||||
cbSource.isChecked = selected.contains(item)
|
||||
} else {
|
||||
for (i in payloads.indices) {
|
||||
val bundle = payloads[i] as Bundle
|
||||
bundle.keySet().forEach {
|
||||
when (it) {
|
||||
"upName" -> cbSource.text = item.getDisplayNameGroup()
|
||||
"enabled" -> swtEnabled.isChecked = bundle.getBoolean("enabled")
|
||||
"selected" -> cbSource.isChecked = selected.contains(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerListener(holder: ItemViewHolder, binding: ItemRssSourceBinding) {
|
||||
binding.apply {
|
||||
swtEnabled.setOnCheckedChangeListener { view, checked ->
|
||||
if (view.isPressed) {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
if (view.isPressed) {
|
||||
it.enabled = checked
|
||||
callBack.update(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cbSource.setOnCheckedChangeListener { view, checked ->
|
||||
if (view.isPressed) {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
if (view.isPressed) {
|
||||
if (checked) {
|
||||
selected.add(it)
|
||||
} else {
|
||||
selected.remove(it)
|
||||
}
|
||||
callBack.upCountView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ivEdit.setOnClickListener {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
callBack.edit(it)
|
||||
}
|
||||
}
|
||||
ivMenuMore.setOnClickListener {
|
||||
showMenu(ivMenuMore, holder.layoutPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCurrentListChanged() {
|
||||
callBack.upCountView()
|
||||
}
|
||||
|
||||
fun selectAll() {
|
||||
getItems().forEach {
|
||||
selected.add(it)
|
||||
}
|
||||
notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null)))
|
||||
callBack.upCountView()
|
||||
}
|
||||
|
||||
fun revertSelection() {
|
||||
getItems().forEach {
|
||||
if (selected.contains(it)) {
|
||||
selected.remove(it)
|
||||
} else {
|
||||
selected.add(it)
|
||||
}
|
||||
}
|
||||
notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null)))
|
||||
callBack.upCountView()
|
||||
}
|
||||
|
||||
fun checkSelectedInterval() {
|
||||
val selectedPosition = linkedSetOf<Int>()
|
||||
getItems().forEachIndexed { index, it ->
|
||||
if (selected.contains(it)) {
|
||||
selectedPosition.add(index)
|
||||
}
|
||||
}
|
||||
val minPosition = Collections.min(selectedPosition)
|
||||
val maxPosition = Collections.max(selectedPosition)
|
||||
val itemCount = maxPosition - minPosition + 1
|
||||
for (i in minPosition..maxPosition) {
|
||||
getItem(i)?.let {
|
||||
selected.add(it)
|
||||
}
|
||||
}
|
||||
notifyItemRangeChanged(minPosition, itemCount, bundleOf(Pair("selected", null)))
|
||||
callBack.upCountView()
|
||||
}
|
||||
|
||||
private fun showMenu(view: View, position: Int) {
|
||||
val source = getItem(position) ?: return
|
||||
val popupMenu = PopupMenu(context, view)
|
||||
popupMenu.inflate(R.menu.rss_source_item)
|
||||
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.menu_top -> callBack.toTop(source)
|
||||
R.id.menu_bottom -> callBack.toBottom(source)
|
||||
R.id.menu_del -> {
|
||||
callBack.del(source)
|
||||
selected.remove(source)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
popupMenu.show()
|
||||
}
|
||||
|
||||
override fun swap(srcPosition: Int, targetPosition: Int): Boolean {
|
||||
val srcItem = getItem(srcPosition)
|
||||
val targetItem = getItem(targetPosition)
|
||||
if (srcItem != null && targetItem != null) {
|
||||
if (srcItem.customOrder == targetItem.customOrder) {
|
||||
callBack.upOrder()
|
||||
} else {
|
||||
val srcOrder = srcItem.customOrder
|
||||
srcItem.customOrder = targetItem.customOrder
|
||||
targetItem.customOrder = srcOrder
|
||||
movedItems.add(srcItem)
|
||||
movedItems.add(targetItem)
|
||||
}
|
||||
}
|
||||
swapItem(srcPosition, targetPosition)
|
||||
return true
|
||||
}
|
||||
|
||||
private val movedItems = hashSetOf<RssSource>()
|
||||
|
||||
override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||
if (movedItems.isNotEmpty()) {
|
||||
callBack.update(*movedItems.toTypedArray())
|
||||
movedItems.clear()
|
||||
}
|
||||
}
|
||||
|
||||
val dragSelectCallback: DragSelectTouchHelper.Callback =
|
||||
object : DragSelectTouchHelper.AdvanceCallback<RssSource>(Mode.ToggleAndReverse) {
|
||||
override fun currentSelectedId(): MutableSet<RssSource> {
|
||||
return selected
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): RssSource {
|
||||
return getItem(position)!!
|
||||
}
|
||||
|
||||
override fun updateSelectState(position: Int, isSelected: Boolean): Boolean {
|
||||
getItem(position)?.let {
|
||||
if (isSelected) {
|
||||
selected.add(it)
|
||||
} else {
|
||||
selected.remove(it)
|
||||
}
|
||||
notifyItemChanged(position, bundleOf(Pair("selected", null)))
|
||||
callBack.upCountView()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
interface CallBack {
|
||||
fun del(source: RssSource)
|
||||
fun edit(source: RssSource)
|
||||
fun update(vararg source: RssSource)
|
||||
fun toTop(source: RssSource)
|
||||
fun toBottom(source: RssSource)
|
||||
fun upOrder()
|
||||
fun upCountView()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package io.legato.kazusa.ui.rss.source.manage
|
||||
|
||||
import android.app.Application
|
||||
import android.text.TextUtils
|
||||
import io.legato.kazusa.base.BaseViewModel
|
||||
import io.legato.kazusa.data.appDb
|
||||
import io.legato.kazusa.data.entities.RssSource
|
||||
import io.legato.kazusa.help.DefaultData
|
||||
import io.legato.kazusa.help.source.SourceHelp
|
||||
import io.legato.kazusa.utils.FileUtils
|
||||
import io.legato.kazusa.utils.GSON
|
||||
import io.legato.kazusa.utils.splitNotBlank
|
||||
import io.legato.kazusa.utils.stackTraceStr
|
||||
import io.legato.kazusa.utils.toastOnUi
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* 订阅源管理数据修改
|
||||
* 修改数据要copy,直接修改会导致界面不刷新
|
||||
*/
|
||||
class RssSourceViewModel(application: Application) : BaseViewModel(application) {
|
||||
|
||||
fun topSource(vararg sources: RssSource) {
|
||||
execute {
|
||||
sources.sortBy { it.customOrder }
|
||||
val minOrder = appDb.rssSourceDao.minOrder - 1
|
||||
val array = Array(sources.size) {
|
||||
sources[it].copy(customOrder = minOrder - it)
|
||||
}
|
||||
appDb.rssSourceDao.update(*array)
|
||||
}
|
||||
}
|
||||
|
||||
fun bottomSource(vararg sources: RssSource) {
|
||||
execute {
|
||||
sources.sortBy { it.customOrder }
|
||||
val maxOrder = appDb.rssSourceDao.maxOrder + 1
|
||||
val array = Array(sources.size) {
|
||||
sources[it].copy(customOrder = maxOrder + it)
|
||||
}
|
||||
appDb.rssSourceDao.update(*array)
|
||||
}
|
||||
}
|
||||
|
||||
fun del(vararg rssSource: RssSource) {
|
||||
execute {
|
||||
SourceHelp.deleteRssSources(rssSource.toList())
|
||||
}
|
||||
}
|
||||
|
||||
fun update(vararg rssSource: RssSource) {
|
||||
execute { appDb.rssSourceDao.update(*rssSource) }
|
||||
}
|
||||
|
||||
fun upOrder() {
|
||||
execute {
|
||||
val sources = appDb.rssSourceDao.all
|
||||
for ((index: Int, source: RssSource) in sources.withIndex()) {
|
||||
source.customOrder = index + 1
|
||||
}
|
||||
appDb.rssSourceDao.update(*sources.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
fun enableSelection(sources: List<RssSource>) {
|
||||
execute {
|
||||
val array = Array(sources.size) {
|
||||
sources[it].copy(enabled = true)
|
||||
}
|
||||
appDb.rssSourceDao.update(*array)
|
||||
}
|
||||
}
|
||||
|
||||
fun disableSelection(sources: List<RssSource>) {
|
||||
execute {
|
||||
val array = Array(sources.size) {
|
||||
sources[it].copy(enabled = false)
|
||||
}
|
||||
appDb.rssSourceDao.update(*array)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveToFile(sources: List<RssSource>, success: (file: File) -> Unit) {
|
||||
execute {
|
||||
val path = "${context.filesDir}/shareRssSource.json"
|
||||
FileUtils.delete(path)
|
||||
val file = FileUtils.createFileWithReplace(path)
|
||||
file.writeText(GSON.toJson(sources))
|
||||
file
|
||||
}.onSuccess {
|
||||
success.invoke(it)
|
||||
}.onError {
|
||||
context.toastOnUi(it.stackTraceStr)
|
||||
}
|
||||
}
|
||||
|
||||
fun selectionAddToGroups(sources: List<RssSource>, groups: String) {
|
||||
execute {
|
||||
val array = Array(sources.size) {
|
||||
sources[it].copy().addGroup(groups)
|
||||
}
|
||||
appDb.rssSourceDao.update(*array)
|
||||
}
|
||||
}
|
||||
|
||||
fun selectionRemoveFromGroups(sources: List<RssSource>, groups: String) {
|
||||
execute {
|
||||
val array = Array(sources.size) {
|
||||
sources[it].copy().removeGroup(groups)
|
||||
}
|
||||
appDb.rssSourceDao.update(*array)
|
||||
}
|
||||
}
|
||||
|
||||
fun addGroup(group: String) {
|
||||
execute {
|
||||
val sources = appDb.rssSourceDao.noGroup
|
||||
sources.forEach { source ->
|
||||
source.sourceGroup = group
|
||||
}
|
||||
appDb.rssSourceDao.update(*sources.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
fun upGroup(oldGroup: String, newGroup: String?) {
|
||||
execute {
|
||||
val sources = appDb.rssSourceDao.getByGroup(oldGroup)
|
||||
sources.forEach { source ->
|
||||
source.sourceGroup?.splitNotBlank(",")?.toHashSet()?.let {
|
||||
it.remove(oldGroup)
|
||||
if (!newGroup.isNullOrEmpty())
|
||||
it.add(newGroup)
|
||||
source.sourceGroup = TextUtils.join(",", it)
|
||||
}
|
||||
}
|
||||
appDb.rssSourceDao.update(*sources.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
fun delGroup(group: String) {
|
||||
execute {
|
||||
execute {
|
||||
val sources = appDb.rssSourceDao.getByGroup(group)
|
||||
sources.forEach { source ->
|
||||
source.sourceGroup?.splitNotBlank(",")?.toHashSet()?.let {
|
||||
it.remove(group)
|
||||
source.sourceGroup = TextUtils.join(",", it)
|
||||
}
|
||||
}
|
||||
appDb.rssSourceDao.update(*sources.toTypedArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun importDefault() {
|
||||
execute {
|
||||
DefaultData.importDefaultRssSources()
|
||||
}
|
||||
}
|
||||
|
||||
fun disable(rssSource: RssSource) {
|
||||
execute {
|
||||
rssSource.enabled = false
|
||||
appDb.rssSourceDao.update(rssSource)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package io.legato.kazusa.ui.rss.subscription
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.core.view.isGone
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.BaseActivity
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.data.appDb
|
||||
import io.legato.kazusa.data.entities.RuleSub
|
||||
import io.legato.kazusa.databinding.ActivityRuleSubBinding
|
||||
import io.legato.kazusa.databinding.DialogRuleSubEditBinding
|
||||
import io.legato.kazusa.lib.dialogs.alert
|
||||
import io.legato.kazusa.ui.association.ImportBookSourceDialog
|
||||
import io.legato.kazusa.ui.association.ImportReplaceRuleDialog
|
||||
import io.legato.kazusa.ui.association.ImportRssSourceDialog
|
||||
import io.legato.kazusa.ui.widget.recycler.ItemTouchCallback
|
||||
import io.legato.kazusa.utils.applyNavigationBarPadding
|
||||
import io.legato.kazusa.utils.showDialogFragment
|
||||
import io.legato.kazusa.utils.toastOnUi
|
||||
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* 规则订阅界面
|
||||
*/
|
||||
class RuleSubActivity : BaseActivity<ActivityRuleSubBinding>(),
|
||||
RuleSubAdapter.Callback {
|
||||
|
||||
override val binding by viewBinding(ActivityRuleSubBinding::inflate)
|
||||
private val adapter by lazy { RuleSubAdapter(this, this) }
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
initView()
|
||||
initData()
|
||||
}
|
||||
|
||||
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.source_subscription, menu)
|
||||
return super.onCompatCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_add -> {
|
||||
val order = appDb.ruleSubDao.maxOrder + 1
|
||||
editSubscription(RuleSub(customOrder = order))
|
||||
}
|
||||
}
|
||||
return super.onCompatOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.recyclerView.applyNavigationBarPadding()
|
||||
val itemTouchCallback = ItemTouchCallback(adapter)
|
||||
itemTouchCallback.isCanDrag = true
|
||||
ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView)
|
||||
}
|
||||
|
||||
private fun initData() {
|
||||
lifecycleScope.launch {
|
||||
appDb.ruleSubDao.flowAll().catch {
|
||||
AppLog.put("规则订阅界面获取数据失败\n${it.localizedMessage}", it)
|
||||
}.flowOn(IO).conflate().collect {
|
||||
binding.tvEmptyMsg.isGone = it.isNotEmpty()
|
||||
adapter.setItems(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun openSubscription(ruleSub: RuleSub) {
|
||||
when (ruleSub.type) {
|
||||
0 -> showDialogFragment(
|
||||
ImportBookSourceDialog(ruleSub.url)
|
||||
)
|
||||
1 -> showDialogFragment(
|
||||
ImportRssSourceDialog(ruleSub.url)
|
||||
)
|
||||
2 -> showDialogFragment(
|
||||
ImportReplaceRuleDialog(ruleSub.url)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun editSubscription(ruleSub: RuleSub) {
|
||||
alert(R.string.rule_subscription) {
|
||||
val alertBinding = DialogRuleSubEditBinding.inflate(layoutInflater).apply {
|
||||
spType.setSelection(ruleSub.type)
|
||||
etName.setText(ruleSub.name)
|
||||
etUrl.setText(ruleSub.url)
|
||||
}
|
||||
customView { alertBinding.root }
|
||||
okButton {
|
||||
lifecycleScope.launch {
|
||||
ruleSub.type = alertBinding.spType.selectedItemPosition
|
||||
ruleSub.name = alertBinding.etName.text?.toString() ?: ""
|
||||
ruleSub.url = alertBinding.etUrl.text?.toString() ?: ""
|
||||
val rs = withContext(IO) {
|
||||
appDb.ruleSubDao.findByUrl(ruleSub.url)
|
||||
}
|
||||
if (rs != null && rs.id != ruleSub.id) {
|
||||
toastOnUi("${getString(R.string.url_already)}(${rs.name})")
|
||||
return@launch
|
||||
}
|
||||
withContext(IO) {
|
||||
appDb.ruleSubDao.insert(ruleSub)
|
||||
}
|
||||
}
|
||||
}
|
||||
cancelButton()
|
||||
}
|
||||
}
|
||||
|
||||
override fun delSubscription(ruleSub: RuleSub) {
|
||||
lifecycleScope.launch(IO) {
|
||||
appDb.ruleSubDao.delete(ruleSub)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateSourceSub(vararg ruleSub: RuleSub) {
|
||||
lifecycleScope.launch(IO) {
|
||||
appDb.ruleSubDao.update(*ruleSub)
|
||||
}
|
||||
}
|
||||
|
||||
override fun upOrder() {
|
||||
lifecycleScope.launch(IO) {
|
||||
val sourceSubs = appDb.ruleSubDao.all
|
||||
for ((index: Int, ruleSub: RuleSub) in sourceSubs.withIndex()) {
|
||||
ruleSub.customOrder = index + 1
|
||||
}
|
||||
appDb.ruleSubDao.update(*sourceSubs.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package io.legato.kazusa.ui.rss.subscription
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.PopupMenu
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.adapter.ItemViewHolder
|
||||
import io.legato.kazusa.base.adapter.RecyclerAdapter
|
||||
import io.legato.kazusa.data.entities.RuleSub
|
||||
import io.legato.kazusa.databinding.ItemRuleSubBinding
|
||||
import io.legato.kazusa.ui.widget.recycler.ItemTouchCallback
|
||||
|
||||
|
||||
class RuleSubAdapter(context: Context, val callBack: Callback) :
|
||||
RecyclerAdapter<RuleSub, ItemRuleSubBinding>(context),
|
||||
ItemTouchCallback.Callback {
|
||||
|
||||
private val typeArray = context.resources.getStringArray(R.array.rule_type)
|
||||
|
||||
override fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemRuleSubBinding,
|
||||
item: RuleSub,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
binding.tvType.text = typeArray[item.type]
|
||||
binding.tvName.text = item.name
|
||||
binding.tvUrl.text = item.url
|
||||
}
|
||||
|
||||
override fun registerListener(holder: ItemViewHolder, binding: ItemRuleSubBinding) {
|
||||
binding.root.setOnClickListener {
|
||||
callBack.openSubscription(getItem(holder.layoutPosition)!!)
|
||||
}
|
||||
binding.ivEdit.setOnClickListener {
|
||||
callBack.editSubscription(getItem(holder.layoutPosition)!!)
|
||||
}
|
||||
binding.ivMenuMore.setOnClickListener {
|
||||
showMenu(binding.ivMenuMore, holder.layoutPosition)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMenu(view: View, position: Int) {
|
||||
val source = getItem(position) ?: return
|
||||
val popupMenu = PopupMenu(context, view)
|
||||
popupMenu.inflate(R.menu.source_sub_item)
|
||||
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.menu_del -> callBack.delSubscription(source)
|
||||
}
|
||||
true
|
||||
}
|
||||
popupMenu.show()
|
||||
}
|
||||
|
||||
override fun getViewBinding(parent: ViewGroup): ItemRuleSubBinding {
|
||||
return ItemRuleSubBinding.inflate(inflater, parent, false)
|
||||
}
|
||||
|
||||
override fun swap(srcPosition: Int, targetPosition: Int): Boolean {
|
||||
val srcItem = getItem(srcPosition)
|
||||
val targetItem = getItem(targetPosition)
|
||||
if (srcItem != null && targetItem != null) {
|
||||
if (srcItem.customOrder == targetItem.customOrder) {
|
||||
callBack.upOrder()
|
||||
} else {
|
||||
val srcOrder = srcItem.customOrder
|
||||
srcItem.customOrder = targetItem.customOrder
|
||||
targetItem.customOrder = srcOrder
|
||||
movedItems.add(srcItem)
|
||||
movedItems.add(targetItem)
|
||||
}
|
||||
}
|
||||
swapItem(srcPosition, targetPosition)
|
||||
return true
|
||||
}
|
||||
|
||||
private val movedItems = hashSetOf<RuleSub>()
|
||||
|
||||
override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||
if (movedItems.isNotEmpty()) {
|
||||
callBack.updateSourceSub(*movedItems.toTypedArray())
|
||||
movedItems.clear()
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun openSubscription(ruleSub: RuleSub)
|
||||
fun editSubscription(ruleSub: RuleSub)
|
||||
fun delSubscription(ruleSub: RuleSub)
|
||||
fun updateSourceSub(vararg ruleSub: RuleSub)
|
||||
fun upOrder()
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user