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 573fa4b9023..60dd343b9f8 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,45 @@ 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 getSubscriptionsFiltered(filter: String): Flowable> + + @Query(""" + SELECT * FROM subscriptions s + + LEFT JOIN feed_group_subscription_join fgs + ON s.uid = fgs.subscription_id + + WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId) + + ORDER BY name COLLATE NOCASE ASC + """) + abstract fun getSubscriptionsOnlyUngrouped( + currentGroupId: Long + ): Flowable> + + @Query(""" + SELECT * FROM subscriptions s + + LEFT JOIN feed_group_subscription_join fgs + ON s.uid = fgs.subscription_id + + WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId) + AND s.name LIKE '%' || :filter || '%' + + ORDER BY name COLLATE NOCASE ASC + """) + abstract fun getSubscriptionsOnlyUngroupedFiltered( + currentGroupId: Long, + filter: String + ): Flowable> + @Query("SELECT * FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId") abstract fun getSubscriptionFlowable(serviceId: Int, url: String): Flowable> @@ -52,7 +91,7 @@ abstract class SubscriptionDAO : BasicDAO { entity.uid = uidFromInsert } else { val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url) - ?: throw IllegalStateException("Subscription cannot be null just after insertion.") + ?: throw IllegalStateException("Subscription cannot be null just after insertion.") entity.uid = subscriptionIdFromDb update(entity) 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 cc7219543d2..a47f17d1346 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 ChannelInfoItem toChannelInfoItem() { 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 92ab8cb0ce7..2740591e662 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 @@ -2,9 +2,11 @@ package org.schabi.newpipe.local.subscription import android.content.Context import io.reactivex.Completable +import io.reactivex.Flowable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import org.schabi.newpipe.NewPipeDatabase +import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.subscription.SubscriptionDAO import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.extractor.ListInfo @@ -21,9 +23,28 @@ class SubscriptionManager(context: Context) { fun subscriptionTable(): SubscriptionDAO = subscriptionTable fun subscriptions() = subscriptionTable.all + fun getSubscriptions( + currentGroupId: Long = FeedGroupEntity.GROUP_ALL_ID, + filterQuery: String = "", + showOnlyUngrouped: Boolean = false + ): Flowable> { + return when { + filterQuery.isNotEmpty() -> { + return if (showOnlyUngrouped) { + subscriptionTable.getSubscriptionsOnlyUngroupedFiltered( + currentGroupId, filterQuery) + } else { + subscriptionTable.getSubscriptionsFiltered(filterQuery) + } + } + showOnlyUngrouped -> subscriptionTable.getSubscriptionsOnlyUngrouped(currentGroupId) + else -> subscriptionTable.all + } + } + fun upsertAll(infoList: List): List { val listEntities = subscriptionTable.upsertAll( - infoList.map { SubscriptionEntity.from(it) }) + infoList.map { SubscriptionEntity.from(it) }) database.runInTransaction { infoList.forEachIndexed { index, info -> @@ -35,13 +56,13 @@ class SubscriptionManager(context: Context) { } fun updateChannelInfo(info: ChannelInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url) - .flatMapCompletable { - Completable.fromRunnable { - it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount) - subscriptionTable.update(it) - feedDatabaseManager.upsertAll(it.uid, info.relatedItems) - } + .flatMapCompletable { + Completable.fromRunnable { + it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount) + subscriptionTable.update(it) + feedDatabaseManager.upsertAll(it.uid, info.relatedItems) } + } fun updateFromInfo(subscriptionId: Long, info: ListInfo) { val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId) @@ -57,8 +78,8 @@ class SubscriptionManager(context: Context) { fun deleteSubscription(serviceId: Int, url: String): Completable { return Completable.fromCallable { subscriptionTable.deleteSubscription(serviceId, url) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) } fun insertSubscription(subscriptionEntity: SubscriptionEntity, info: ChannelInfo) { 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 e9d9ac5b372..66387d29852 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,20 @@ 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 subscriptionsListState: Parcelable? = null - @State - @JvmField - var iconsListState: Parcelable? = null + @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 wasSearchSubscriptionsVisible = false + @State @JvmField var subscriptionsCurrentSearchQuery = "" + @State @JvmField var subscriptionsShowOnlyUngrouped = false + + private val subscriptionMainSection = Section() + private val subscriptionEmptyFooter = Section() + private lateinit var subscriptionGroupAdapter: GroupAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -91,22 +79,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 +115,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, subscriptionsShowOnlyUngrouped) + ).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 +131,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 +202,64 @@ 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 + } + + headerMenu.findItem(R.id.feed_group_toggle_show_only_ungrouped_subscriptions).apply { + isChecked = subscriptionsShowOnlyUngrouped + setOnMenuItemClickListener { + subscriptionsShowOnlyUngrouped = !subscriptionsShowOnlyUngrouped + it.isChecked = subscriptionsShowOnlyUngrouped + viewModel.toggleShowOnlyUngrouped(subscriptionsShowOnlyUngrouped) + 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 +292,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()) + val isSelected = if (this.selectedSubscriptions.contains(subscriptionId)) { + this.selectedSubscriptions.remove(subscriptionId) + false + } else { + this.selectedSubscriptions.add(subscriptionId) + true + } - groupAdapter.add(this) + item.updateSelected(view, isSelected) + updateSubscriptionSelectedCount() } + } - subscriptions_selector_list.apply { - layoutManager = if (useGridLayout) { - GridLayoutManager(requireContext(), groupAdapter.spanCount, RecyclerView.VERTICAL, false) - } else { - LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) - } + private fun setupSubscriptionPicker( + subscriptions: List, + selectedSubscriptions: Set + ) { + if (!wasSubscriptionSelectionChanged) { + this.selectedSubscriptions.addAll(selectedSubscriptions) + } - adapter = groupAdapter + updateSubscriptionSelectedCount() - if (subscriptionsListState != null) { - layoutManager?.onRestoreInstanceState(subscriptionsListState) - subscriptionsListState = null - } + if (subscriptions.isEmpty()) { + subscriptionEmptyFooter.clear() + subscriptionEmptyFooter.add(EmptyPlaceholderItem()) + } else { + subscriptionEmptyFooter.clear() } - groupAdapter.setOnItemClickListener { item, _ -> - when (item) { - is PickerSubscriptionItem -> { - val subscriptionId = item.subscriptionEntity.uid - - 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 - } - } + subscriptions.forEach { + it.isSelected = this@FeedGroupDialog.selectedSubscriptions + .contains(it.subscriptionEntity.uid) } - select_channel_button.setOnClickListener { + 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 +394,9 @@ class FeedGroupDialog : DialogFragment() { } } - // ///////////////////////////////////////////////////////////////////////// + /*/​////////////////////////////////////////////////////////////////////////// // Screen Selector - // ///////////////////////////////////////////////////////////////////////// + //​//////////////////////////////////////////////////////////////////////// */ private fun showScreen(screen: ScreenState) { currentScreen = screen @@ -337,7 +420,8 @@ class FeedGroupDialog : DialogFragment() { else -> View.VISIBLE } - if (currentScreen != InitialScreen) hideKeyboard() + hideKeyboard() + hideSearch() } private fun View.onlyVisibleIn(vararg screens: ScreenState) { @@ -347,13 +431,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 ac00245e694..e9a7e4eb70a 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,56 @@ 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 = "", + initialShowOnlyUngrouped: Boolean = false +) : ViewModel() { private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext) private var subscriptionManager = SubscriptionManager(applicationContext) + private var filterSubscriptions = BehaviorProcessor.create() + private var toggleShowOnlyUngrouped = BehaviorProcessor.create() + + private var subscriptionsFlowable = Flowable + .combineLatest( + filterSubscriptions.startWith(initialQuery), + toggleShowOnlyUngrouped.startWith(initialShowOnlyUngrouped), + BiFunction { t1: String, t2: Boolean -> Filter(t1, t2) } + ) + .distinctUntilChanged() + .switchMap { filter -> + subscriptionManager.getSubscriptions(groupId, filter.query, filter.showOnlyUngrouped) + }.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 +69,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 +88,40 @@ 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("") + } + + fun toggleShowOnlyUngrouped(showOnlyUngrouped: Boolean) { + toggleShowOnlyUngrouped.onNext(showOnlyUngrouped) + } + sealed class DialogEvent { object ProcessingEvent : DialogEvent() object SuccessEvent : DialogEvent() } + + data class Filter(val query: String, val showOnlyUngrouped: Boolean) + + class Factory( + private val context: Context, + private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID, + private val initialQuery: String = "", + private val initialShowOnlyUngrouped: Boolean = false + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return FeedGroupDialogViewModel(context.applicationContext, + groupId, initialQuery, initialShowOnlyUngrouped) 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 c806277ee43..ef7eb93cd00 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 d90ac0d82f4..7d33da71f3a 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 2bd0e1141e8..17893fecc42 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: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 34e659ece84..9a7d56a6e1a 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 00000000000..af9be1c65b5 --- /dev/null +++ b/app/src/main/res/menu/menu_feed_group_dialog.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f2e79a79b39..a4dfd255d6d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -637,6 +637,7 @@ Name Do you want to delete this group? New + Show only ungrouped subscriptions Feed Feed update threshold Time after last update before a subscription is considered outdated — %s