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/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; + } } 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