Merge pull request #3373 from mauriciocolli/feed-add-search-sub-list

Add search for subscription picker in the feed group dialog
This commit is contained in:
Tobias Groza 2020-07-01 00:16:01 +02:00 committed by GitHub
commit 734680b9f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 414 additions and 185 deletions

View File

@ -20,6 +20,13 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
@Query("SELECT * FROM subscriptions ORDER BY name COLLATE NOCASE ASC")
abstract override fun getAll(): Flowable<List<SubscriptionEntity>>
@Query("""
SELECT * FROM subscriptions
WHERE name LIKE '%' || :filter || '%'
ORDER BY name COLLATE NOCASE ASC
""")
abstract fun filterByName(filter: String): Flowable<List<SubscriptionEntity>>
@Query("SELECT * FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId")
abstract fun getSubscriptionFlowable(serviceId: Int, url: String): Flowable<List<SubscriptionEntity>>

View File

@ -130,4 +130,55 @@ public class SubscriptionEntity {
item.setDescription(getDescription());
return item;
}
// TODO: Remove these generated methods by migrating this class to a data class from Kotlin.
@Override
@SuppressWarnings("EqualsReplaceableByObjectsCall")
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final SubscriptionEntity that = (SubscriptionEntity) o;
if (uid != that.uid) {
return false;
}
if (serviceId != that.serviceId) {
return false;
}
if (!url.equals(that.url)) {
return false;
}
if (name != null ? !name.equals(that.name) : that.name != null) {
return false;
}
if (avatarUrl != null ? !avatarUrl.equals(that.avatarUrl) : that.avatarUrl != null) {
return false;
}
if (subscriberCount != null
? !subscriberCount.equals(that.subscriberCount)
: that.subscriberCount != null) {
return false;
}
return description != null
? description.equals(that.description)
: that.description == null;
}
@Override
public int hashCode() {
int result = (int) (uid ^ (uid >>> 32));
result = 31 * result + serviceId;
result = 31 * result + url.hashCode();
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + (avatarUrl != null ? avatarUrl.hashCode() : 0);
result = 31 * result + (subscriberCount != null ? subscriberCount.hashCode() : 0);
result = 31 * result + (description != null ? description.hashCode() : 0);
return result;
}
}

View File

@ -21,6 +21,8 @@ class SubscriptionManager(context: Context) {
fun subscriptionTable(): SubscriptionDAO = subscriptionTable
fun subscriptions() = subscriptionTable.all
fun filterByName(filter: String) = subscriptionTable.filterByName(filter)
fun upsertAll(infoList: List<ChannelInfo>): List<SubscriptionEntity> {
val listEntities = subscriptionTable.upsertAll(
infoList.map { SubscriptionEntity.from(it) })

View File

@ -5,6 +5,7 @@ import android.content.Context
import android.os.Bundle
import android.os.Parcelable
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
@ -13,34 +14,22 @@ import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.Section
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import icepick.Icepick
import icepick.State
import java.io.Serializable
import kotlinx.android.synthetic.main.dialog_feed_group_create.cancel_button
import kotlinx.android.synthetic.main.dialog_feed_group_create.confirm_button
import kotlinx.android.synthetic.main.dialog_feed_group_create.delete_button
import kotlinx.android.synthetic.main.dialog_feed_group_create.delete_screen_message
import kotlinx.android.synthetic.main.dialog_feed_group_create.group_name_input
import kotlinx.android.synthetic.main.dialog_feed_group_create.group_name_input_container
import kotlinx.android.synthetic.main.dialog_feed_group_create.icon_preview
import kotlinx.android.synthetic.main.dialog_feed_group_create.icon_selector
import kotlinx.android.synthetic.main.dialog_feed_group_create.options_root
import kotlinx.android.synthetic.main.dialog_feed_group_create.select_channel_button
import kotlinx.android.synthetic.main.dialog_feed_group_create.selected_subscription_count_view
import kotlinx.android.synthetic.main.dialog_feed_group_create.separator
import kotlinx.android.synthetic.main.dialog_feed_group_create.subscriptions_selector
import kotlinx.android.synthetic.main.dialog_feed_group_create.subscriptions_selector_header_info
import kotlinx.android.synthetic.main.dialog_feed_group_create.subscriptions_selector_list
import kotlin.collections.contains
import kotlinx.android.synthetic.main.dialog_feed_group_create.*
import kotlinx.android.synthetic.main.toolbar_search_layout.*
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.fragments.BackPressable
import org.schabi.newpipe.local.subscription.FeedGroupIcon
import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.DeleteScreen
import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.IconPickerScreen
@ -51,9 +40,10 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialogViewModel.Dia
import org.schabi.newpipe.local.subscription.item.EmptyPlaceholderItem
import org.schabi.newpipe.local.subscription.item.PickerIconItem
import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem
import org.schabi.newpipe.util.AndroidTvUtils
import org.schabi.newpipe.util.ThemeHelper
class FeedGroupDialog : DialogFragment() {
class FeedGroupDialog : DialogFragment(), BackPressable {
private lateinit var viewModel: FeedGroupDialogViewModel
private var groupId: Long = NO_GROUP_SELECTED
private var groupIcon: FeedGroupIcon? = null
@ -66,22 +56,19 @@ class FeedGroupDialog : DialogFragment() {
object DeleteScreen : ScreenState()
}
@State
@JvmField
var selectedIcon: FeedGroupIcon? = null
@State
@JvmField
var selectedSubscriptions: HashSet<Long> = HashSet()
@State
@JvmField
var currentScreen: ScreenState = InitialScreen
@State @JvmField var selectedIcon: FeedGroupIcon? = null
@State @JvmField var selectedSubscriptions: HashSet<Long> = HashSet()
@State @JvmField var wasSubscriptionSelectionChanged: Boolean = false
@State @JvmField var currentScreen: ScreenState = InitialScreen
@State
@JvmField
var subscriptionsListState: Parcelable? = null
@State
@JvmField
var iconsListState: Parcelable? = null
@State @JvmField var subscriptionsListState: Parcelable? = null
@State @JvmField var iconsListState: Parcelable? = null
@State @JvmField var wasSearchSubscriptionsVisible = false
@State @JvmField var subscriptionsCurrentSearchQuery = ""
private val subscriptionMainSection = Section()
private val subscriptionEmptyFooter = Section()
private lateinit var subscriptionGroupAdapter: GroupAdapter<GroupieViewHolder>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -91,22 +78,30 @@ class FeedGroupDialog : DialogFragment() {
groupId = arguments?.getLong(KEY_GROUP_ID, NO_GROUP_SELECTED) ?: NO_GROUP_SELECTED
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.dialog_feed_group_create, container)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return object : Dialog(requireActivity(), theme) {
override fun onBackPressed() {
if (currentScreen !is InitialScreen) {
showScreen(InitialScreen)
} else {
if (!this@FeedGroupDialog.onBackPressed()) {
super.onBackPressed()
}
}
}
}
override fun onPause() {
super.onPause()
wasSearchSubscriptionsVisible = isSearchVisible()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
@ -119,11 +114,15 @@ class FeedGroupDialog : DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this, FeedGroupDialogViewModel.Factory(requireContext(), groupId))
.get(FeedGroupDialogViewModel::class.java)
viewModel = ViewModelProvider(this,
FeedGroupDialogViewModel.Factory(requireContext(),
groupId, subscriptionsCurrentSearchQuery)
).get(FeedGroupDialogViewModel::class.java)
viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup))
viewModel.subscriptionsLiveData.observe(viewLifecycleOwner, Observer { setupSubscriptionPicker(it.first, it.second) })
viewModel.subscriptionsLiveData.observe(viewLifecycleOwner, Observer {
setupSubscriptionPicker(it.first, it.second)
})
viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer {
when (it) {
ProcessingEvent -> disableInput()
@ -131,15 +130,54 @@ class FeedGroupDialog : DialogFragment() {
}
})
subscriptionGroupAdapter = GroupAdapter<GroupieViewHolder>().apply {
add(subscriptionMainSection)
add(subscriptionEmptyFooter)
spanCount = 4
}
subscriptions_selector_list.apply {
// Disable animations, too distracting.
itemAnimator = null
adapter = subscriptionGroupAdapter
layoutManager = GridLayoutManager(requireContext(), subscriptionGroupAdapter.spanCount,
RecyclerView.VERTICAL, false).apply {
spanSizeLookup = subscriptionGroupAdapter.spanSizeLookup
}
}
setupIconPicker()
setupListeners()
showScreen(currentScreen)
if (currentScreen == SubscriptionsPickerScreen && wasSearchSubscriptionsVisible) {
showSearch()
} else if (currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED) {
showKeyboard()
}
}
// /////////////////////////////////////////////////////////////////////////
override fun onDestroyView() {
super.onDestroyView()
subscriptions_selector_list?.adapter = null
icon_selector?.adapter = null
}
/*///////////////////////////////////////////////////////////////////////////
// Setup
// /////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////// */
override fun onBackPressed(): Boolean {
if (currentScreen is SubscriptionsPickerScreen && isSearchVisible()) {
hideSearch()
return true
} else if (currentScreen !is InitialScreen) {
showScreen(InitialScreen)
return true
}
return false
}
private fun setupListeners() {
delete_button.setOnClickListener { showScreen(DeleteScreen) }
@ -163,13 +201,54 @@ class FeedGroupDialog : DialogFragment() {
}
})
confirm_button.setOnClickListener {
when (currentScreen) {
InitialScreen -> handlePositiveButtonInitialScreen()
DeleteScreen -> viewModel.deleteGroup()
else -> showScreen(InitialScreen)
confirm_button.setOnClickListener { handlePositiveButton() }
select_channel_button.setOnClickListener {
subscriptions_selector_list.scrollToPosition(0)
showScreen(SubscriptionsPickerScreen)
}
val headerMenu = subscriptions_header_toolbar.menu
requireActivity().menuInflater.inflate(R.menu.menu_feed_group_dialog, headerMenu)
headerMenu.findItem(R.id.action_search).setOnMenuItemClickListener {
showSearch()
true
}
toolbar_search_clear.setOnClickListener {
if (TextUtils.isEmpty(toolbar_search_edit_text.text)) {
hideSearch()
return@setOnClickListener
}
resetSearch()
showKeyboardSearch()
}
toolbar_search_edit_text.setOnClickListener {
if (AndroidTvUtils.isTv(context)) {
showKeyboardSearch()
}
}
toolbar_search_edit_text.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit
override fun afterTextChanged(s: Editable) = Unit
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
val newQuery: String = toolbar_search_edit_text.text.toString()
subscriptionsCurrentSearchQuery = newQuery
viewModel.filterSubscriptionsBy(newQuery)
}
})
subscriptionGroupAdapter?.setOnItemClickListener(subscriptionPickerItemListener)
}
private fun handlePositiveButton() = when {
currentScreen is InitialScreen -> handlePositiveButtonInitialScreen()
currentScreen is DeleteScreen -> viewModel.deleteGroup()
currentScreen is SubscriptionsPickerScreen && isSearchVisible() -> hideSearch()
else -> showScreen(InitialScreen)
}
private fun handlePositiveButtonInitialScreen() {
@ -202,80 +281,73 @@ class FeedGroupDialog : DialogFragment() {
groupIcon = feedGroupEntity?.icon
groupSortOrder = feedGroupEntity?.sortOrder ?: -1
icon_preview.setImageResource((if (selectedIcon == null) icon else selectedIcon!!).getDrawableRes(requireContext()))
val feedGroupIcon = if (selectedIcon == null) icon else selectedIcon!!
icon_preview.setImageResource(feedGroupIcon.getDrawableRes(requireContext()))
if (group_name_input.text.isNullOrBlank()) {
group_name_input.setText(name)
}
}
private fun setupSubscriptionPicker(subscriptions: List<SubscriptionEntity>, selectedSubscriptions: Set<Long>) {
this.selectedSubscriptions.addAll(selectedSubscriptions)
val useGridLayout = subscriptions.isNotEmpty()
private val subscriptionPickerItemListener = OnItemClickListener { item, view ->
if (item is PickerSubscriptionItem) {
val subscriptionId = item.subscriptionEntity.uid
wasSubscriptionSelectionChanged = true
val groupAdapter = GroupAdapter<GroupieViewHolder>()
groupAdapter.spanCount = if (useGridLayout) 4 else 1
val subscriptionsCount = this.selectedSubscriptions.size
val selectedCountText = resources.getQuantityString(R.plurals.feed_group_dialog_selection_count, subscriptionsCount, subscriptionsCount)
selected_subscription_count_view.text = selectedCountText
subscriptions_selector_header_info.text = selectedCountText
Section().apply {
addAll(subscriptions.map {
val isSelected = this@FeedGroupDialog.selectedSubscriptions.contains(it.uid)
PickerSubscriptionItem(it, isSelected)
})
setPlaceholder(EmptyPlaceholderItem())
groupAdapter.add(this)
}
subscriptions_selector_list.apply {
layoutManager = if (useGridLayout) {
GridLayoutManager(requireContext(), groupAdapter.spanCount, RecyclerView.VERTICAL, false)
val isSelected = if (this.selectedSubscriptions.contains(subscriptionId)) {
this.selectedSubscriptions.remove(subscriptionId)
false
} else {
LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
this.selectedSubscriptions.add(subscriptionId)
true
}
adapter = groupAdapter
item.updateSelected(view, isSelected)
updateSubscriptionSelectedCount()
}
}
if (subscriptionsListState != null) {
layoutManager?.onRestoreInstanceState(subscriptionsListState)
subscriptionsListState = null
}
private fun setupSubscriptionPicker(
subscriptions: List<PickerSubscriptionItem>,
selectedSubscriptions: Set<Long>
) {
if (!wasSubscriptionSelectionChanged) {
this.selectedSubscriptions.addAll(selectedSubscriptions)
}
groupAdapter.setOnItemClickListener { item, _ ->
when (item) {
is PickerSubscriptionItem -> {
val subscriptionId = item.subscriptionEntity.uid
updateSubscriptionSelectedCount()
val isSelected = if (this.selectedSubscriptions.contains(subscriptionId)) {
this.selectedSubscriptions.remove(subscriptionId)
false
} else {
this.selectedSubscriptions.add(subscriptionId)
true
}
item.isSelected = isSelected
item.notifyChanged(PickerSubscriptionItem.UPDATE_SELECTED)
val subscriptionsCount = this.selectedSubscriptions.size
val updateSelectedCountText = resources.getQuantityString(R.plurals.feed_group_dialog_selection_count, subscriptionsCount, subscriptionsCount)
selected_subscription_count_view.text = updateSelectedCountText
subscriptions_selector_header_info.text = updateSelectedCountText
}
}
if (subscriptions.isEmpty()) {
subscriptionEmptyFooter.clear()
subscriptionEmptyFooter.add(EmptyPlaceholderItem())
} else {
subscriptionEmptyFooter.clear()
}
select_channel_button.setOnClickListener {
subscriptions.forEach {
it.isSelected = this@FeedGroupDialog.selectedSubscriptions
.contains(it.subscriptionEntity.uid)
}
subscriptionMainSection.update(subscriptions, false)
if (subscriptionsListState != null) {
subscriptions_selector_list.layoutManager?.onRestoreInstanceState(subscriptionsListState)
subscriptionsListState = null
} else {
subscriptions_selector_list.scrollToPosition(0)
showScreen(SubscriptionsPickerScreen)
}
}
private fun updateSubscriptionSelectedCount() {
val selectedCount = this.selectedSubscriptions.size
val selectedCountText = resources.getQuantityString(
R.plurals.feed_group_dialog_selection_count,
selectedCount, selectedCount)
selected_subscription_count_view.text = selectedCountText
subscriptions_header_info.text = selectedCountText
}
private fun setupIconPicker() {
val groupAdapter = GroupAdapter<GroupieViewHolder>()
groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(requireContext(), it) })
@ -311,9 +383,9 @@ class FeedGroupDialog : DialogFragment() {
}
}
// /////////////////////////////////////////////////////////////////////////
/*///////////////////////////////////////////////////////////////////////////
// Screen Selector
// /////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////// */
private fun showScreen(screen: ScreenState) {
currentScreen = screen
@ -337,7 +409,8 @@ class FeedGroupDialog : DialogFragment() {
else -> View.VISIBLE
}
if (currentScreen != InitialScreen) hideKeyboard()
hideKeyboard()
hideSearch()
}
private fun View.onlyVisibleIn(vararg screens: ScreenState) {
@ -347,13 +420,58 @@ class FeedGroupDialog : DialogFragment() {
}
}
// /////////////////////////////////////////////////////////////////////////
/*///////////////////////////////////////////////////////////////////////////
// Utils
// /////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////// */
private fun isSearchVisible() = subscriptions_header_search_container?.visibility == View.VISIBLE
private fun resetSearch() {
toolbar_search_edit_text.setText("")
subscriptionsCurrentSearchQuery = ""
viewModel.clearSubscriptionsFilter()
}
private fun hideSearch() {
resetSearch()
subscriptions_header_search_container.visibility = View.GONE
subscriptions_header_info_container.visibility = View.VISIBLE
subscriptions_header_toolbar.menu.findItem(R.id.action_search).isVisible = true
hideKeyboardSearch()
}
private fun showSearch() {
subscriptions_header_search_container.visibility = View.VISIBLE
subscriptions_header_info_container.visibility = View.GONE
subscriptions_header_toolbar.menu.findItem(R.id.action_search).isVisible = false
showKeyboardSearch()
}
private val inputMethodManager by lazy {
requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
}
private fun showKeyboardSearch() {
if (toolbar_search_edit_text.requestFocus()) {
inputMethodManager.showSoftInput(toolbar_search_edit_text, InputMethodManager.SHOW_IMPLICIT)
}
}
private fun hideKeyboardSearch() {
inputMethodManager.hideSoftInputFromWindow(toolbar_search_edit_text.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN)
toolbar_search_edit_text.clearFocus()
}
private fun showKeyboard() {
if (group_name_input.requestFocus()) {
inputMethodManager.showSoftInput(group_name_input, InputMethodManager.SHOW_IMPLICIT)
}
}
private fun hideKeyboard() {
val inputMethodManager = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(group_name_input.windowToken, InputMethodManager.RESULT_UNCHANGED_SHOWN)
inputMethodManager.hideSoftInputFromWindow(group_name_input.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN)
group_name_input.clearFocus()
}

View File

@ -9,42 +9,55 @@ import io.reactivex.Completable
import io.reactivex.Flowable
import io.reactivex.disposables.Disposable
import io.reactivex.functions.BiFunction
import io.reactivex.processors.BehaviorProcessor
import io.reactivex.schedulers.Schedulers
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.subscription.FeedGroupIcon
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem
class FeedGroupDialogViewModel(applicationContext: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModel() {
class Factory(val context: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return FeedGroupDialogViewModel(context.applicationContext, groupId) as T
}
}
class FeedGroupDialogViewModel(
applicationContext: Context,
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
initialQuery: String = ""
) : ViewModel() {
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
private var subscriptionManager = SubscriptionManager(applicationContext)
private var filterSubscriptions = BehaviorProcessor.create<String>()
private var allSubscriptions = subscriptionManager.subscriptions()
private var subscriptionsFlowable = filterSubscriptions
.startWith(initialQuery)
.distinctUntilChanged()
.switchMap { query ->
if (query.isEmpty()) {
allSubscriptions
} else {
subscriptionManager.filterByName(query)
}
}.map { list -> list.map { PickerSubscriptionItem(it) } }
private val mutableGroupLiveData = MutableLiveData<FeedGroupEntity>()
private val mutableSubscriptionsLiveData = MutableLiveData<Pair<List<SubscriptionEntity>, Set<Long>>>()
private val mutableSubscriptionsLiveData = MutableLiveData<Pair<List<PickerSubscriptionItem>, Set<Long>>>()
private val mutableDialogEventLiveData = MutableLiveData<DialogEvent>()
val groupLiveData: LiveData<FeedGroupEntity> = mutableGroupLiveData
val subscriptionsLiveData: LiveData<Pair<List<SubscriptionEntity>, Set<Long>>> = mutableSubscriptionsLiveData
val subscriptionsLiveData: LiveData<Pair<List<PickerSubscriptionItem>, Set<Long>>> = mutableSubscriptionsLiveData
val dialogEventLiveData: LiveData<DialogEvent> = mutableDialogEventLiveData
private var actionProcessingDisposable: Disposable? = null
private var feedGroupDisposable = feedDatabaseManager.getGroup(groupId)
.subscribeOn(Schedulers.io())
.subscribe(mutableGroupLiveData::postValue)
.subscribeOn(Schedulers.io())
.subscribe(mutableGroupLiveData::postValue)
private var subscriptionsDisposable = Flowable
.combineLatest(subscriptionManager.subscriptions(), feedDatabaseManager.subscriptionIdsForGroup(groupId),
BiFunction { t1: List<SubscriptionEntity>, t2: List<Long> -> t1 to t2.toSet() })
.subscribeOn(Schedulers.io())
.subscribe(mutableSubscriptionsLiveData::postValue)
.combineLatest(subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId),
BiFunction { t1: List<PickerSubscriptionItem>, t2: List<Long> -> t1 to t2.toSet() })
.subscribeOn(Schedulers.io())
.subscribe(mutableSubscriptionsLiveData::postValue)
override fun onCleared() {
super.onCleared()
@ -55,14 +68,14 @@ class FeedGroupDialogViewModel(applicationContext: Context, val groupId: Long =
fun createGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set<Long>) {
doAction(feedDatabaseManager.createGroup(name, selectedIcon)
.flatMapCompletable {
feedDatabaseManager.updateSubscriptionsForGroup(it, selectedSubscriptions.toList())
})
.flatMapCompletable {
feedDatabaseManager.updateSubscriptionsForGroup(it, selectedSubscriptions.toList())
})
}
fun updateGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set<Long>, sortOrder: Long) {
doAction(feedDatabaseManager.updateSubscriptionsForGroup(groupId, selectedSubscriptions.toList())
.andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon, sortOrder))))
.andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon, sortOrder))))
}
fun deleteGroup() {
@ -74,13 +87,33 @@ class FeedGroupDialogViewModel(applicationContext: Context, val groupId: Long =
mutableDialogEventLiveData.value = DialogEvent.ProcessingEvent
actionProcessingDisposable = completable
.subscribeOn(Schedulers.io())
.subscribe { mutableDialogEventLiveData.postValue(DialogEvent.SuccessEvent) }
.subscribeOn(Schedulers.io())
.subscribe { mutableDialogEventLiveData.postValue(DialogEvent.SuccessEvent) }
}
}
fun filterSubscriptionsBy(query: String) {
filterSubscriptions.onNext(query)
}
fun clearSubscriptionsFilter() {
filterSubscriptions.onNext("")
}
sealed class DialogEvent {
object ProcessingEvent : DialogEvent()
object SuccessEvent : DialogEvent()
}
class Factory(
private val context: Context,
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
private val initialQuery: String = ""
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return FeedGroupDialogViewModel(context.applicationContext,
groupId, initialQuery) as T
}
}
}

View File

@ -7,4 +7,5 @@ import org.schabi.newpipe.R
class EmptyPlaceholderItem : Item() {
override fun getLayout(): Int = R.layout.list_empty_view
override fun bind(viewHolder: GroupieViewHolder, position: Int) {}
override fun getSpanSize(spanCount: Int, position: Int): Int = spanCount
}

View File

@ -1,39 +1,28 @@
package org.schabi.newpipe.local.subscription.item
import android.view.View
import com.nostra13.universalimageloader.core.DisplayImageOptions
import com.nostra13.universalimageloader.core.ImageLoader
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import com.xwray.groupie.kotlinandroidextensions.Item
import kotlinx.android.synthetic.main.picker_subscription_item.selected_highlight
import kotlinx.android.synthetic.main.picker_subscription_item.thumbnail_view
import kotlinx.android.synthetic.main.picker_subscription_item.title_view
import kotlinx.android.synthetic.main.picker_subscription_item.*
import kotlinx.android.synthetic.main.picker_subscription_item.view.*
import org.schabi.newpipe.R
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.util.AnimationUtils
import org.schabi.newpipe.util.AnimationUtils.animateView
import org.schabi.newpipe.util.ImageDisplayConstants
data class PickerSubscriptionItem(val subscriptionEntity: SubscriptionEntity, var isSelected: Boolean = false) : Item() {
companion object {
const val UPDATE_SELECTED = 123
val IMAGE_LOADING_OPTIONS: DisplayImageOptions = ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS
}
data class PickerSubscriptionItem(
val subscriptionEntity: SubscriptionEntity,
var isSelected: Boolean = false
) : Item() {
override fun getId(): Long = subscriptionEntity.uid
override fun getLayout(): Int = R.layout.picker_subscription_item
override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList<Any>) {
if (payloads.contains(UPDATE_SELECTED)) {
animateView(viewHolder.selected_highlight, AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, isSelected, 150)
return
}
super.bind(viewHolder, position, payloads)
}
override fun getSpanSize(spanCount: Int, position: Int): Int = 1
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
ImageLoader.getInstance().displayImage(subscriptionEntity.avatarUrl, viewHolder.thumbnail_view, IMAGE_LOADING_OPTIONS)
ImageLoader.getInstance().displayImage(subscriptionEntity.avatarUrl,
viewHolder.thumbnail_view, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS)
viewHolder.title_view.text = subscriptionEntity.name
viewHolder.selected_highlight.visibility = if (isSelected) View.VISIBLE else View.GONE
@ -47,7 +36,9 @@ data class PickerSubscriptionItem(val subscriptionEntity: SubscriptionEntity, va
viewHolder.selected_highlight.alpha = 1F
}
override fun getId(): Long {
return subscriptionEntity.uid
fun updateSelected(containerView: View, isSelected: Boolean) {
this.isSelected = isSelected
animateView(containerView.selected_highlight,
AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, isSelected, 150)
}
}

View File

@ -102,42 +102,56 @@
android:id="@+id/subscriptions_selector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
android:orientation="vertical">
<LinearLayout
<androidx.appcompat.widget.Toolbar
android:id="@+id/subscriptions_header_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingTop="12dp"
android:paddingRight="16dp"
android:paddingBottom="12dp">
android:layout_height="?attr/actionBarSize"
android:gravity="center_vertical"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.ActionBar"
app:titleTextAppearance="@style/Toolbar.Title">
<TextView
android:layout_width="0dp"
<LinearLayout
android:id="@+id/subscriptions_header_info_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start|center_vertical"
android:text="@string/tab_subscriptions"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:textStyle="bold" />
android:orientation="vertical"
android:gravity="center_vertical">
<TextView
android:id="@+id/subscriptions_selector_header_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:textColor="?android:attr/textColorPrimary"
android:textSize="12sp"
tools:text="1 selected" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:text="@string/tab_subscriptions"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/subscriptions_header_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start|center_vertical"
android:textColor="?android:attr/textColorPrimary"
android:textSize="12sp"
tools:text="1 selected" />
</LinearLayout>
<include
android:id="@+id/subscriptions_header_search_container"
layout="@layout/toolbar_search_layout"
android:visibility="gone"/>
</androidx.appcompat.widget.Toolbar>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/subscriptions_selector_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="2dp"
android:clipToPadding="false"
tools:itemCount="200"
tools:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
tools:listitem="@layout/picker_subscription_item"

View File

@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="?attr/colorPrimary">
tools:background="?attr/colorPrimary">
<EditText
android:id="@+id/toolbar_search_edit_text"
@ -14,7 +15,7 @@
android:layout_marginBottom="4dp"
android:layout_marginRight="48dp"
android:layout_marginTop="4dp"
android:background="?attr/colorPrimary"
android:background="@null"
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusDown="@+id/suggestions_list"
@ -29,6 +30,7 @@
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="right|center_vertical"
android:contentDescription="@string/clear"
android:focusable="true"
tools:ignore="RtlHardcoded">

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="?attr/ic_search"
android:title="@string/search"
app:showAsAction="always" />
</menu>