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

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.text.Editable import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -13,34 +14,22 @@ import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.xwray.groupie.GroupAdapter import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.Section import com.xwray.groupie.Section
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
import icepick.Icepick import icepick.Icepick
import icepick.State import icepick.State
import java.io.Serializable import java.io.Serializable
import kotlinx.android.synthetic.main.dialog_feed_group_create.cancel_button import kotlin.collections.contains
import kotlinx.android.synthetic.main.dialog_feed_group_create.confirm_button import kotlinx.android.synthetic.main.dialog_feed_group_create.*
import kotlinx.android.synthetic.main.dialog_feed_group_create.delete_button import kotlinx.android.synthetic.main.toolbar_search_layout.*
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 org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity 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.FeedGroupIcon
import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.DeleteScreen import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.DeleteScreen
import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.IconPickerScreen 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.EmptyPlaceholderItem
import org.schabi.newpipe.local.subscription.item.PickerIconItem import org.schabi.newpipe.local.subscription.item.PickerIconItem
import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem
import org.schabi.newpipe.util.AndroidTvUtils
import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.ThemeHelper
class FeedGroupDialog : DialogFragment() { class FeedGroupDialog : DialogFragment(), BackPressable {
private lateinit var viewModel: FeedGroupDialogViewModel private lateinit var viewModel: FeedGroupDialogViewModel
private var groupId: Long = NO_GROUP_SELECTED private var groupId: Long = NO_GROUP_SELECTED
private var groupIcon: FeedGroupIcon? = null private var groupIcon: FeedGroupIcon? = null
@ -66,22 +56,19 @@ class FeedGroupDialog : DialogFragment() {
object DeleteScreen : ScreenState() object DeleteScreen : ScreenState()
} }
@State @State @JvmField var selectedIcon: FeedGroupIcon? = null
@JvmField @State @JvmField var selectedSubscriptions: HashSet<Long> = HashSet()
var selectedIcon: FeedGroupIcon? = null @State @JvmField var wasSubscriptionSelectionChanged: Boolean = false
@State @State @JvmField var currentScreen: ScreenState = InitialScreen
@JvmField
var selectedSubscriptions: HashSet<Long> = HashSet()
@State
@JvmField
var currentScreen: ScreenState = InitialScreen
@State @State @JvmField var subscriptionsListState: Parcelable? = null
@JvmField @State @JvmField var iconsListState: Parcelable? = null
var subscriptionsListState: Parcelable? = null @State @JvmField var wasSearchSubscriptionsVisible = false
@State @State @JvmField var subscriptionsCurrentSearchQuery = ""
@JvmField
var iconsListState: Parcelable? = null private val subscriptionMainSection = Section()
private val subscriptionEmptyFooter = Section()
private lateinit var subscriptionGroupAdapter: GroupAdapter<GroupieViewHolder>
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -91,22 +78,30 @@ class FeedGroupDialog : DialogFragment() {
groupId = arguments?.getLong(KEY_GROUP_ID, NO_GROUP_SELECTED) ?: NO_GROUP_SELECTED 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) return inflater.inflate(R.layout.dialog_feed_group_create, container)
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return object : Dialog(requireActivity(), theme) { return object : Dialog(requireActivity(), theme) {
override fun onBackPressed() { override fun onBackPressed() {
if (currentScreen !is InitialScreen) { if (!this@FeedGroupDialog.onBackPressed()) {
showScreen(InitialScreen)
} else {
super.onBackPressed() super.onBackPressed()
} }
} }
} }
} }
override fun onPause() {
super.onPause()
wasSearchSubscriptionsVisible = isSearchVisible()
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
@ -119,11 +114,15 @@ class FeedGroupDialog : DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this, FeedGroupDialogViewModel.Factory(requireContext(), groupId)) viewModel = ViewModelProvider(this,
.get(FeedGroupDialogViewModel::class.java) FeedGroupDialogViewModel.Factory(requireContext(),
groupId, subscriptionsCurrentSearchQuery)
).get(FeedGroupDialogViewModel::class.java)
viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup)) 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 { viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer {
when (it) { when (it) {
ProcessingEvent -> disableInput() ProcessingEvent -> disableInput()
@ -131,15 +130,54 @@ class FeedGroupDialog : DialogFragment() {
} }
}) })
subscriptionGroupAdapter = GroupAdapter<GroupieViewHolder>().apply {
add(subscriptionMainSection)
add(subscriptionEmptyFooter)
spanCount = 4
}
subscriptions_selector_list.apply {
// Disable animations, too distracting.
itemAnimator = null
adapter = subscriptionGroupAdapter
layoutManager = GridLayoutManager(requireContext(), subscriptionGroupAdapter.spanCount,
RecyclerView.VERTICAL, false).apply {
spanSizeLookup = subscriptionGroupAdapter.spanSizeLookup
}
}
setupIconPicker() setupIconPicker()
setupListeners() setupListeners()
showScreen(currentScreen) 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 // 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() { private fun setupListeners() {
delete_button.setOnClickListener { showScreen(DeleteScreen) } delete_button.setOnClickListener { showScreen(DeleteScreen) }
@ -163,13 +201,54 @@ class FeedGroupDialog : DialogFragment() {
} }
}) })
confirm_button.setOnClickListener { confirm_button.setOnClickListener { handlePositiveButton() }
when (currentScreen) {
InitialScreen -> handlePositiveButtonInitialScreen() select_channel_button.setOnClickListener {
DeleteScreen -> viewModel.deleteGroup() subscriptions_selector_list.scrollToPosition(0)
else -> showScreen(InitialScreen) 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() { private fun handlePositiveButtonInitialScreen() {
@ -202,80 +281,73 @@ class FeedGroupDialog : DialogFragment() {
groupIcon = feedGroupEntity?.icon groupIcon = feedGroupEntity?.icon
groupSortOrder = feedGroupEntity?.sortOrder ?: -1 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()) { if (group_name_input.text.isNullOrBlank()) {
group_name_input.setText(name) group_name_input.setText(name)
} }
} }
private fun setupSubscriptionPicker(subscriptions: List<SubscriptionEntity>, selectedSubscriptions: Set<Long>) { private val subscriptionPickerItemListener = OnItemClickListener { item, view ->
this.selectedSubscriptions.addAll(selectedSubscriptions) if (item is PickerSubscriptionItem) {
val useGridLayout = subscriptions.isNotEmpty() val subscriptionId = item.subscriptionEntity.uid
wasSubscriptionSelectionChanged = true
val groupAdapter = GroupAdapter<GroupieViewHolder>() val isSelected = if (this.selectedSubscriptions.contains(subscriptionId)) {
groupAdapter.spanCount = if (useGridLayout) 4 else 1 this.selectedSubscriptions.remove(subscriptionId)
false
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)
} else { } else {
LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) this.selectedSubscriptions.add(subscriptionId)
true
} }
adapter = groupAdapter item.updateSelected(view, isSelected)
updateSubscriptionSelectedCount()
}
}
if (subscriptionsListState != null) { private fun setupSubscriptionPicker(
layoutManager?.onRestoreInstanceState(subscriptionsListState) subscriptions: List<PickerSubscriptionItem>,
subscriptionsListState = null selectedSubscriptions: Set<Long>
} ) {
if (!wasSubscriptionSelectionChanged) {
this.selectedSubscriptions.addAll(selectedSubscriptions)
} }
groupAdapter.setOnItemClickListener { item, _ -> updateSubscriptionSelectedCount()
when (item) {
is PickerSubscriptionItem -> {
val subscriptionId = item.subscriptionEntity.uid
val isSelected = if (this.selectedSubscriptions.contains(subscriptionId)) { if (subscriptions.isEmpty()) {
this.selectedSubscriptions.remove(subscriptionId) subscriptionEmptyFooter.clear()
false subscriptionEmptyFooter.add(EmptyPlaceholderItem())
} else { } else {
this.selectedSubscriptions.add(subscriptionId) subscriptionEmptyFooter.clear()
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
}
}
} }
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) 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() { private fun setupIconPicker() {
val groupAdapter = GroupAdapter<GroupieViewHolder>() val groupAdapter = GroupAdapter<GroupieViewHolder>()
groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(requireContext(), it) }) groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(requireContext(), it) })
@ -311,9 +383,9 @@ class FeedGroupDialog : DialogFragment() {
} }
} }
// ///////////////////////////////////////////////////////////////////////// /*///////////////////////////////////////////////////////////////////////////
// Screen Selector // Screen Selector
// ///////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////// */
private fun showScreen(screen: ScreenState) { private fun showScreen(screen: ScreenState) {
currentScreen = screen currentScreen = screen
@ -337,7 +409,8 @@ class FeedGroupDialog : DialogFragment() {
else -> View.VISIBLE else -> View.VISIBLE
} }
if (currentScreen != InitialScreen) hideKeyboard() hideKeyboard()
hideSearch()
} }
private fun View.onlyVisibleIn(vararg screens: ScreenState) { private fun View.onlyVisibleIn(vararg screens: ScreenState) {
@ -347,13 +420,58 @@ class FeedGroupDialog : DialogFragment() {
} }
} }
// ///////////////////////////////////////////////////////////////////////// /*///////////////////////////////////////////////////////////////////////////
// Utils // 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() { private fun hideKeyboard() {
val inputMethodManager = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager inputMethodManager.hideSoftInputFromWindow(group_name_input.windowToken,
inputMethodManager.hideSoftInputFromWindow(group_name_input.windowToken, InputMethodManager.RESULT_UNCHANGED_SHOWN) InputMethodManager.RESULT_UNCHANGED_SHOWN)
group_name_input.clearFocus() group_name_input.clearFocus()
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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