NewPipe/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt

528 lines
20 KiB
Kotlin
Raw Normal View History

package org.schabi.newpipe.local.subscription.dialog
import android.app.Dialog
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.core.content.getSystemService
2020-10-17 12:08:45 +02:00
import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.GroupieViewHolder
import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.Section
import icepick.Icepick
import icepick.State
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
2020-11-03 09:45:43 +01:00
import org.schabi.newpipe.databinding.DialogFeedGroupCreateBinding
import org.schabi.newpipe.databinding.ToolbarSearchLayoutBinding
import org.schabi.newpipe.fragments.BackPressable
import org.schabi.newpipe.local.subscription.FeedGroupIcon
2020-05-01 20:13:01 +02:00
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.InitialScreen
import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog.ScreenState.SubscriptionsPickerScreen
import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialogViewModel.DialogEvent.ProcessingEvent
import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialogViewModel.DialogEvent.SuccessEvent
import org.schabi.newpipe.local.subscription.item.EmptyPlaceholderItem
import org.schabi.newpipe.local.subscription.item.PickerIconItem
import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem
2020-07-21 00:43:49 +02:00
import org.schabi.newpipe.util.DeviceUtils
import org.schabi.newpipe.util.ThemeHelper
2020-10-31 21:55:45 +01:00
import java.io.Serializable
class FeedGroupDialog : DialogFragment(), BackPressable {
2020-11-03 09:45:43 +01:00
private var _feedGroupCreateBinding: DialogFeedGroupCreateBinding? = null
private val feedGroupCreateBinding get() = _feedGroupCreateBinding!!
private var _searchLayoutBinding: ToolbarSearchLayoutBinding? = null
private val searchLayoutBinding get() = _searchLayoutBinding!!
private lateinit var viewModel: FeedGroupDialogViewModel
private var groupId: Long = NO_GROUP_SELECTED
private var groupIcon: FeedGroupIcon? = null
private var groupSortOrder: Long = -1
sealed class ScreenState : Serializable {
object InitialScreen : ScreenState()
object IconPickerScreen : ScreenState()
object SubscriptionsPickerScreen : ScreenState()
object DeleteScreen : ScreenState()
}
@State @JvmField var selectedIcon: FeedGroupIcon? = null
@State @JvmField var selectedSubscriptions: HashSet<Long> = HashSet()
@State @JvmField var wasSubscriptionSelectionChanged: Boolean = false
@State @JvmField var currentScreen: ScreenState = InitialScreen
@State @JvmField var subscriptionsListState: Parcelable? = null
@State @JvmField var iconsListState: Parcelable? = null
@State @JvmField var wasSearchSubscriptionsVisible = false
@State @JvmField var subscriptionsCurrentSearchQuery = ""
@State @JvmField var subscriptionsShowOnlyUngrouped = false
private val subscriptionMainSection = Section()
private val subscriptionEmptyFooter = Section()
private lateinit var subscriptionGroupAdapter: GroupAdapter<GroupieViewHolder>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Icepick.restoreInstanceState(this, savedInstanceState)
setStyle(STYLE_NO_TITLE, ThemeHelper.getMinWidthDialogTheme(requireContext()))
groupId = arguments?.getLong(KEY_GROUP_ID, NO_GROUP_SELECTED) ?: NO_GROUP_SELECTED
}
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 (!this@FeedGroupDialog.onBackPressed()) {
super.onBackPressed()
}
}
}
}
override fun onPause() {
super.onPause()
wasSearchSubscriptionsVisible = isSearchVisible()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
2020-11-03 09:45:43 +01:00
iconsListState = feedGroupCreateBinding.iconSelector.layoutManager?.onSaveInstanceState()
subscriptionsListState = feedGroupCreateBinding.subscriptionsSelectorList.layoutManager?.onSaveInstanceState()
Icepick.saveInstanceState(this, outState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
2020-11-03 09:45:43 +01:00
_feedGroupCreateBinding = DialogFeedGroupCreateBinding.bind(view)
_searchLayoutBinding = feedGroupCreateBinding.subscriptionsHeaderSearchContainer
2020-10-31 21:55:45 +01:00
viewModel = ViewModelProvider(
this,
FeedGroupDialogViewModel.Factory(
requireContext(),
groupId, subscriptionsCurrentSearchQuery, subscriptionsShowOnlyUngrouped
)
).get(FeedGroupDialogViewModel::class.java)
viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup))
2020-10-31 21:55:45 +01:00
viewModel.subscriptionsLiveData.observe(
viewLifecycleOwner,
Observer {
setupSubscriptionPicker(it.first, it.second)
}
2020-10-31 21:55:45 +01:00
)
viewModel.dialogEventLiveData.observe(
viewLifecycleOwner,
Observer {
when (it) {
ProcessingEvent -> disableInput()
SuccessEvent -> dismiss()
}
}
)
subscriptionGroupAdapter = GroupAdapter<GroupieViewHolder>().apply {
add(subscriptionMainSection)
add(subscriptionEmptyFooter)
spanCount = 4
}
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.subscriptionsSelectorList.apply {
// Disable animations, too distracting.
itemAnimator = null
adapter = subscriptionGroupAdapter
2020-10-31 21:55:45 +01:00
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()
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.subscriptionsSelectorList.adapter = null
feedGroupCreateBinding.iconSelector.adapter = null
_feedGroupCreateBinding = null
_searchLayoutBinding = 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() {
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.deleteButton.setOnClickListener { showScreen(DeleteScreen) }
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.cancelButton.setOnClickListener {
when (currentScreen) {
InitialScreen -> dismiss()
else -> showScreen(InitialScreen)
}
}
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.groupNameInputContainer.error = null
feedGroupCreateBinding.groupNameInput.doOnTextChanged { text, _, _, _ ->
if (feedGroupCreateBinding.groupNameInputContainer.isErrorEnabled && !text.isNullOrBlank()) {
feedGroupCreateBinding.groupNameInputContainer.error = null
}
}
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.confirmButton.setOnClickListener { handlePositiveButton() }
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.selectChannelButton.setOnClickListener {
feedGroupCreateBinding.subscriptionsSelectorList.scrollToPosition(0)
showScreen(SubscriptionsPickerScreen)
}
2020-11-03 09:45:43 +01:00
val headerMenu = feedGroupCreateBinding.subscriptionsHeaderToolbar.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
}
}
2020-11-03 09:45:43 +01:00
searchLayoutBinding.toolbarSearchClear.setOnClickListener {
if (searchLayoutBinding.toolbarSearchEditText.text.isNullOrEmpty()) {
hideSearch()
return@setOnClickListener
}
resetSearch()
showKeyboardSearch()
}
2020-11-03 09:45:43 +01:00
searchLayoutBinding.toolbarSearchEditText.setOnClickListener {
2020-07-21 00:43:49 +02:00
if (DeviceUtils.isTv(context)) {
showKeyboardSearch()
}
}
2020-11-03 09:45:43 +01:00
searchLayoutBinding.toolbarSearchEditText.doOnTextChanged { _, _, _, _ ->
val newQuery: String = searchLayoutBinding.toolbarSearchEditText.text.toString()
subscriptionsCurrentSearchQuery = newQuery
viewModel.filterSubscriptionsBy(newQuery)
}
2020-08-27 22:56:58 +02:00
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() {
2020-11-03 09:45:43 +01:00
val name = feedGroupCreateBinding.groupNameInput.text.toString().trim()
val icon = selectedIcon ?: groupIcon ?: FeedGroupIcon.ALL
if (name.isBlank()) {
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.groupNameInputContainer.error = getString(R.string.feed_group_dialog_empty_name)
feedGroupCreateBinding.groupNameInput.text = null
feedGroupCreateBinding.groupNameInput.requestFocus()
return
} else {
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.groupNameInputContainer.error = null
}
if (selectedSubscriptions.isEmpty()) {
Toast.makeText(requireContext(), getString(R.string.feed_group_dialog_empty_selection), Toast.LENGTH_SHORT).show()
return
}
when (groupId) {
NO_GROUP_SELECTED -> viewModel.createGroup(name, icon, selectedSubscriptions)
else -> viewModel.updateGroup(name, icon, selectedSubscriptions, groupSortOrder)
}
}
private fun handleGroup(feedGroupEntity: FeedGroupEntity? = null) {
val icon = feedGroupEntity?.icon ?: FeedGroupIcon.ALL
val name = feedGroupEntity?.name ?: ""
groupIcon = feedGroupEntity?.icon
groupSortOrder = feedGroupEntity?.sortOrder ?: -1
val feedGroupIcon = if (selectedIcon == null) icon else selectedIcon!!
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.iconPreview.setImageResource(feedGroupIcon.getDrawableRes(requireContext()))
2020-11-03 09:45:43 +01:00
if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) {
feedGroupCreateBinding.groupNameInput.setText(name)
}
}
private val subscriptionPickerItemListener = OnItemClickListener { item, view ->
if (item is PickerSubscriptionItem) {
val subscriptionId = item.subscriptionEntity.uid
wasSubscriptionSelectionChanged = true
val isSelected = if (this.selectedSubscriptions.contains(subscriptionId)) {
this.selectedSubscriptions.remove(subscriptionId)
false
} else {
this.selectedSubscriptions.add(subscriptionId)
true
}
item.updateSelected(view, isSelected)
updateSubscriptionSelectedCount()
}
}
private fun setupSubscriptionPicker(
subscriptions: List<PickerSubscriptionItem>,
selectedSubscriptions: Set<Long>
) {
if (!wasSubscriptionSelectionChanged) {
this.selectedSubscriptions.addAll(selectedSubscriptions)
}
updateSubscriptionSelectedCount()
if (subscriptions.isEmpty()) {
subscriptionEmptyFooter.clear()
subscriptionEmptyFooter.add(EmptyPlaceholderItem())
} else {
subscriptionEmptyFooter.clear()
}
subscriptions.forEach {
it.isSelected = this@FeedGroupDialog.selectedSubscriptions
.contains(it.subscriptionEntity.uid)
}
subscriptionMainSection.update(subscriptions, false)
if (subscriptionsListState != null) {
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.subscriptionsSelectorList.layoutManager?.onRestoreInstanceState(subscriptionsListState)
subscriptionsListState = null
} else {
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.subscriptionsSelectorList.scrollToPosition(0)
}
}
private fun updateSubscriptionSelectedCount() {
val selectedCount = this.selectedSubscriptions.size
val selectedCountText = resources.getQuantityString(
R.plurals.feed_group_dialog_selection_count,
selectedCount, selectedCount
)
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.selectedSubscriptionCountView.text = selectedCountText
feedGroupCreateBinding.subscriptionsHeaderInfo.text = selectedCountText
}
private fun setupIconPicker() {
2019-10-10 15:28:57 +02:00
val groupAdapter = GroupAdapter<GroupieViewHolder>()
groupAdapter.addAll(FeedGroupIcon.values().map { PickerIconItem(requireContext(), it) })
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.iconSelector.apply {
layoutManager = GridLayoutManager(requireContext(), 7, RecyclerView.VERTICAL, false)
adapter = groupAdapter
if (iconsListState != null) {
layoutManager?.onRestoreInstanceState(iconsListState)
iconsListState = null
}
}
groupAdapter.setOnItemClickListener { item, _ ->
when (item) {
is PickerIconItem -> {
selectedIcon = item.icon
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.iconPreview.setImageResource(item.iconRes)
showScreen(InitialScreen)
}
}
}
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.iconPreview.setOnClickListener {
feedGroupCreateBinding.iconSelector.scrollToPosition(0)
showScreen(IconPickerScreen)
}
if (groupId == NO_GROUP_SELECTED) {
val icon = selectedIcon ?: FeedGroupIcon.ALL
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.iconPreview.setImageResource(icon.getDrawableRes(requireContext()))
}
}
/*///////////////////////////////////////////////////////////////////////////
// Screen Selector
////////////////////////////////////////////////////////////////////////// */
private fun showScreen(screen: ScreenState) {
currentScreen = screen
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.optionsRoot.onlyVisibleIn(InitialScreen)
feedGroupCreateBinding.iconSelector.onlyVisibleIn(IconPickerScreen)
feedGroupCreateBinding.subscriptionsSelector.onlyVisibleIn(SubscriptionsPickerScreen)
feedGroupCreateBinding.deleteScreenMessage.onlyVisibleIn(DeleteScreen)
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.separator.onlyVisibleIn(SubscriptionsPickerScreen, IconPickerScreen)
feedGroupCreateBinding.cancelButton.onlyVisibleIn(InitialScreen, DeleteScreen)
feedGroupCreateBinding.confirmButton.setText(
when {
currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create
else -> android.R.string.ok
}
)
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.deleteButton.isGone = currentScreen != InitialScreen || groupId == NO_GROUP_SELECTED
hideKeyboard()
hideSearch()
}
private fun View.onlyVisibleIn(vararg screens: ScreenState) {
isVisible = currentScreen in screens
}
/*///////////////////////////////////////////////////////////////////////////
// Utils
////////////////////////////////////////////////////////////////////////// */
2020-11-03 09:45:43 +01:00
private fun isSearchVisible() = _searchLayoutBinding?.root?.visibility == View.VISIBLE
private fun resetSearch() {
2020-11-03 09:45:43 +01:00
searchLayoutBinding.toolbarSearchEditText.setText("")
subscriptionsCurrentSearchQuery = ""
viewModel.clearSubscriptionsFilter()
}
private fun hideSearch() {
resetSearch()
2020-11-03 09:45:43 +01:00
searchLayoutBinding.root.visibility = View.GONE
feedGroupCreateBinding.subscriptionsHeaderInfoContainer.visibility = View.VISIBLE
feedGroupCreateBinding.subscriptionsHeaderToolbar.menu.findItem(R.id.action_search).isVisible = true
hideKeyboardSearch()
}
private fun showSearch() {
2020-11-03 09:45:43 +01:00
searchLayoutBinding.root.visibility = View.VISIBLE
feedGroupCreateBinding.subscriptionsHeaderInfoContainer.visibility = View.GONE
feedGroupCreateBinding.subscriptionsHeaderToolbar.menu.findItem(R.id.action_search).isVisible = false
showKeyboardSearch()
}
private val inputMethodManager by lazy {
requireActivity().getSystemService<InputMethodManager>()!!
}
private fun showKeyboardSearch() {
2020-11-03 09:45:43 +01:00
if (searchLayoutBinding.toolbarSearchEditText.requestFocus()) {
inputMethodManager.showSoftInput(
searchLayoutBinding.toolbarSearchEditText,
InputMethodManager.SHOW_IMPLICIT
)
}
}
private fun hideKeyboardSearch() {
inputMethodManager.hideSoftInputFromWindow(
searchLayoutBinding.toolbarSearchEditText.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN
)
2020-11-03 09:45:43 +01:00
searchLayoutBinding.toolbarSearchEditText.clearFocus()
}
private fun showKeyboard() {
2020-11-03 09:45:43 +01:00
if (feedGroupCreateBinding.groupNameInput.requestFocus()) {
inputMethodManager.showSoftInput(
feedGroupCreateBinding.groupNameInput,
InputMethodManager.SHOW_IMPLICIT
)
}
}
private fun hideKeyboard() {
inputMethodManager.hideSoftInputFromWindow(
feedGroupCreateBinding.groupNameInput.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN
)
2020-11-03 09:45:43 +01:00
feedGroupCreateBinding.groupNameInput.clearFocus()
}
private fun disableInput() {
2020-11-03 09:45:43 +01:00
_feedGroupCreateBinding?.deleteButton?.isEnabled = false
_feedGroupCreateBinding?.confirmButton?.isEnabled = false
_feedGroupCreateBinding?.cancelButton?.isEnabled = false
isCancelable = false
hideKeyboard()
}
companion object {
private const val KEY_GROUP_ID = "KEY_GROUP_ID"
private const val NO_GROUP_SELECTED = -1L
fun newInstance(groupId: Long = NO_GROUP_SELECTED): FeedGroupDialog {
val dialog = FeedGroupDialog()
2020-10-17 12:08:45 +02:00
dialog.arguments = bundleOf(KEY_GROUP_ID to groupId)
return dialog
}
}
2020-05-01 20:13:01 +02:00
}