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

463 lines
18 KiB
Kotlin
Raw Normal View History

package org.schabi.newpipe.local.subscription
import android.app.Activity
2020-05-01 20:13:01 +02:00
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
2020-05-01 20:13:01 +02:00
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
2021-11-28 16:13:14 +01:00
import android.view.MenuItem
import android.view.SubMenu
2020-05-01 20:13:01 +02:00
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
2021-11-28 16:13:14 +01:00
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
2020-08-27 22:56:12 +02:00
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import com.xwray.groupie.Group
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.Section
import com.xwray.groupie.viewbinding.GroupieViewHolder
import icepick.State
2020-10-31 21:55:45 +01:00
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID
import org.schabi.newpipe.databinding.DialogTitleBinding
import org.schabi.newpipe.databinding.FeedItemCarouselBinding
import org.schabi.newpipe.databinding.FragmentSubscriptionBinding
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.UserAction
2022-05-09 20:58:10 +02:00
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.fragments.BaseStateFragment
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.local.subscription.SubscriptionViewModel.SubscriptionState
import org.schabi.newpipe.local.subscription.dialog.FeedGroupDialog
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialog
2020-05-01 20:13:01 +02:00
import org.schabi.newpipe.local.subscription.item.ChannelItem
import org.schabi.newpipe.local.subscription.item.FeedGroupAddNewGridItem
import org.schabi.newpipe.local.subscription.item.FeedGroupAddNewItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCardGridItem
2020-05-01 20:13:01 +02:00
import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCarouselItem
import org.schabi.newpipe.local.subscription.item.GroupsHeader
import org.schabi.newpipe.local.subscription.item.Header
import org.schabi.newpipe.local.subscription.item.ImportSubscriptionsHintPlaceholderItem
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
2020-05-01 20:13:01 +02:00
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
2021-11-28 14:07:45 +01:00
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard
import org.schabi.newpipe.streams.io.StoredFileHelper
2020-05-01 20:13:01 +02:00
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.OnClickGesture
2022-03-20 17:22:48 +01:00
import org.schabi.newpipe.util.ServiceHelper
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountChannels
2021-06-23 14:29:57 +02:00
import org.schabi.newpipe.util.external_communication.ShareUtils
2020-10-31 21:55:45 +01:00
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private var _binding: FragmentSubscriptionBinding? = null
private val binding get() = _binding!!
private lateinit var viewModel: SubscriptionViewModel
private lateinit var subscriptionManager: SubscriptionManager
private val disposables: CompositeDisposable = CompositeDisposable()
private val groupAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
private lateinit var carouselAdapter: GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>
private lateinit var feedGroupsCarousel: FeedGroupCarouselItem
private lateinit var feedGroupsSortMenuItem: GroupsHeader
private val subscriptionsSection = Section()
private val requestExportLauncher =
registerForActivityResult(StartActivityForResult(), this::requestExportResult)
private val requestImportLauncher =
registerForActivityResult(StartActivityForResult(), this::requestImportResult)
@State
@JvmField
var itemsListState: Parcelable? = null
@State
@JvmField
var feedGroupsCarouselState: Parcelable? = null
init {
setHasOptionsMenu(true)
}
2020-05-01 20:13:21 +02:00
// /////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle
2020-05-01 20:13:21 +02:00
// /////////////////////////////////////////////////////////////////////////
override fun onAttach(context: Context) {
super.onAttach(context)
subscriptionManager = SubscriptionManager(requireContext())
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_subscription, container, false)
}
override fun onPause() {
super.onPause()
itemsListState = binding.itemsList.layoutManager?.onSaveInstanceState()
feedGroupsCarouselState = feedGroupsCarousel.onSaveInstanceState()
}
override fun onDestroy() {
super.onDestroy()
disposables.dispose()
}
2020-05-01 20:13:21 +02:00
// ////////////////////////////////////////////////////////////////////////
// Menu
2020-05-01 20:13:21 +02:00
// ////////////////////////////////////////////////////////////////////////
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
activity.supportActionBar?.setDisplayShowTitleEnabled(true)
activity.supportActionBar?.setTitle(R.string.tab_subscriptions)
2021-11-28 16:13:14 +01:00
buildImportExportMenu(menu)
}
2021-11-28 16:13:14 +01:00
private fun buildImportExportMenu(menu: Menu) {
// -- Import --
val importSubMenu = menu.addSubMenu(R.string.import_from)
addMenuItemToSubmenu(importSubMenu, R.string.previous_export) { onImportPreviousSelected() }
2022-03-20 17:32:41 +01:00
.setIcon(R.drawable.ic_backup)
2022-05-09 20:58:10 +02:00
for (service in ServiceList.all()) {
val subscriptionExtractor = service.subscriptionExtractor ?: continue
val supportedSources = subscriptionExtractor.supportedSources
if (supportedSources.isEmpty()) continue
addMenuItemToSubmenu(importSubMenu, service.serviceInfo.name) {
onImportFromServiceSelected(service.serviceId)
}
2022-05-09 20:58:10 +02:00
.setIcon(ServiceHelper.getIcon(service.serviceId))
}
2021-11-28 16:13:14 +01:00
// -- Export --
val exportSubMenu = menu.addSubMenu(R.string.export_to)
addMenuItemToSubmenu(exportSubMenu, R.string.file) { onExportSelected() }
2022-03-20 17:32:41 +01:00
.setIcon(R.drawable.ic_save)
2021-11-28 16:13:14 +01:00
}
private fun addMenuItemToSubmenu(
2021-11-28 16:13:14 +01:00
subMenu: SubMenu,
@StringRes title: Int,
onClick: Runnable
): MenuItem {
return setClickListenerToMenuItem(subMenu.add(title), onClick)
2021-11-28 16:13:14 +01:00
}
private fun addMenuItemToSubmenu(
2021-11-28 16:13:14 +01:00
subMenu: SubMenu,
title: String,
onClick: Runnable
): MenuItem {
return setClickListenerToMenuItem(subMenu.add(title), onClick)
2021-11-28 16:13:14 +01:00
}
private fun setClickListenerToMenuItem(
menuItem: MenuItem,
2021-11-28 16:13:14 +01:00
onClick: Runnable
): MenuItem {
menuItem.setOnMenuItemClickListener {
2021-11-28 16:13:14 +01:00
onClick.run()
true
}
return menuItem
}
private fun onImportFromServiceSelected(serviceId: Int) {
val fragmentManager = fm
NavigationHelper.openSubscriptionsImportFragment(fragmentManager, serviceId)
}
private fun onImportPreviousSelected() {
2021-11-28 14:07:45 +01:00
NoFileManagerSafeGuard.launchSafe(
requestImportLauncher,
StoredFileHelper.getPicker(activity, JSON_MIME_TYPE),
TAG,
requireContext()
)
}
private fun onExportSelected() {
val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date())
val exportName = "newpipe_subscriptions_$date.json"
2021-11-28 14:07:45 +01:00
NoFileManagerSafeGuard.launchSafe(
requestExportLauncher,
StoredFileHelper.getNewPicker(activity, exportName, JSON_MIME_TYPE, null),
TAG,
requireContext()
)
}
private fun openReorderDialog() {
FeedGroupReorderDialog().show(parentFragmentManager, null)
}
private fun requestExportResult(result: ActivityResult) {
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
activity.startService(
Intent(activity, SubscriptionsExportService::class.java)
.putExtra(SubscriptionsExportService.KEY_FILE_PATH, result.data?.data)
)
}
}
private fun requestImportResult(result: ActivityResult) {
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
ImportConfirmationDialog.show(
this,
Intent(activity, SubscriptionsImportService::class.java)
.putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE)
.putExtra(KEY_VALUE, result.data?.data)
)
}
}
2020-05-01 20:13:21 +02:00
// ////////////////////////////////////////////////////////////////////////
// Fragment Views
2020-05-01 20:13:21 +02:00
// ////////////////////////////////////////////////////////////////////////
override fun initViews(rootView: View, savedInstanceState: Bundle?) {
super.initViews(rootView, savedInstanceState)
_binding = FragmentSubscriptionBinding.bind(rootView)
groupAdapter.spanCount = if (SubscriptionViewModel.shouldUseGridForSubscription(requireContext())) getGridSpanCountChannels(context) else 1
binding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
spanSizeLookup = groupAdapter.spanSizeLookup
}
binding.itemsList.adapter = groupAdapter
binding.itemsList.itemAnimator = null
viewModel = ViewModelProvider(this)[SubscriptionViewModel::class.java]
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) }
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) {
it?.let { (groups, listViewMode) ->
handleFeedGroups(groups, listViewMode)
}
}
setupInitialLayout()
}
private fun setupInitialLayout() {
Section().apply {
carouselAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
carouselAdapter.setOnItemClickListener { item, _ ->
when (item) {
is FeedGroupCardItem ->
NavigationHelper.openFeedFragment(fm, item.groupId, item.name)
is FeedGroupCardGridItem ->
NavigationHelper.openFeedFragment(fm, item.groupId, item.name)
is FeedGroupAddNewItem ->
FeedGroupDialog.newInstance().show(fm, null)
is FeedGroupAddNewGridItem ->
FeedGroupDialog.newInstance().show(fm, null)
}
}
carouselAdapter.setOnItemLongClickListener { item, _ ->
if ((item is FeedGroupCardItem && item.groupId == GROUP_ALL_ID) ||
(item is FeedGroupCardGridItem && item.groupId == GROUP_ALL_ID)
) {
return@setOnItemLongClickListener false
}
when (item) {
is FeedGroupCardItem ->
FeedGroupDialog.newInstance(item.groupId).show(fm, null)
is FeedGroupCardGridItem ->
FeedGroupDialog.newInstance(item.groupId).show(fm, null)
}
return@setOnItemLongClickListener true
}
feedGroupsCarousel = FeedGroupCarouselItem(
carouselAdapter = carouselAdapter,
listViewMode = viewModel.getListViewMode()
)
feedGroupsSortMenuItem = GroupsHeader(
title = getString(R.string.feed_groups_header_title),
onSortClicked = ::openReorderDialog,
onToggleListViewModeClicked = ::toggleListViewMode,
listViewMode = viewModel.getListViewMode(),
)
add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel)))
groupAdapter.clear()
groupAdapter.add(this)
}
subscriptionsSection.setPlaceholder(ImportSubscriptionsHintPlaceholderItem())
subscriptionsSection.setHideWhenEmpty(true)
groupAdapter.add(
Section(
Header(getString(R.string.tab_subscriptions)),
listOf(subscriptionsSection)
)
)
}
private fun toggleListViewMode() {
viewModel.setListViewMode(!viewModel.getListViewMode())
}
private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
2021-06-06 11:56:38 +02:00
val commands = arrayOf(
getString(R.string.share),
getString(R.string.open_in_browser),
2021-06-06 11:56:38 +02:00
getString(R.string.unsubscribe)
)
val actions = DialogInterface.OnClickListener { _, i ->
when (i) {
2021-06-23 14:29:57 +02:00
0 -> ShareUtils.shareText(
requireContext(), selectedItem.name, selectedItem.url,
selectedItem.thumbnailUrl
)
1 -> ShareUtils.openUrlInBrowser(requireContext(), selectedItem.url)
2 -> deleteChannel(selectedItem)
}
}
val dialogTitleBinding = DialogTitleBinding.inflate(LayoutInflater.from(requireContext()))
dialogTitleBinding.root.isSelected = true
dialogTitleBinding.itemTitleView.text = selectedItem.name
dialogTitleBinding.itemAdditionalDetails.visibility = View.GONE
AlertDialog.Builder(requireContext())
.setCustomTitle(dialogTitleBinding.root)
.setItems(commands, actions)
.create()
.show()
}
private fun deleteChannel(selectedItem: ChannelInfoItem) {
2020-10-31 21:55:45 +01:00
disposables.add(
subscriptionManager.deleteSubscription(selectedItem.serviceId, selectedItem.url).subscribe {
Toast.makeText(requireContext(), getString(R.string.channel_unsubscribed), Toast.LENGTH_SHORT).show()
}
)
}
override fun doInitialLoadLogic() = Unit
override fun startLoading(forceLoad: Boolean) = Unit
2022-07-31 10:08:24 +02:00
private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem> {
2020-10-31 21:55:45 +01:00
override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(
fm,
selectedItem.serviceId, selectedItem.url, selectedItem.name
)
override fun held(selectedItem: ChannelInfoItem) = showLongTapDialog(selectedItem)
}
override fun handleResult(result: SubscriptionState) {
super.handleResult(result)
when (result) {
is SubscriptionState.LoadedState -> {
result.subscriptions.forEach {
if (it is ChannelItem) {
it.gesturesListener = listenerChannelItem
it.itemVersion = if (SubscriptionViewModel.shouldUseGridForSubscription(requireContext())) {
ChannelItem.ItemVersion.GRID
} else {
ChannelItem.ItemVersion.MINI
}
}
}
subscriptionsSection.update(result.subscriptions)
subscriptionsSection.setHideWhenEmpty(false)
if (itemsListState != null) {
binding.itemsList.layoutManager?.onRestoreInstanceState(itemsListState)
itemsListState = null
}
}
is SubscriptionState.ErrorState -> {
result.error?.let {
showError(ErrorInfo(result.error, UserAction.SOMETHING_ELSE, "Subscriptions"))
}
}
}
}
private fun handleFeedGroups(groups: List<Group>, listViewMode: Boolean) {
if (feedGroupsCarouselState != null) {
feedGroupsCarousel.onRestoreInstanceState(feedGroupsCarouselState)
feedGroupsCarouselState = null
}
binding.itemsList.post {
if (context == null) {
// since this part was posted to the next UI cycle, the fragment might have been
// removed in the meantime
return@post
}
feedGroupsCarousel.listViewMode = listViewMode
feedGroupsSortMenuItem.showSortButton = groups.size > 1
feedGroupsSortMenuItem.listViewMode = listViewMode
feedGroupsCarousel.notifyChanged(FeedGroupCarouselItem.PAYLOAD_UPDATE_LIST_VIEW_MODE)
feedGroupsSortMenuItem.notifyChanged(GroupsHeader.PAYLOAD_UPDATE_ICONS)
2022-10-27 14:01:04 +02:00
// update items here to prevent flickering
carouselAdapter.apply {
clear()
if (listViewMode) {
add(FeedGroupAddNewItem())
2023-01-13 16:33:45 +01:00
add(FeedGroupCardItem(GROUP_ALL_ID, getString(R.string.all), FeedGroupIcon.WHATS_NEW))
2022-10-27 14:01:04 +02:00
} else {
add(FeedGroupAddNewGridItem())
2023-01-13 16:33:45 +01:00
add(FeedGroupCardGridItem(GROUP_ALL_ID, getString(R.string.all), FeedGroupIcon.WHATS_NEW))
2022-10-27 14:01:04 +02:00
}
addAll(groups)
}
}
}
2020-05-01 20:13:21 +02:00
// /////////////////////////////////////////////////////////////////////////
// Contract
2020-05-01 20:13:21 +02:00
// /////////////////////////////////////////////////////////////////////////
override fun showLoading() {
super.showLoading()
binding.itemsList.animate(false, 100)
}
override fun hideLoading() {
super.hideLoading()
binding.itemsList.animate(true, 200)
}
companion object {
const val JSON_MIME_TYPE = "application/json"
}
}