From c24dfc63dce8caa161d76b7cb5743a082b33caa0 Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Sun, 5 Apr 2020 16:25:44 -0300 Subject: [PATCH 1/2] Add search for subscription picker in the feed group dialog --- .../database/subscription/SubscriptionDAO.kt | 7 + .../local/subscription/SubscriptionManager.kt | 2 + .../subscription/dialog/FeedGroupDialog.kt | 336 ++++++++++++------ .../dialog/FeedGroupDialogViewModel.kt | 77 ++-- .../subscription/item/EmptyPlaceholderItem.kt | 1 + .../item/PickerSubscriptionItem.kt | 37 +- .../res/layout/dialog_feed_group_create.xml | 68 ++-- .../main/res/layout/toolbar_search_layout.xml | 10 +- .../main/res/menu/menu_feed_group_dialog.xml | 10 + 9 files changed, 363 insertions(+), 185 deletions(-) create mode 100644 app/src/main/res/menu/menu_feed_group_dialog.xml diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt index 573fa4b90..aa34a2867 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt @@ -20,6 +20,13 @@ abstract class SubscriptionDAO : BasicDAO { @Query("SELECT * FROM subscriptions ORDER BY name COLLATE NOCASE ASC") abstract override fun getAll(): Flowable> + @Query(""" + SELECT * FROM subscriptions + WHERE name LIKE '%' || :filter || '%' + ORDER BY name COLLATE NOCASE ASC + """) + abstract fun filterByName(filter: String): Flowable> + @Query("SELECT * FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId") abstract fun getSubscriptionFlowable(serviceId: Int, url: String): Flowable> diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt index 92ab8cb0c..ce272c856 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt @@ -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): List { val listEntities = subscriptionTable.upsertAll( infoList.map { SubscriptionEntity.from(it) }) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt index e9d9ac5b3..0b77ec1d8 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt @@ -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 = HashSet() - @State - @JvmField - var currentScreen: ScreenState = InitialScreen + @State @JvmField var selectedIcon: FeedGroupIcon? = null + @State @JvmField var selectedSubscriptions: HashSet = 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 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().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, selectedSubscriptions: Set) { - 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() - 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, + selectedSubscriptions: Set + ) { + 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() 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() } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt index ac00245e6..a7ab300f2 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt @@ -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 create(modelClass: Class): 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() + 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() - private val mutableSubscriptionsLiveData = MutableLiveData, Set>>() + private val mutableSubscriptionsLiveData = MutableLiveData, Set>>() private val mutableDialogEventLiveData = MutableLiveData() val groupLiveData: LiveData = mutableGroupLiveData - val subscriptionsLiveData: LiveData, Set>> = mutableSubscriptionsLiveData + val subscriptionsLiveData: LiveData, Set>> = mutableSubscriptionsLiveData val dialogEventLiveData: LiveData = 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, t2: List -> t1 to t2.toSet() }) - .subscribeOn(Schedulers.io()) - .subscribe(mutableSubscriptionsLiveData::postValue) + .combineLatest(subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId), + BiFunction { t1: List, t2: List -> 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) { 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, 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 create(modelClass: Class): T { + return FeedGroupDialogViewModel(context.applicationContext, + groupId, initialQuery) as T + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/EmptyPlaceholderItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/EmptyPlaceholderItem.kt index c806277ee..ef7eb93cd 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/EmptyPlaceholderItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/EmptyPlaceholderItem.kt @@ -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 } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt index d90ac0d82..7d33da71f 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt @@ -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) { - 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) } } diff --git a/app/src/main/res/layout/dialog_feed_group_create.xml b/app/src/main/res/layout/dialog_feed_group_create.xml index 2bd0e1141..17893fecc 100644 --- a/app/src/main/res/layout/dialog_feed_group_create.xml +++ b/app/src/main/res/layout/dialog_feed_group_create.xml @@ -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"> - + 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"> - + android:orientation="vertical" + android:gravity="center_vertical"> - - + + + + + + + + - \ No newline at end of file + diff --git a/app/src/main/res/layout/toolbar_search_layout.xml b/app/src/main/res/layout/toolbar_search_layout.xml index 34e659ece..9a7d56a6e 100644 --- a/app/src/main/res/layout/toolbar_search_layout.xml +++ b/app/src/main/res/layout/toolbar_search_layout.xml @@ -1,10 +1,11 @@ - + tools:background="?attr/colorPrimary"> diff --git a/app/src/main/res/menu/menu_feed_group_dialog.xml b/app/src/main/res/menu/menu_feed_group_dialog.xml new file mode 100644 index 000000000..390088b39 --- /dev/null +++ b/app/src/main/res/menu/menu_feed_group_dialog.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file From 9f3b35634a66ad07776145020c30a553482af478 Mon Sep 17 00:00:00 2001 From: Mauricio Colli Date: Sun, 5 Apr 2020 16:27:53 -0300 Subject: [PATCH 2/2] Fix subscription picker items flickering in the feed group dialog The adapter could not tell the items were the same because the subscription class was missing some methods (i.e. equals and hashcode), so a full rebind was being done. --- .../subscription/SubscriptionEntity.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java index cc7219543..a47f17d13 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java @@ -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; + } }