mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2024-06-26 15:13:20 +00:00
![TiA4f8R](/assets/img/avatar_default.png)
Apply the requested changes, use ShareUtils.shareText to share an stream in the play queue and optimize imports for Java files, using Android Studio functionality. Apply the requested changes and do little improvements Apply the requested changes, use ShareUtils.shareText to share an stream in the play queue and optimize imports for Java files, using Android Studio functionality.
463 lines
19 KiB
Kotlin
463 lines
19 KiB
Kotlin
package org.schabi.newpipe.local.subscription
|
|
|
|
import android.app.Activity
|
|
import android.app.AlertDialog
|
|
import android.content.BroadcastReceiver
|
|
import android.content.Context
|
|
import android.content.DialogInterface
|
|
import android.content.Intent
|
|
import android.content.IntentFilter
|
|
import android.content.res.Configuration
|
|
import android.os.Bundle
|
|
import android.os.Environment
|
|
import android.os.Parcelable
|
|
import android.view.LayoutInflater
|
|
import android.view.Menu
|
|
import android.view.MenuInflater
|
|
import android.view.View
|
|
import android.view.ViewGroup
|
|
import android.widget.Toast
|
|
import androidx.lifecycle.Observer
|
|
import androidx.lifecycle.ViewModelProvider
|
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
|
import androidx.preference.PreferenceManager
|
|
import androidx.recyclerview.widget.GridLayoutManager
|
|
import com.nononsenseapps.filepicker.Utils
|
|
import com.xwray.groupie.Group
|
|
import com.xwray.groupie.GroupAdapter
|
|
import com.xwray.groupie.Item
|
|
import com.xwray.groupie.Section
|
|
import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder
|
|
import icepick.State
|
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
|
import org.schabi.newpipe.R
|
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
|
import org.schabi.newpipe.databinding.DialogTitleBinding
|
|
import org.schabi.newpipe.databinding.FragmentSubscriptionBinding
|
|
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
|
|
import org.schabi.newpipe.local.subscription.item.ChannelItem
|
|
import org.schabi.newpipe.local.subscription.item.EmptyPlaceholderItem
|
|
import org.schabi.newpipe.local.subscription.item.FeedGroupAddItem
|
|
import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
|
|
import org.schabi.newpipe.local.subscription.item.FeedGroupCarouselItem
|
|
import org.schabi.newpipe.local.subscription.item.FeedImportExportItem
|
|
import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem
|
|
import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem.Companion.PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM
|
|
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
|
|
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.EXPORT_COMPLETE_ACTION
|
|
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.KEY_FILE_PATH
|
|
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
|
|
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.IMPORT_COMPLETE_ACTION
|
|
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
|
|
import org.schabi.newpipe.report.UserAction
|
|
import org.schabi.newpipe.util.FilePickerActivityHelper
|
|
import org.schabi.newpipe.util.NavigationHelper
|
|
import org.schabi.newpipe.util.OnClickGesture
|
|
import org.schabi.newpipe.util.ShareUtils
|
|
import org.schabi.newpipe.util.ThemeHelper
|
|
import java.io.File
|
|
import java.text.SimpleDateFormat
|
|
import java.util.Date
|
|
import java.util.Locale
|
|
import kotlin.math.floor
|
|
import kotlin.math.max
|
|
|
|
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 var subscriptionBroadcastReceiver: BroadcastReceiver? = null
|
|
|
|
private val groupAdapter = GroupAdapter<GroupieViewHolder>()
|
|
private val feedGroupsSection = Section()
|
|
private var feedGroupsCarousel: FeedGroupCarouselItem? = null
|
|
private lateinit var importExportItem: FeedImportExportItem
|
|
private lateinit var feedGroupsSortMenuItem: HeaderWithMenuItem
|
|
private val subscriptionsSection = Section()
|
|
|
|
@State
|
|
@JvmField
|
|
var itemsListState: Parcelable? = null
|
|
@State
|
|
@JvmField
|
|
var feedGroupsListState: Parcelable? = null
|
|
@State
|
|
@JvmField
|
|
var importExportItemExpandedState: Boolean? = null
|
|
|
|
init {
|
|
setHasOptionsMenu(true)
|
|
}
|
|
|
|
// /////////////////////////////////////////////////////////////////////////
|
|
// Fragment LifeCycle
|
|
// /////////////////////////////////////////////////////////////////////////
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
setupInitialLayout()
|
|
}
|
|
|
|
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
|
super.setUserVisibleHint(isVisibleToUser)
|
|
if (activity != null && isVisibleToUser) {
|
|
setTitle(activity.getString(R.string.tab_subscriptions))
|
|
}
|
|
}
|
|
|
|
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 onResume() {
|
|
super.onResume()
|
|
setupBroadcastReceiver()
|
|
}
|
|
|
|
override fun onPause() {
|
|
super.onPause()
|
|
itemsListState = binding.itemsList.layoutManager?.onSaveInstanceState()
|
|
feedGroupsListState = feedGroupsCarousel?.onSaveInstanceState()
|
|
importExportItemExpandedState = importExportItem.isExpanded
|
|
|
|
if (subscriptionBroadcastReceiver != null && activity != null) {
|
|
LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver!!)
|
|
}
|
|
}
|
|
|
|
override fun onDestroy() {
|
|
super.onDestroy()
|
|
disposables.dispose()
|
|
}
|
|
|
|
// ////////////////////////////////////////////////////////////////////////
|
|
// Menu
|
|
// ////////////////////////////////////////////////////////////////////////
|
|
|
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
super.onCreateOptionsMenu(menu, inflater)
|
|
|
|
val supportActionBar = activity.supportActionBar
|
|
if (supportActionBar != null) {
|
|
supportActionBar.setDisplayShowTitleEnabled(true)
|
|
setTitle(getString(R.string.tab_subscriptions))
|
|
}
|
|
}
|
|
|
|
private fun setupBroadcastReceiver() {
|
|
if (activity == null) return
|
|
|
|
if (subscriptionBroadcastReceiver != null) {
|
|
LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver!!)
|
|
}
|
|
|
|
val filters = IntentFilter()
|
|
filters.addAction(EXPORT_COMPLETE_ACTION)
|
|
filters.addAction(IMPORT_COMPLETE_ACTION)
|
|
subscriptionBroadcastReceiver = object : BroadcastReceiver() {
|
|
override fun onReceive(context: Context, intent: Intent) {
|
|
_binding?.itemsList?.post {
|
|
importExportItem.isExpanded = false
|
|
importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS)
|
|
}
|
|
}
|
|
}
|
|
|
|
LocalBroadcastManager.getInstance(activity).registerReceiver(subscriptionBroadcastReceiver!!, filters)
|
|
}
|
|
|
|
private fun onImportFromServiceSelected(serviceId: Int) {
|
|
val fragmentManager = fm
|
|
NavigationHelper.openSubscriptionsImportFragment(fragmentManager, serviceId)
|
|
}
|
|
|
|
private fun onImportPreviousSelected() {
|
|
startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_CODE)
|
|
}
|
|
|
|
private fun onExportSelected() {
|
|
val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date())
|
|
val exportName = "newpipe_subscriptions_$date.json"
|
|
val exportFile = File(Environment.getExternalStorageDirectory(), exportName)
|
|
|
|
startActivityForResult(FilePickerActivityHelper.chooseFileToSave(activity, exportFile.absolutePath), REQUEST_EXPORT_CODE)
|
|
}
|
|
|
|
private fun openReorderDialog() {
|
|
FeedGroupReorderDialog().show(requireFragmentManager(), null)
|
|
}
|
|
|
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
super.onActivityResult(requestCode, resultCode, data)
|
|
if (data != null && data.data != null && resultCode == Activity.RESULT_OK) {
|
|
if (requestCode == REQUEST_EXPORT_CODE) {
|
|
val exportFile = Utils.getFileForUri(data.data!!)
|
|
if (!exportFile.parentFile.canWrite() || !exportFile.parentFile.canRead()) {
|
|
Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show()
|
|
} else {
|
|
activity.startService(
|
|
Intent(activity, SubscriptionsExportService::class.java)
|
|
.putExtra(KEY_FILE_PATH, exportFile.absolutePath)
|
|
)
|
|
}
|
|
} else if (requestCode == REQUEST_IMPORT_CODE) {
|
|
val path = Utils.getFileForUri(data.data!!).absolutePath
|
|
ImportConfirmationDialog.show(
|
|
this,
|
|
Intent(activity, SubscriptionsImportService::class.java)
|
|
.putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE)
|
|
.putExtra(KEY_VALUE, path)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ////////////////////////////////////////////////////////////////////////
|
|
// Fragment Views
|
|
// ////////////////////////////////////////////////////////////////////////
|
|
|
|
private fun setupInitialLayout() {
|
|
Section().apply {
|
|
val carouselAdapter = GroupAdapter<GroupieViewHolder>()
|
|
|
|
carouselAdapter.add(FeedGroupCardItem(-1, getString(R.string.all), FeedGroupIcon.RSS))
|
|
carouselAdapter.add(feedGroupsSection)
|
|
carouselAdapter.add(FeedGroupAddItem())
|
|
|
|
carouselAdapter.setOnItemClickListener { item, _ ->
|
|
listenerFeedGroups.selected(item)
|
|
}
|
|
carouselAdapter.setOnItemLongClickListener { item, _ ->
|
|
if (item is FeedGroupCardItem) {
|
|
if (item.groupId == FeedGroupEntity.GROUP_ALL_ID) {
|
|
return@setOnItemLongClickListener false
|
|
}
|
|
}
|
|
listenerFeedGroups.held(item)
|
|
return@setOnItemLongClickListener true
|
|
}
|
|
|
|
feedGroupsCarousel = FeedGroupCarouselItem(requireContext(), carouselAdapter)
|
|
feedGroupsSortMenuItem = HeaderWithMenuItem(
|
|
getString(R.string.feed_groups_header_title),
|
|
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_sort),
|
|
menuItemOnClickListener = ::openReorderDialog
|
|
)
|
|
add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel)))
|
|
|
|
groupAdapter.add(this)
|
|
}
|
|
|
|
subscriptionsSection.setPlaceholder(EmptyPlaceholderItem())
|
|
subscriptionsSection.setHideWhenEmpty(true)
|
|
|
|
importExportItem = FeedImportExportItem(
|
|
{ onImportPreviousSelected() },
|
|
{ onImportFromServiceSelected(it) },
|
|
{ onExportSelected() },
|
|
importExportItemExpandedState ?: false
|
|
)
|
|
groupAdapter.add(Section(importExportItem, listOf(subscriptionsSection)))
|
|
}
|
|
|
|
override fun initViews(rootView: View, savedInstanceState: Bundle?) {
|
|
super.initViews(rootView, savedInstanceState)
|
|
_binding = FragmentSubscriptionBinding.bind(rootView)
|
|
|
|
val shouldUseGridLayout = shouldUseGridLayout()
|
|
groupAdapter.spanCount = if (shouldUseGridLayout) getGridSpanCount() else 1
|
|
binding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
|
|
spanSizeLookup = groupAdapter.spanSizeLookup
|
|
}
|
|
binding.itemsList.adapter = groupAdapter
|
|
|
|
viewModel = ViewModelProvider(this).get(SubscriptionViewModel::class.java)
|
|
viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { it?.let(this::handleResult) })
|
|
viewModel.feedGroupsLiveData.observe(viewLifecycleOwner, Observer { it?.let(this::handleFeedGroups) })
|
|
}
|
|
|
|
private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
|
|
val commands = arrayOf(
|
|
getString(R.string.share),
|
|
getString(R.string.unsubscribe)
|
|
)
|
|
|
|
val actions = DialogInterface.OnClickListener { _, i ->
|
|
when (i) {
|
|
0 -> ShareUtils.shareText(requireContext(), selectedItem.name, selectedItem.url)
|
|
1 -> 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) {
|
|
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
|
|
|
|
private val listenerFeedGroups = object : OnClickGesture<Item<*>>() {
|
|
override fun selected(selectedItem: Item<*>?) {
|
|
when (selectedItem) {
|
|
is FeedGroupCardItem -> NavigationHelper.openFeedFragment(fm, selectedItem.groupId, selectedItem.name)
|
|
is FeedGroupAddItem -> FeedGroupDialog.newInstance().show(fm, null)
|
|
}
|
|
}
|
|
|
|
override fun held(selectedItem: Item<*>?) {
|
|
when (selectedItem) {
|
|
is FeedGroupCardItem -> FeedGroupDialog.newInstance(selectedItem.groupId).show(fm, null)
|
|
}
|
|
}
|
|
}
|
|
|
|
private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem>() {
|
|
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)
|
|
|
|
val shouldUseGridLayout = shouldUseGridLayout()
|
|
when (result) {
|
|
is SubscriptionState.LoadedState -> {
|
|
result.subscriptions.forEach {
|
|
if (it is ChannelItem) {
|
|
it.gesturesListener = listenerChannelItem
|
|
it.itemVersion = when {
|
|
shouldUseGridLayout -> ChannelItem.ItemVersion.GRID
|
|
else -> ChannelItem.ItemVersion.MINI
|
|
}
|
|
}
|
|
}
|
|
|
|
subscriptionsSection.update(result.subscriptions)
|
|
subscriptionsSection.setHideWhenEmpty(false)
|
|
|
|
if (result.subscriptions.isEmpty() && importExportItemExpandedState == null) {
|
|
binding.itemsList.post {
|
|
importExportItem.isExpanded = true
|
|
importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS)
|
|
}
|
|
}
|
|
|
|
if (itemsListState != null) {
|
|
binding.itemsList.layoutManager?.onRestoreInstanceState(itemsListState)
|
|
itemsListState = null
|
|
}
|
|
}
|
|
is SubscriptionState.ErrorState -> {
|
|
result.error?.let { onError(result.error) }
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun handleFeedGroups(groups: List<Group>) {
|
|
feedGroupsSection.update(groups)
|
|
|
|
if (feedGroupsListState != null) {
|
|
feedGroupsCarousel?.onRestoreInstanceState(feedGroupsListState)
|
|
feedGroupsListState = null
|
|
}
|
|
|
|
feedGroupsSortMenuItem.showMenuItem = groups.size > 1
|
|
binding.itemsList.post { feedGroupsSortMenuItem.notifyChanged(PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM) }
|
|
}
|
|
|
|
// /////////////////////////////////////////////////////////////////////////
|
|
// Contract
|
|
// /////////////////////////////////////////////////////////////////////////
|
|
|
|
override fun showLoading() {
|
|
super.showLoading()
|
|
binding.itemsList.animate(false, 100)
|
|
}
|
|
|
|
override fun hideLoading() {
|
|
super.hideLoading()
|
|
binding.itemsList.animate(true, 200)
|
|
}
|
|
|
|
// /////////////////////////////////////////////////////////////////////////
|
|
// Fragment Error Handling
|
|
// /////////////////////////////////////////////////////////////////////////
|
|
|
|
override fun onError(exception: Throwable): Boolean {
|
|
if (super.onError(exception)) return true
|
|
|
|
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Subscriptions", R.string.general_error)
|
|
return true
|
|
}
|
|
|
|
// /////////////////////////////////////////////////////////////////////////
|
|
// Grid Mode
|
|
// /////////////////////////////////////////////////////////////////////////
|
|
|
|
// TODO: Move these out of this class, as it can be reused
|
|
|
|
private fun shouldUseGridLayout(): Boolean {
|
|
val listMode = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
|
.getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value))
|
|
|
|
return when (listMode) {
|
|
getString(R.string.list_view_mode_auto_key) -> {
|
|
val configuration = resources.configuration
|
|
|
|
(
|
|
configuration.orientation == Configuration.ORIENTATION_LANDSCAPE &&
|
|
configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
|
|
)
|
|
}
|
|
getString(R.string.list_view_mode_grid_key) -> true
|
|
else -> false
|
|
}
|
|
}
|
|
|
|
private fun getGridSpanCount(): Int {
|
|
val minWidth = resources.getDimensionPixelSize(R.dimen.channel_item_grid_min_width)
|
|
return max(1, floor(resources.displayMetrics.widthPixels / minWidth.toDouble()).toInt())
|
|
}
|
|
|
|
companion object {
|
|
private const val REQUEST_EXPORT_CODE = 666
|
|
private const val REQUEST_IMPORT_CODE = 667
|
|
}
|
|
}
|