Implement feed groups manual sorting

Now, the user can sort its groups to his liking even after he created
them.

Also updated the database diagram to reflect the table's new column.
This commit is contained in:
Mauricio Colli 2020-02-25 23:01:23 -03:00
parent eb8b394d96
commit 7aecb00211
20 changed files with 473 additions and 19 deletions

View file

@ -147,6 +147,15 @@ class FeedDatabaseManager(context: Context) {
.observeOn(AndroidSchedulers.mainThread())
}
fun updateGroupsOrder(groupIdList: List<Long>): Completable {
var index = 0L
val orderMap = groupIdList.associateBy({ it }, { index++ })
return Completable.fromCallable { feedGroupTable.updateOrder(orderMap) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<Date>> {
return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> feedTable.oldestSubscriptionUpdateFromAll()

View file

@ -25,8 +25,9 @@ import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.fragments.BaseStateFragment
import org.schabi.newpipe.local.subscription.SubscriptionViewModel.*
import org.schabi.newpipe.local.subscription.SubscriptionViewModel.SubscriptionState
import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialog
import org.schabi.newpipe.local.subscription.item.*
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.EXPORT_COMPLETE_ACTION
@ -34,11 +35,8 @@ import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.*
import org.schabi.newpipe.report.UserAction
import org.schabi.newpipe.util.*
import org.schabi.newpipe.util.AnimationUtils.animateView
import org.schabi.newpipe.util.FilePickerActivityHelper
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.OnClickGesture
import org.schabi.newpipe.util.ShareUtils
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
@ -54,6 +52,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private val feedGroupsSection = Section()
private var feedGroupsCarousel: FeedGroupCarouselItem? = null
private lateinit var importExportItem: FeedImportExportItem
private lateinit var feedGroupsSortMenuItem: HeaderWithMenuItem
private val subscriptionsSection = Section()
@State @JvmField var itemsListState: Parcelable? = null
@ -164,6 +163,10 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
startActivityForResult(FilePickerActivityHelper.chooseFileToSave(activity, exportFile.absolutePath), REQUEST_EXPORT_CODE)
}
private fun openReorderDialog() {
FeedGroupReorderDialog().show(requireFragmentManager(), null)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (data != null && data.data != null && resultCode == Activity.RESULT_OK) {
@ -210,7 +213,12 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
}
feedGroupsCarousel = FeedGroupCarouselItem(requireContext(), carouselAdapter)
add(Section(HeaderItem(getString(R.string.feed_groups_header_title)), listOf(feedGroupsCarousel)))
feedGroupsSortMenuItem = HeaderWithMenuItem(
getString(R.string.feed_groups_header_title),
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_sort),
menuItemOnClickListener = ::openReorderDialog
)
add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel)))
groupAdapter.add(this)
}
@ -333,6 +341,12 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
feedGroupsCarousel?.onRestoreInstanceState(feedGroupsListState)
feedGroupsListState = null
}
if (groups.size < 2) {
items_list.post { feedGroupsSortMenuItem.notifyChanged(HeaderWithMenuItem.PAYLOAD_HIDE_MENU_ITEM) }
} else {
items_list.post { feedGroupsSortMenuItem.notifyChanged(HeaderWithMenuItem.PAYLOAD_SHOW_MENU_ITEM) }
}
}
///////////////////////////////////////////////////////////////////////////

View file

@ -39,6 +39,7 @@ class FeedGroupDialog : DialogFragment() {
private lateinit var viewModel: FeedGroupDialogViewModel
private var groupId: Long = NO_GROUP_SELECTED
private var groupIcon: FeedGroupIcon? = null
private var groupSortOrder: Long = -1
sealed class ScreenState : Serializable {
object InitialScreen : ScreenState()
@ -145,7 +146,7 @@ class FeedGroupDialog : DialogFragment() {
when (groupId) {
NO_GROUP_SELECTED -> viewModel.createGroup(name, icon, selectedSubscriptions)
else -> viewModel.updateGroup(name, icon, selectedSubscriptions)
else -> viewModel.updateGroup(name, icon, selectedSubscriptions, groupSortOrder)
}
} else {
showInitialScreen()
@ -167,6 +168,7 @@ class FeedGroupDialog : DialogFragment() {
val icon = feedGroupEntity?.icon ?: FeedGroupIcon.ALL
val name = feedGroupEntity?.name ?: ""
groupIcon = feedGroupEntity?.icon
groupSortOrder = feedGroupEntity?.sortOrder ?: -1
icon_preview.setImageResource((if (selectedIcon == null) icon else selectedIcon!!).getDrawableRes(requireContext()))

View file

@ -58,9 +58,9 @@ class FeedGroupDialogViewModel(applicationContext: Context, val groupId: Long =
.subscribe { successLiveData.postValue(FeedDialogEvent.SuccessEvent) })
}
fun updateGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set<Long>) {
fun updateGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set<Long>, sortOrder: Long) {
disposables.add(feedDatabaseManager.updateSubscriptionsForGroup(groupId, selectedSubscriptions.toList())
.andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon)))
.andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon, sortOrder)))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { successLiveData.postValue(FeedDialogEvent.SuccessEvent) })

View file

@ -0,0 +1,103 @@
package org.schabi.newpipe.local.subscription.dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.TouchCallback
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import icepick.Icepick
import icepick.State
import kotlinx.android.synthetic.main.dialog_feed_group_reorder.*
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.SuccessEvent
import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
import org.schabi.newpipe.util.ThemeHelper
import java.util.*
import kotlin.collections.ArrayList
class FeedGroupReorderDialog : DialogFragment() {
private lateinit var viewModel: FeedGroupReorderDialogViewModel
@State @JvmField var groupOrderedIdList = ArrayList<Long>()
private val groupAdapter = GroupAdapter<GroupieViewHolder>()
private val itemTouchHelper = ItemTouchHelper(getItemTouchCallback())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Icepick.restoreInstanceState(this, savedInstanceState)
setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext()))
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.dialog_feed_group_reorder, container)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this).get(FeedGroupReorderDialogViewModel::class.java)
viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups))
viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer {
when (it) {
is SuccessEvent -> dismiss()
}
})
feed_groups_list.layoutManager = LinearLayoutManager(requireContext())
feed_groups_list.adapter = groupAdapter
itemTouchHelper.attachToRecyclerView(feed_groups_list)
confirm_button.setOnClickListener {
viewModel.updateOrder(groupOrderedIdList)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
Icepick.saveInstanceState(this, outState)
}
private fun handleGroups(list: List<FeedGroupEntity>) {
val groupList: List<FeedGroupEntity>
if (groupOrderedIdList.isEmpty()) {
groupList = list
groupOrderedIdList.addAll(groupList.map { it.uid })
} else {
groupList = list.sortedBy { groupOrderedIdList.indexOf(it.uid) }
}
groupAdapter.update(groupList.map { FeedGroupReorderItem(it, itemTouchHelper) })
}
private fun getItemTouchCallback(): SimpleCallback {
return object : TouchCallback() {
override fun onMove(recyclerView: RecyclerView, source: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder): Boolean {
val sourceIndex = source.adapterPosition
val targetIndex = target.adapterPosition
groupAdapter.notifyItemMoved(sourceIndex, targetIndex)
Collections.swap(groupOrderedIdList, sourceIndex, targetIndex)
return true
}
override fun isLongPressDragEnabled(): Boolean = false
override fun isItemViewSwipeEnabled(): Boolean = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, swipeDir: Int) {}
}
}
}

View file

@ -0,0 +1,41 @@
package org.schabi.newpipe.local.subscription.dialog
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.local.feed.FeedDatabaseManager
class FeedGroupReorderDialogViewModel(application: Application) : AndroidViewModel(application) {
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application)
val groupsLiveData = MutableLiveData<List<FeedGroupEntity>>()
val dialogEventLiveData = MutableLiveData<DialogEvent>()
private val disposables = CompositeDisposable()
private var groupsDisposable = feedDatabaseManager.groups()
.limit(1)
.subscribeOn(Schedulers.io())
.subscribe(groupsLiveData::postValue)
override fun onCleared() {
super.onCleared()
groupsDisposable.dispose()
disposables.dispose()
}
fun updateOrder(groupIdList: List<Long>) {
disposables.add(feedDatabaseManager.updateGroupsOrder(groupIdList)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { dialogEventLiveData.postValue(DialogEvent.SuccessEvent) })
}
sealed class DialogEvent {
object SuccessEvent : DialogEvent()
}
}

View file

@ -0,0 +1,48 @@
package org.schabi.newpipe.local.subscription.item
import android.view.MotionEvent
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
import androidx.recyclerview.widget.ItemTouchHelper.UP
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.kotlinandroidextensions.Item
import kotlinx.android.synthetic.main.feed_group_reorder_item.*
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.local.subscription.FeedGroupIcon
data class FeedGroupReorderItem(
val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
val name: String,
val icon: FeedGroupIcon,
val dragCallback: ItemTouchHelper
) : Item() {
constructor (feedGroupEntity: FeedGroupEntity, dragCallback: ItemTouchHelper)
: this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon, dragCallback)
override fun getId(): Long {
return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> super.getId()
else -> groupId
}
}
override fun getLayout(): Int = R.layout.feed_group_reorder_item
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.group_name.text = name
viewHolder.group_icon.setImageResource(icon.getDrawableRes(viewHolder.containerView.context))
viewHolder.handle.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
dragCallback.startDrag(viewHolder)
return@setOnTouchListener true
}
false
}
}
override fun getDragDirs(): Int {
return UP or DOWN
}
}

View file

@ -0,0 +1,48 @@
package org.schabi.newpipe.local.subscription.item
import android.view.View.*
import androidx.annotation.DrawableRes
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.kotlinandroidextensions.Item
import kotlinx.android.synthetic.main.header_with_menu_item.*
import org.schabi.newpipe.R
class HeaderWithMenuItem(
val title: String,
@DrawableRes val itemIcon: Int = 0,
private val onClickListener: (() -> Unit)? = null,
private val menuItemOnClickListener: (() -> Unit)? = null
) : Item() {
companion object {
const val PAYLOAD_SHOW_MENU_ITEM = 1
const val PAYLOAD_HIDE_MENU_ITEM = 2
}
override fun getLayout(): Int = R.layout.header_with_menu_item
override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.contains(PAYLOAD_SHOW_MENU_ITEM)) {
viewHolder.header_menu_item.visibility = VISIBLE
return
} else if (payloads.contains(PAYLOAD_HIDE_MENU_ITEM)) {
viewHolder.header_menu_item.visibility = GONE
return
}
super.bind(viewHolder, position, payloads)
}
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
viewHolder.header_title.text = title
viewHolder.header_menu_item.setImageResource(itemIcon)
val listener: OnClickListener? =
onClickListener?.let { OnClickListener { onClickListener.invoke() } }
viewHolder.root.setOnClickListener(listener)
val menuItemListener: OnClickListener? =
menuItemOnClickListener?.let { OnClickListener { menuItemOnClickListener.invoke() } }
viewHolder.header_menu_item.setOnClickListener(menuItemListener)
}
}