2019-04-28 22:43:54 +02:00
|
|
|
/*
|
|
|
|
* Copyright 2019 Mauricio Colli <mauriciocolli@outlook.com>
|
|
|
|
* FeedFragment.kt is part of NewPipe
|
|
|
|
*
|
|
|
|
* License: GPL-3.0+
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package org.schabi.newpipe.local.feed
|
|
|
|
|
2020-04-05 16:11:03 +02:00
|
|
|
import android.annotation.SuppressLint
|
|
|
|
import android.app.Activity
|
2021-09-28 21:51:57 +02:00
|
|
|
import android.content.Context
|
2019-04-28 22:43:54 +02:00
|
|
|
import android.content.Intent
|
2021-06-15 18:40:25 +02:00
|
|
|
import android.content.SharedPreferences
|
2021-09-05 19:04:42 +02:00
|
|
|
import android.graphics.Typeface
|
2021-09-28 21:51:57 +02:00
|
|
|
import android.graphics.drawable.LayerDrawable
|
2019-04-28 22:43:54 +02:00
|
|
|
import android.os.Bundle
|
|
|
|
import android.os.Parcelable
|
2021-11-05 18:04:49 +01:00
|
|
|
import android.util.Log
|
2020-05-01 20:13:01 +02:00
|
|
|
import android.view.LayoutInflater
|
|
|
|
import android.view.Menu
|
|
|
|
import android.view.MenuInflater
|
|
|
|
import android.view.MenuItem
|
|
|
|
import android.view.View
|
|
|
|
import android.view.ViewGroup
|
2021-09-12 15:04:33 +02:00
|
|
|
import android.widget.Button
|
2020-03-14 04:11:30 +01:00
|
|
|
import androidx.appcompat.app.AlertDialog
|
2020-04-05 16:11:03 +02:00
|
|
|
import androidx.appcompat.content.res.AppCompatResources
|
2020-11-18 23:29:58 +01:00
|
|
|
import androidx.core.content.edit
|
2022-08-08 03:40:16 +02:00
|
|
|
import androidx.core.math.MathUtils
|
2020-10-17 12:08:45 +02:00
|
|
|
import androidx.core.os.bundleOf
|
2022-09-13 15:08:37 +02:00
|
|
|
import androidx.core.view.MenuItemCompat
|
2020-10-17 12:24:35 +02:00
|
|
|
import androidx.core.view.isVisible
|
2020-08-27 22:56:12 +02:00
|
|
|
import androidx.lifecycle.ViewModelProvider
|
2020-03-14 04:11:30 +01:00
|
|
|
import androidx.preference.PreferenceManager
|
2020-04-05 16:11:03 +02:00
|
|
|
import androidx.recyclerview.widget.GridLayoutManager
|
2021-09-03 22:03:34 +02:00
|
|
|
import androidx.recyclerview.widget.RecyclerView
|
2021-09-23 01:08:03 +02:00
|
|
|
import com.xwray.groupie.GroupieAdapter
|
2020-04-05 16:11:03 +02:00
|
|
|
import com.xwray.groupie.Item
|
|
|
|
import com.xwray.groupie.OnItemClickListener
|
|
|
|
import com.xwray.groupie.OnItemLongClickListener
|
2019-04-28 22:43:54 +02:00
|
|
|
import icepick.State
|
2021-04-02 21:41:06 +02:00
|
|
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|
|
|
import io.reactivex.rxjava3.core.Single
|
|
|
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
|
|
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
|
|
|
import org.schabi.newpipe.NewPipeDatabase
|
2019-04-28 22:43:54 +02:00
|
|
|
import org.schabi.newpipe.R
|
2020-01-28 06:59:49 +01:00
|
|
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
2021-04-02 21:41:06 +02:00
|
|
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
2020-11-03 09:21:37 +01:00
|
|
|
import org.schabi.newpipe.databinding.FragmentFeedBinding
|
2020-12-11 14:55:47 +01:00
|
|
|
import org.schabi.newpipe.error.ErrorInfo
|
2020-12-09 12:42:01 +01:00
|
|
|
import org.schabi.newpipe.error.UserAction
|
2021-04-02 21:41:06 +02:00
|
|
|
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
|
|
|
|
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
|
2020-04-05 16:11:03 +02:00
|
|
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
2021-03-31 23:32:53 +02:00
|
|
|
import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty
|
2020-04-05 16:11:03 +02:00
|
|
|
import org.schabi.newpipe.fragments.BaseStateFragment
|
2022-11-02 19:56:10 +01:00
|
|
|
import org.schabi.newpipe.info_list.ItemViewMode
|
2022-02-18 23:46:23 +01:00
|
|
|
import org.schabi.newpipe.info_list.dialog.InfoItemDialog
|
2021-01-16 04:32:01 +01:00
|
|
|
import org.schabi.newpipe.ktx.animate
|
2021-01-15 22:29:18 +01:00
|
|
|
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
|
2021-09-03 22:03:34 +02:00
|
|
|
import org.schabi.newpipe.ktx.slideUp
|
2020-04-05 16:11:03 +02:00
|
|
|
import org.schabi.newpipe.local.feed.item.StreamItem
|
2019-04-28 22:43:54 +02:00
|
|
|
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
2021-04-02 21:41:06 +02:00
|
|
|
import org.schabi.newpipe.local.subscription.SubscriptionManager
|
2021-09-08 20:51:43 +02:00
|
|
|
import org.schabi.newpipe.util.DeviceUtils
|
2019-04-28 22:43:54 +02:00
|
|
|
import org.schabi.newpipe.util.Localization
|
2020-04-05 16:11:03 +02:00
|
|
|
import org.schabi.newpipe.util.NavigationHelper
|
2021-07-31 10:51:59 +02:00
|
|
|
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
|
2022-11-02 19:56:10 +01:00
|
|
|
import org.schabi.newpipe.util.ThemeHelper.getItemViewMode
|
2022-03-17 18:34:44 +01:00
|
|
|
import org.schabi.newpipe.util.ThemeHelper.resolveDrawable
|
2021-07-19 20:47:50 +02:00
|
|
|
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
|
2020-12-20 05:23:05 +01:00
|
|
|
import java.time.OffsetDateTime
|
2021-09-03 22:03:34 +02:00
|
|
|
import java.util.function.Consumer
|
2019-04-28 22:43:54 +02:00
|
|
|
|
2020-04-05 16:11:03 +02:00
|
|
|
class FeedFragment : BaseStateFragment<FeedState>() {
|
2020-11-03 09:21:37 +01:00
|
|
|
private var _feedBinding: FragmentFeedBinding? = null
|
|
|
|
private val feedBinding get() = _feedBinding!!
|
|
|
|
|
2021-04-02 21:41:06 +02:00
|
|
|
private val disposables = CompositeDisposable()
|
|
|
|
|
2019-04-28 22:43:54 +02:00
|
|
|
private lateinit var viewModel: FeedViewModel
|
2020-04-05 16:11:03 +02:00
|
|
|
@State @JvmField var listState: Parcelable? = null
|
2019-04-28 22:43:54 +02:00
|
|
|
|
2020-01-28 06:59:49 +01:00
|
|
|
private var groupId = FeedGroupEntity.GROUP_ALL_ID
|
2019-04-28 22:43:54 +02:00
|
|
|
private var groupName = ""
|
2020-12-20 05:23:05 +01:00
|
|
|
private var oldestSubscriptionUpdate: OffsetDateTime? = null
|
2019-04-28 22:43:54 +02:00
|
|
|
|
2021-09-23 01:08:03 +02:00
|
|
|
private lateinit var groupAdapter: GroupieAdapter
|
2020-04-05 16:11:03 +02:00
|
|
|
@State @JvmField var showPlayedItems: Boolean = true
|
2022-06-24 18:03:48 +02:00
|
|
|
@State @JvmField var showFutureItems: Boolean = true
|
2020-04-05 16:11:03 +02:00
|
|
|
|
2021-06-15 18:40:25 +02:00
|
|
|
private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null
|
|
|
|
private var updateListViewModeOnResume = false
|
2021-07-11 04:00:32 +02:00
|
|
|
private var isRefreshing = false
|
2021-06-15 18:40:25 +02:00
|
|
|
|
2021-09-03 22:03:34 +02:00
|
|
|
private var lastNewItemsCount = 0
|
|
|
|
|
2019-04-28 22:43:54 +02:00
|
|
|
init {
|
|
|
|
setHasOptionsMenu(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
|
|
super.onCreate(savedInstanceState)
|
|
|
|
|
2020-03-31 19:20:15 +02:00
|
|
|
groupId = arguments?.getLong(KEY_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID)
|
2020-10-31 21:55:45 +01:00
|
|
|
?: FeedGroupEntity.GROUP_ALL_ID
|
2019-04-28 22:43:54 +02:00
|
|
|
groupName = arguments?.getString(KEY_GROUP_NAME) ?: ""
|
2021-06-15 18:40:25 +02:00
|
|
|
|
|
|
|
onSettingsChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
2023-01-16 23:05:29 +01:00
|
|
|
if (getString(R.string.list_view_mode_key).equals(key)) {
|
2021-06-15 18:40:25 +02:00
|
|
|
updateListViewModeOnResume = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
PreferenceManager.getDefaultSharedPreferences(activity)
|
|
|
|
.registerOnSharedPreferenceChangeListener(onSettingsChangeListener)
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
|
|
return inflater.inflate(R.layout.fragment_feed, container, false)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
|
2021-01-16 18:38:29 +01:00
|
|
|
// super.onViewCreated() calls initListeners() which require the binding to be initialized
|
2020-11-03 09:21:37 +01:00
|
|
|
_feedBinding = FragmentFeedBinding.bind(rootView)
|
2021-01-16 18:38:29 +01:00
|
|
|
super.onViewCreated(rootView, savedInstanceState)
|
2020-11-03 09:21:37 +01:00
|
|
|
|
2022-07-26 17:31:14 +02:00
|
|
|
val factory = FeedViewModel.getFactory(requireContext(), groupId)
|
|
|
|
viewModel = ViewModelProvider(this, factory)[FeedViewModel::class.java]
|
2021-11-11 19:46:15 +01:00
|
|
|
showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
|
2022-06-24 18:03:48 +02:00
|
|
|
showFutureItems = viewModel.getShowFutureItemsFromPreferences()
|
2022-03-18 18:15:44 +01:00
|
|
|
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
|
2020-04-05 16:11:03 +02:00
|
|
|
|
2021-09-23 01:08:03 +02:00
|
|
|
groupAdapter = GroupieAdapter().apply {
|
2020-04-05 16:11:03 +02:00
|
|
|
setOnItemClickListener(listenerStreamItem)
|
|
|
|
setOnItemLongClickListener(listenerStreamItem)
|
|
|
|
}
|
|
|
|
|
2021-09-03 22:03:34 +02:00
|
|
|
feedBinding.itemsList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
|
|
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
|
|
// Check if we scrolled to the top
|
|
|
|
if (newState == RecyclerView.SCROLL_STATE_IDLE &&
|
|
|
|
!recyclerView.canScrollVertically(-1)
|
|
|
|
) {
|
|
|
|
|
2021-09-12 15:04:33 +02:00
|
|
|
if (tryGetNewItemsLoadedButton()?.isVisible == true) {
|
2021-09-03 22:03:34 +02:00
|
|
|
hideNewItemsLoaded(true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2021-06-15 18:40:25 +02:00
|
|
|
feedBinding.itemsList.adapter = groupAdapter
|
|
|
|
setupListViewMode()
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onPause() {
|
|
|
|
super.onPause()
|
2021-01-18 11:35:45 +01:00
|
|
|
listState = feedBinding.itemsList.layoutManager?.onSaveInstanceState()
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onResume() {
|
|
|
|
super.onResume()
|
|
|
|
updateRelativeTimeViews()
|
2021-06-15 18:40:25 +02:00
|
|
|
|
|
|
|
if (updateListViewModeOnResume) {
|
|
|
|
updateListViewModeOnResume = false
|
|
|
|
|
|
|
|
setupListViewMode()
|
|
|
|
if (viewModel.stateLiveData.value != null) {
|
|
|
|
handleResult(viewModel.stateLiveData.value!!)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-08 05:11:10 +01:00
|
|
|
private fun setupListViewMode() {
|
2021-06-15 18:40:25 +02:00
|
|
|
// does everything needed to setup the layouts for grid or list modes
|
2021-07-31 10:51:59 +02:00
|
|
|
groupAdapter.spanCount = if (shouldUseGridLayout(context)) getGridSpanCountStreams(context) else 1
|
2021-06-15 18:40:25 +02:00
|
|
|
feedBinding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
|
|
|
|
spanSizeLookup = groupAdapter.spanSizeLookup
|
|
|
|
}
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun initListeners() {
|
|
|
|
super.initListeners()
|
2021-01-16 18:38:29 +01:00
|
|
|
feedBinding.refreshRootView.setOnClickListener { reloadContent() }
|
2021-01-15 22:29:18 +01:00
|
|
|
feedBinding.swipeRefreshLayout.setOnRefreshListener { reloadContent() }
|
2021-09-03 22:03:34 +02:00
|
|
|
feedBinding.newItemsLoadedButton.setOnClickListener {
|
|
|
|
hideNewItemsLoaded(true)
|
|
|
|
feedBinding.itemsList.scrollToPosition(0)
|
|
|
|
}
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
2020-05-01 20:13:21 +02:00
|
|
|
// /////////////////////////////////////////////////////////////////////////
|
2019-04-28 22:43:54 +02:00
|
|
|
// Menu
|
2020-05-01 20:13:21 +02:00
|
|
|
// /////////////////////////////////////////////////////////////////////////
|
2019-04-28 22:43:54 +02:00
|
|
|
|
|
|
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
|
|
super.onCreateOptionsMenu(menu, inflater)
|
2020-04-05 16:11:03 +02:00
|
|
|
|
|
|
|
activity.supportActionBar?.setDisplayShowTitleEnabled(true)
|
2019-10-11 06:09:28 +02:00
|
|
|
activity.supportActionBar?.setTitle(R.string.fragment_feed_title)
|
2019-04-28 22:43:54 +02:00
|
|
|
activity.supportActionBar?.subtitle = groupName
|
2020-03-14 04:11:30 +01:00
|
|
|
|
|
|
|
inflater.inflate(R.menu.menu_feed_fragment, menu)
|
2021-06-09 16:14:03 +02:00
|
|
|
updateTogglePlayedItemsButton(menu.findItem(R.id.menu_item_feed_toggle_played_items))
|
2022-06-24 18:03:48 +02:00
|
|
|
updateToggleFutureItemsButton(menu.findItem(R.id.menu_item_feed_toggle_future_items))
|
2020-03-14 04:11:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
|
|
if (item.itemId == R.id.menu_item_feed_help) {
|
|
|
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
|
|
|
|
2020-04-05 16:11:03 +02:00
|
|
|
val usingDedicatedMethod = sharedPreferences
|
|
|
|
.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
|
2020-03-14 04:11:30 +01:00
|
|
|
val enableDisableButtonText = when {
|
|
|
|
usingDedicatedMethod -> R.string.feed_use_dedicated_fetch_method_disable_button
|
|
|
|
else -> R.string.feed_use_dedicated_fetch_method_enable_button
|
|
|
|
}
|
|
|
|
|
|
|
|
AlertDialog.Builder(requireContext())
|
2020-10-31 21:55:45 +01:00
|
|
|
.setMessage(R.string.feed_use_dedicated_fetch_method_help_text)
|
|
|
|
.setNeutralButton(enableDisableButtonText) { _, _ ->
|
|
|
|
sharedPreferences.edit {
|
|
|
|
putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), !usingDedicatedMethod)
|
2020-03-14 04:11:30 +01:00
|
|
|
}
|
2020-10-31 21:55:45 +01:00
|
|
|
}
|
2021-08-30 16:15:26 +02:00
|
|
|
.setPositiveButton(resources.getString(R.string.ok), null)
|
2020-10-31 21:55:45 +01:00
|
|
|
.create()
|
|
|
|
.show()
|
2020-03-14 04:11:30 +01:00
|
|
|
return true
|
2020-04-05 16:11:03 +02:00
|
|
|
} else if (item.itemId == R.id.menu_item_feed_toggle_played_items) {
|
|
|
|
showPlayedItems = !item.isChecked
|
|
|
|
updateTogglePlayedItemsButton(item)
|
|
|
|
viewModel.togglePlayedItems(showPlayedItems)
|
2021-11-11 19:46:15 +01:00
|
|
|
viewModel.saveShowPlayedItemsToPreferences(showPlayedItems)
|
2022-06-24 18:03:48 +02:00
|
|
|
} else if (item.itemId == R.id.menu_item_feed_toggle_future_items) {
|
|
|
|
showFutureItems = !item.isChecked
|
|
|
|
updateToggleFutureItemsButton(item)
|
|
|
|
viewModel.toggleFutureItems(showFutureItems)
|
|
|
|
viewModel.saveShowFutureItemsToPreferences(showFutureItems)
|
2020-03-14 04:11:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return super.onOptionsItemSelected(item)
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onDestroyOptionsMenu() {
|
|
|
|
super.onDestroyOptionsMenu()
|
2019-10-09 04:59:11 +02:00
|
|
|
activity?.supportActionBar?.subtitle = null
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onDestroy() {
|
2021-04-02 21:41:06 +02:00
|
|
|
disposables.dispose()
|
2021-06-15 18:40:25 +02:00
|
|
|
if (onSettingsChangeListener != null) {
|
|
|
|
PreferenceManager.getDefaultSharedPreferences(activity)
|
|
|
|
.unregisterOnSharedPreferenceChangeListener(onSettingsChangeListener)
|
|
|
|
onSettingsChangeListener = null
|
|
|
|
}
|
|
|
|
|
2019-10-09 04:59:11 +02:00
|
|
|
super.onDestroy()
|
|
|
|
activity?.supportActionBar?.subtitle = null
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
2020-11-03 09:21:37 +01:00
|
|
|
override fun onDestroyView() {
|
2021-09-12 15:04:33 +02:00
|
|
|
// Ensure that all animations are canceled
|
2022-01-12 15:28:51 +01:00
|
|
|
tryGetNewItemsLoadedButton()?.clearAnimation()
|
2021-09-12 15:04:33 +02:00
|
|
|
|
2020-04-05 16:11:03 +02:00
|
|
|
feedBinding.itemsList.adapter = null
|
2020-11-03 09:21:37 +01:00
|
|
|
_feedBinding = null
|
|
|
|
super.onDestroyView()
|
|
|
|
}
|
|
|
|
|
2020-04-05 16:11:03 +02:00
|
|
|
private fun updateTogglePlayedItemsButton(menuItem: MenuItem) {
|
|
|
|
menuItem.isChecked = showPlayedItems
|
|
|
|
menuItem.icon = AppCompatResources.getDrawable(
|
|
|
|
requireContext(),
|
|
|
|
if (showPlayedItems) R.drawable.ic_visibility_on else R.drawable.ic_visibility_off
|
|
|
|
)
|
2022-09-13 15:08:37 +02:00
|
|
|
MenuItemCompat.setTooltipText(
|
|
|
|
menuItem,
|
|
|
|
getString(
|
|
|
|
if (showPlayedItems)
|
|
|
|
R.string.feed_toggle_hide_played_items
|
|
|
|
else
|
|
|
|
R.string.feed_toggle_show_played_items
|
|
|
|
)
|
|
|
|
)
|
2020-04-05 16:11:03 +02:00
|
|
|
}
|
|
|
|
|
2022-06-24 18:03:48 +02:00
|
|
|
private fun updateToggleFutureItemsButton(menuItem: MenuItem) {
|
|
|
|
menuItem.isChecked = showFutureItems
|
|
|
|
menuItem.icon = AppCompatResources.getDrawable(
|
|
|
|
requireContext(),
|
|
|
|
if (showFutureItems) R.drawable.ic_history_future else R.drawable.ic_history
|
|
|
|
)
|
2022-09-13 15:08:37 +02:00
|
|
|
MenuItemCompat.setTooltipText(
|
|
|
|
menuItem,
|
|
|
|
getString(
|
2022-09-19 11:21:38 +02:00
|
|
|
if (showFutureItems)
|
2022-09-13 15:08:37 +02:00
|
|
|
R.string.feed_toggle_hide_future_items
|
|
|
|
else
|
|
|
|
R.string.feed_toggle_show_future_items
|
|
|
|
)
|
|
|
|
)
|
2022-06-24 18:03:48 +02:00
|
|
|
}
|
|
|
|
|
2020-04-05 16:11:03 +02:00
|
|
|
// //////////////////////////////////////////////////////////////////////////
|
2019-04-28 22:43:54 +02:00
|
|
|
// Handling
|
2020-04-05 16:11:03 +02:00
|
|
|
// //////////////////////////////////////////////////////////////////////////
|
2019-04-28 22:43:54 +02:00
|
|
|
|
|
|
|
override fun showLoading() {
|
2020-12-11 14:55:47 +01:00
|
|
|
super.showLoading()
|
2021-01-15 22:29:18 +01:00
|
|
|
feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling()
|
2021-01-16 04:32:01 +01:00
|
|
|
feedBinding.refreshRootView.animate(false, 0)
|
|
|
|
feedBinding.loadingProgressText.animate(true, 200)
|
2021-01-15 22:29:18 +01:00
|
|
|
feedBinding.swipeRefreshLayout.isRefreshing = true
|
2021-07-11 04:00:32 +02:00
|
|
|
isRefreshing = true
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun hideLoading() {
|
2020-12-11 14:55:47 +01:00
|
|
|
super.hideLoading()
|
2020-04-05 16:11:03 +02:00
|
|
|
feedBinding.itemsList.animate(true, 0)
|
2021-01-16 04:32:01 +01:00
|
|
|
feedBinding.refreshRootView.animate(true, 200)
|
|
|
|
feedBinding.loadingProgressText.animate(false, 0)
|
2021-01-15 22:29:18 +01:00
|
|
|
feedBinding.swipeRefreshLayout.isRefreshing = false
|
2021-07-11 04:00:32 +02:00
|
|
|
isRefreshing = false
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun showEmptyState() {
|
2020-12-11 14:55:47 +01:00
|
|
|
super.showEmptyState()
|
2021-01-15 22:29:18 +01:00
|
|
|
feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling()
|
2021-01-16 04:32:01 +01:00
|
|
|
feedBinding.refreshRootView.animate(true, 200)
|
|
|
|
feedBinding.loadingProgressText.animate(false, 0)
|
2021-01-15 22:29:18 +01:00
|
|
|
feedBinding.swipeRefreshLayout.isRefreshing = false
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun handleResult(result: FeedState) {
|
|
|
|
when (result) {
|
|
|
|
is FeedState.ProgressState -> handleProgressState(result)
|
|
|
|
is FeedState.LoadedState -> handleLoadedState(result)
|
|
|
|
is FeedState.ErrorState -> if (handleErrorState(result)) return
|
|
|
|
}
|
|
|
|
|
|
|
|
updateRefreshViewState()
|
|
|
|
}
|
|
|
|
|
2020-12-11 14:55:47 +01:00
|
|
|
override fun handleError() {
|
|
|
|
super.handleError()
|
2021-01-15 22:29:18 +01:00
|
|
|
feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling()
|
|
|
|
feedBinding.refreshRootView.animate(false, 0)
|
|
|
|
feedBinding.loadingProgressText.animate(false, 0)
|
|
|
|
feedBinding.swipeRefreshLayout.isRefreshing = false
|
2021-07-11 04:00:32 +02:00
|
|
|
isRefreshing = false
|
2020-12-11 14:55:47 +01:00
|
|
|
}
|
|
|
|
|
2019-04-28 22:43:54 +02:00
|
|
|
private fun handleProgressState(progressState: FeedState.ProgressState) {
|
|
|
|
showLoading()
|
|
|
|
|
|
|
|
val isIndeterminate = progressState.currentProgress == -1 &&
|
2020-10-31 21:55:45 +01:00
|
|
|
progressState.maxProgress == -1
|
2019-04-28 22:43:54 +02:00
|
|
|
|
2021-01-18 11:35:45 +01:00
|
|
|
feedBinding.loadingProgressText.text = if (!isIndeterminate) {
|
|
|
|
"${progressState.currentProgress}/${progressState.maxProgress}"
|
2019-04-28 22:43:54 +02:00
|
|
|
} else if (progressState.progressMessage > 0) {
|
2021-01-20 10:44:44 +01:00
|
|
|
getString(progressState.progressMessage)
|
2019-04-28 22:43:54 +02:00
|
|
|
} else {
|
2021-01-18 11:35:45 +01:00
|
|
|
"∞/∞"
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
2020-11-03 09:21:37 +01:00
|
|
|
feedBinding.loadingProgressBar.isIndeterminate = isIndeterminate ||
|
2021-01-15 02:15:07 +01:00
|
|
|
(progressState.maxProgress > 0 && progressState.currentProgress == 0)
|
2020-11-03 09:21:37 +01:00
|
|
|
feedBinding.loadingProgressBar.progress = progressState.currentProgress
|
2019-04-28 22:43:54 +02:00
|
|
|
|
2020-11-03 09:21:37 +01:00
|
|
|
feedBinding.loadingProgressBar.max = progressState.maxProgress
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
2021-12-27 16:14:26 +01:00
|
|
|
private fun showInfoItemDialog(item: StreamInfoItem) {
|
2020-04-05 16:11:03 +02:00
|
|
|
val context = context
|
|
|
|
val activity: Activity? = getActivity()
|
|
|
|
if (context == null || context.resources == null || activity == null) return
|
|
|
|
|
2021-12-26 15:34:36 +01:00
|
|
|
InfoItemDialog.Builder(activity, context, this, item).create().show()
|
2020-04-05 16:11:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private val listenerStreamItem = object : OnItemClickListener, OnItemLongClickListener {
|
|
|
|
override fun onItemClick(item: Item<*>, view: View) {
|
2021-07-11 04:00:32 +02:00
|
|
|
if (item is StreamItem && !isRefreshing) {
|
2020-04-05 16:11:03 +02:00
|
|
|
val stream = item.streamWithState.stream
|
|
|
|
NavigationHelper.openVideoDetailFragment(
|
|
|
|
requireContext(), fm,
|
|
|
|
stream.serviceId, stream.url, stream.title, null, false
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onItemLongClick(item: Item<*>, view: View): Boolean {
|
2021-07-11 04:00:32 +02:00
|
|
|
if (item is StreamItem && !isRefreshing) {
|
2021-12-27 16:14:26 +01:00
|
|
|
showInfoItemDialog(item.streamWithState.stream.toStreamInfoItem())
|
2020-04-05 16:11:03 +02:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@SuppressLint("StringFormatMatches")
|
2019-04-28 22:43:54 +02:00
|
|
|
private fun handleLoadedState(loadedState: FeedState.LoadedState) {
|
2022-11-02 19:56:10 +01:00
|
|
|
val itemVersion = when (getItemViewMode(requireContext())) {
|
|
|
|
ItemViewMode.GRID -> StreamItem.ItemVersion.GRID
|
|
|
|
ItemViewMode.CARD -> StreamItem.ItemVersion.CARD
|
|
|
|
else -> StreamItem.ItemVersion.NORMAL
|
2020-04-05 16:11:03 +02:00
|
|
|
}
|
|
|
|
loadedState.items.forEach { it.itemVersion = itemVersion }
|
|
|
|
|
2021-09-03 22:03:34 +02:00
|
|
|
// This need to be saved in a variable as the update occurs async
|
|
|
|
val oldOldestSubscriptionUpdate = oldestSubscriptionUpdate
|
|
|
|
|
2022-03-18 18:15:44 +01:00
|
|
|
groupAdapter.updateAsync(loadedState.items, false) {
|
|
|
|
oldOldestSubscriptionUpdate?.run {
|
|
|
|
highlightNewItemsAfter(oldOldestSubscriptionUpdate)
|
2021-09-03 22:03:34 +02:00
|
|
|
}
|
2022-03-18 18:15:44 +01:00
|
|
|
}
|
2020-04-05 16:11:03 +02:00
|
|
|
|
2019-04-28 22:43:54 +02:00
|
|
|
listState?.run {
|
2020-11-03 09:21:37 +01:00
|
|
|
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
|
2019-04-28 22:43:54 +02:00
|
|
|
listState = null
|
|
|
|
}
|
|
|
|
|
2021-04-02 21:41:06 +02:00
|
|
|
val feedsNotLoaded = loadedState.notLoadedCount > 0
|
|
|
|
feedBinding.refreshSubtitleText.isVisible = feedsNotLoaded
|
|
|
|
if (feedsNotLoaded) {
|
2021-01-15 02:15:07 +01:00
|
|
|
feedBinding.refreshSubtitleText.text = getString(
|
|
|
|
R.string.feed_subscription_not_loaded_count,
|
|
|
|
loadedState.notLoadedCount
|
|
|
|
)
|
2019-12-16 08:36:04 +01:00
|
|
|
}
|
|
|
|
|
2021-04-02 21:42:40 +02:00
|
|
|
if (oldestSubscriptionUpdate != loadedState.oldestUpdate ||
|
|
|
|
(oldestSubscriptionUpdate == null && loadedState.oldestUpdate == null)
|
|
|
|
) {
|
2021-04-02 18:16:24 +02:00
|
|
|
// ignore errors if they have already been handled for the current update
|
|
|
|
handleItemsErrors(loadedState.itemsErrors)
|
|
|
|
}
|
|
|
|
oldestSubscriptionUpdate = loadedState.oldestUpdate
|
|
|
|
|
2019-04-28 22:43:54 +02:00
|
|
|
if (loadedState.items.isEmpty()) {
|
|
|
|
showEmptyState()
|
|
|
|
} else {
|
|
|
|
hideLoading()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun handleErrorState(errorState: FeedState.ErrorState): Boolean {
|
2020-12-11 14:55:47 +01:00
|
|
|
return if (errorState.error == null) {
|
|
|
|
hideLoading()
|
|
|
|
false
|
|
|
|
} else {
|
2021-04-02 18:16:24 +02:00
|
|
|
showError(ErrorInfo(errorState.error, UserAction.REQUESTED_FEED, "Loading feed"))
|
2020-12-11 14:55:47 +01:00
|
|
|
true
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-02 18:16:24 +02:00
|
|
|
private fun handleItemsErrors(errors: List<Throwable>) {
|
2021-06-09 16:15:04 +02:00
|
|
|
errors.forEachIndexed { i, t ->
|
2021-04-02 18:16:24 +02:00
|
|
|
if (t is FeedLoadService.RequestException &&
|
|
|
|
t.cause is ContentNotAvailableException
|
|
|
|
) {
|
|
|
|
Single.fromCallable {
|
|
|
|
NewPipeDatabase.getInstance(requireContext()).subscriptionDAO()
|
|
|
|
.getSubscription(t.subscriptionId)
|
|
|
|
}.subscribeOn(Schedulers.io())
|
|
|
|
.observeOn(AndroidSchedulers.mainThread())
|
|
|
|
.subscribe(
|
2022-02-26 22:08:10 +01:00
|
|
|
{ subscriptionEntity ->
|
2021-04-02 18:16:24 +02:00
|
|
|
handleFeedNotAvailable(
|
|
|
|
subscriptionEntity,
|
2021-04-02 21:42:40 +02:00
|
|
|
t.cause,
|
2021-04-02 18:16:24 +02:00
|
|
|
errors.subList(i + 1, errors.size)
|
|
|
|
)
|
|
|
|
},
|
2021-11-05 18:04:49 +01:00
|
|
|
{ throwable -> Log.e(TAG, "Unable to process", throwable) }
|
2021-04-02 18:16:24 +02:00
|
|
|
)
|
|
|
|
return // this will be called on the remaining errors by handleFeedNotAvailable()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-02 21:41:06 +02:00
|
|
|
private fun handleFeedNotAvailable(
|
|
|
|
subscriptionEntity: SubscriptionEntity,
|
2023-01-16 23:20:50 +01:00
|
|
|
cause: Throwable?,
|
2021-04-02 18:16:24 +02:00
|
|
|
nextItemsErrors: List<Throwable>
|
2021-04-02 21:41:06 +02:00
|
|
|
) {
|
|
|
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
|
|
|
val isFastFeedModeEnabled = sharedPreferences.getBoolean(
|
|
|
|
getString(R.string.feed_use_dedicated_fetch_method_key), false
|
|
|
|
)
|
2021-04-02 18:16:24 +02:00
|
|
|
|
2021-04-02 21:41:06 +02:00
|
|
|
val builder = AlertDialog.Builder(requireContext())
|
|
|
|
.setTitle(R.string.feed_load_error)
|
|
|
|
.setPositiveButton(
|
2021-04-02 18:16:24 +02:00
|
|
|
R.string.unsubscribe
|
|
|
|
) { _, _ ->
|
|
|
|
SubscriptionManager(requireContext()).deleteSubscription(
|
|
|
|
subscriptionEntity.serviceId, subscriptionEntity.url
|
|
|
|
).subscribe()
|
|
|
|
handleItemsErrors(nextItemsErrors)
|
|
|
|
}
|
|
|
|
.setNegativeButton(R.string.cancel) { _, _ -> }
|
|
|
|
|
2021-03-31 23:32:53 +02:00
|
|
|
var message = getString(R.string.feed_load_error_account_info, subscriptionEntity.name)
|
2021-04-02 21:41:06 +02:00
|
|
|
if (cause is AccountTerminatedException) {
|
2021-03-31 23:32:53 +02:00
|
|
|
message += "\n" + getString(R.string.feed_load_error_terminated)
|
|
|
|
} else if (cause is ContentNotAvailableException) {
|
|
|
|
if (isFastFeedModeEnabled) {
|
|
|
|
message += "\n" + getString(R.string.feed_load_error_fast_unknown)
|
|
|
|
builder.setNeutralButton(R.string.feed_use_dedicated_fetch_method_disable_button) { _, _ ->
|
2021-04-02 21:41:06 +02:00
|
|
|
sharedPreferences.edit {
|
|
|
|
putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
|
|
|
|
}
|
|
|
|
}
|
2021-03-31 23:32:53 +02:00
|
|
|
} else if (!isNullOrEmpty(cause.message)) {
|
|
|
|
message += "\n" + cause.message
|
|
|
|
}
|
2021-04-02 21:41:06 +02:00
|
|
|
}
|
2021-03-31 23:32:53 +02:00
|
|
|
builder.setMessage(message).create().show()
|
2021-04-02 21:41:06 +02:00
|
|
|
}
|
|
|
|
|
2019-04-28 22:43:54 +02:00
|
|
|
private fun updateRelativeTimeViews() {
|
|
|
|
updateRefreshViewState()
|
2020-04-05 16:11:03 +02:00
|
|
|
groupAdapter.notifyItemRangeChanged(
|
|
|
|
0, groupAdapter.itemCount,
|
|
|
|
StreamItem.UPDATE_RELATIVE_TIME
|
|
|
|
)
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun updateRefreshViewState() {
|
2020-12-20 05:23:05 +01:00
|
|
|
feedBinding.refreshText.text = getString(
|
|
|
|
R.string.feed_oldest_subscription_update,
|
|
|
|
oldestSubscriptionUpdate?.let { Localization.relativeTime(it) } ?: "—"
|
|
|
|
)
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
2021-09-03 22:03:34 +02:00
|
|
|
/**
|
|
|
|
* Highlights all items that are after the specified time
|
|
|
|
*/
|
|
|
|
private fun highlightNewItemsAfter(updateTime: OffsetDateTime) {
|
|
|
|
var highlightCount = 0
|
|
|
|
|
|
|
|
var doCheck = true
|
|
|
|
|
|
|
|
for (i in 0 until groupAdapter.itemCount) {
|
|
|
|
val item = groupAdapter.getItem(i) as StreamItem
|
|
|
|
|
2021-09-05 19:04:42 +02:00
|
|
|
var typeface = Typeface.DEFAULT
|
2021-09-28 21:51:57 +02:00
|
|
|
var backgroundSupplier = { ctx: Context ->
|
|
|
|
resolveDrawable(ctx, R.attr.selectableItemBackground)
|
|
|
|
}
|
2021-09-03 22:03:34 +02:00
|
|
|
if (doCheck) {
|
2021-09-28 21:51:57 +02:00
|
|
|
// If the uploadDate is null or true we should highlight the item
|
2021-09-03 22:03:34 +02:00
|
|
|
if (item.streamWithState.stream.uploadDate?.isAfter(updateTime) != false) {
|
|
|
|
highlightCount++
|
2021-09-28 21:51:57 +02:00
|
|
|
|
|
|
|
typeface = Typeface.DEFAULT_BOLD
|
|
|
|
backgroundSupplier = { ctx: Context ->
|
|
|
|
// Merge the drawables together. Otherwise we would lose the "select" effect
|
|
|
|
LayerDrawable(
|
|
|
|
arrayOf(
|
|
|
|
resolveDrawable(ctx, R.attr.dashed_border),
|
|
|
|
resolveDrawable(ctx, R.attr.selectableItemBackground)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
2021-09-03 22:03:34 +02:00
|
|
|
} else {
|
2021-11-14 13:16:53 +01:00
|
|
|
// Decreases execution time due to the order of the items (newest always on top)
|
2021-09-03 22:03:34 +02:00
|
|
|
// Once a item is is before the updateTime we can skip all following items
|
|
|
|
doCheck = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// The highlighter has to be always set
|
|
|
|
// When it's only set on items that are highlighted it will highlight all items
|
|
|
|
// due to the fact that itemRoot is getting recycled
|
|
|
|
item.execBindEnd = Consumer { viewBinding ->
|
|
|
|
val context = viewBinding.itemRoot.context
|
2021-09-28 21:51:57 +02:00
|
|
|
viewBinding.itemRoot.background = backgroundSupplier.invoke(context)
|
2021-09-05 19:04:42 +02:00
|
|
|
viewBinding.itemVideoTitleView.typeface = typeface
|
2021-09-03 22:03:34 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Force updates all items so that the highlighting is correct
|
|
|
|
// If this isn't done visible items that are already highlighted will stay in a highlighted
|
|
|
|
// state until the user scrolls them out of the visible area which causes a update/bind-call
|
|
|
|
groupAdapter.notifyItemRangeChanged(
|
|
|
|
0,
|
2022-08-08 03:40:16 +02:00
|
|
|
MathUtils.clamp(highlightCount, lastNewItemsCount, groupAdapter.itemCount)
|
2021-09-03 22:03:34 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
if (highlightCount > 0) {
|
|
|
|
showNewItemsLoaded()
|
|
|
|
}
|
|
|
|
|
|
|
|
lastNewItemsCount = highlightCount
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun showNewItemsLoaded() {
|
2021-09-12 15:04:33 +02:00
|
|
|
tryGetNewItemsLoadedButton()?.clearAnimation()
|
|
|
|
tryGetNewItemsLoadedButton()
|
|
|
|
?.slideUp(
|
2021-09-03 22:03:34 +02:00
|
|
|
250L,
|
|
|
|
delay = 100,
|
|
|
|
execOnEnd = {
|
2021-09-08 20:51:43 +02:00
|
|
|
// Disabled animations would result in immediately hiding the button
|
|
|
|
// after it showed up
|
|
|
|
if (DeviceUtils.hasAnimationsAnimatorDurationEnabled(context)) {
|
|
|
|
// Hide the new items-"popup" after 10s
|
|
|
|
hideNewItemsLoaded(true, 10000)
|
|
|
|
}
|
2021-09-03 22:03:34 +02:00
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun hideNewItemsLoaded(animate: Boolean, delay: Long = 0) {
|
2021-09-12 15:04:33 +02:00
|
|
|
tryGetNewItemsLoadedButton()?.clearAnimation()
|
2021-09-03 22:03:34 +02:00
|
|
|
if (animate) {
|
2021-09-12 15:04:33 +02:00
|
|
|
tryGetNewItemsLoadedButton()?.animate(
|
2021-09-03 22:03:34 +02:00
|
|
|
false,
|
|
|
|
200,
|
|
|
|
delay = delay,
|
|
|
|
execOnEnd = {
|
|
|
|
// Make the layout invisible so that the onScroll toTop method
|
|
|
|
// only does necessary work
|
2021-09-12 15:04:33 +02:00
|
|
|
tryGetNewItemsLoadedButton()?.isVisible = false
|
2021-09-03 22:03:34 +02:00
|
|
|
}
|
|
|
|
)
|
|
|
|
} else {
|
2021-09-12 15:04:33 +02:00
|
|
|
tryGetNewItemsLoadedButton()?.isVisible = false
|
2021-09-03 22:03:34 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-12 15:04:33 +02:00
|
|
|
/**
|
|
|
|
* The view/button can be disposed/set to null under certain circumstances.
|
|
|
|
* E.g. when the animation is still in progress but the view got destroyed.
|
|
|
|
* This method is a helper for such states and can be used in affected code blocks.
|
|
|
|
*/
|
|
|
|
private fun tryGetNewItemsLoadedButton(): Button? {
|
|
|
|
return _feedBinding?.newItemsLoadedButton
|
|
|
|
}
|
|
|
|
|
2020-05-01 20:13:21 +02:00
|
|
|
// /////////////////////////////////////////////////////////////////////////
|
2019-04-28 22:43:54 +02:00
|
|
|
// Load Service Handling
|
2020-05-01 20:13:21 +02:00
|
|
|
// /////////////////////////////////////////////////////////////////////////
|
2019-04-28 22:43:54 +02:00
|
|
|
|
|
|
|
override fun doInitialLoadLogic() {}
|
|
|
|
|
2021-01-16 18:38:29 +01:00
|
|
|
override fun reloadContent() {
|
2021-09-03 22:03:34 +02:00
|
|
|
hideNewItemsLoaded(false)
|
|
|
|
|
2020-10-31 21:55:45 +01:00
|
|
|
getActivity()?.startService(
|
|
|
|
Intent(requireContext(), FeedLoadService::class.java).apply {
|
|
|
|
putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)
|
|
|
|
}
|
|
|
|
)
|
2019-04-28 22:43:54 +02:00
|
|
|
listState = null
|
|
|
|
}
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
const val KEY_GROUP_ID = "ARG_GROUP_ID"
|
|
|
|
const val KEY_GROUP_NAME = "ARG_GROUP_NAME"
|
|
|
|
|
|
|
|
@JvmStatic
|
2020-01-28 06:59:49 +01:00
|
|
|
fun newInstance(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, groupName: String? = null): FeedFragment {
|
2019-04-28 22:43:54 +02:00
|
|
|
val feedFragment = FeedFragment()
|
2020-10-17 12:08:45 +02:00
|
|
|
feedFragment.arguments = bundleOf(KEY_GROUP_ID to groupId, KEY_GROUP_NAME to groupName)
|
2019-04-28 22:43:54 +02:00
|
|
|
return feedFragment
|
|
|
|
}
|
|
|
|
}
|
2020-03-31 19:20:15 +02:00
|
|
|
}
|