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
|
|
|
|
|
|
|
|
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
|
|
|
|
import android.view.MenuItem
|
|
|
|
import android.view.View
|
|
|
|
import android.view.ViewGroup
|
2020-03-14 04:11:30 +01:00
|
|
|
import androidx.appcompat.app.AlertDialog
|
2020-11-18 23:29:58 +01:00
|
|
|
import androidx.core.content.edit
|
2020-10-17 12:08:45 +02:00
|
|
|
import androidx.core.os.bundleOf
|
2020-10-17 12:24:35 +02:00
|
|
|
import androidx.core.view.isVisible
|
2019-04-28 22:43:54 +02:00
|
|
|
import androidx.lifecycle.Observer
|
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-11-15 17:54:40 +01:00
|
|
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
2019-04-28 22:43:54 +02:00
|
|
|
import icepick.State
|
2020-05-01 20:13:01 +02:00
|
|
|
import kotlinx.android.synthetic.main.error_retry.error_button_retry
|
|
|
|
import kotlinx.android.synthetic.main.error_retry.error_message_view
|
|
|
|
import kotlinx.android.synthetic.main.fragment_feed.empty_state_view
|
|
|
|
import kotlinx.android.synthetic.main.fragment_feed.error_panel
|
|
|
|
import kotlinx.android.synthetic.main.fragment_feed.items_list
|
|
|
|
import kotlinx.android.synthetic.main.fragment_feed.loading_progress_bar
|
|
|
|
import kotlinx.android.synthetic.main.fragment_feed.loading_progress_text
|
|
|
|
import kotlinx.android.synthetic.main.fragment_feed.refresh_root_view
|
|
|
|
import kotlinx.android.synthetic.main.fragment_feed.refresh_subtitle_text
|
|
|
|
import kotlinx.android.synthetic.main.fragment_feed.refresh_text
|
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
|
2019-04-28 22:43:54 +02:00
|
|
|
import org.schabi.newpipe.fragments.list.BaseListFragment
|
|
|
|
import org.schabi.newpipe.local.feed.service.FeedLoadService
|
|
|
|
import org.schabi.newpipe.report.UserAction
|
|
|
|
import org.schabi.newpipe.util.AnimationUtils.animateView
|
|
|
|
import org.schabi.newpipe.util.Localization
|
2020-10-31 21:55:45 +01:00
|
|
|
import java.util.Calendar
|
2019-04-28 22:43:54 +02:00
|
|
|
|
|
|
|
class FeedFragment : BaseListFragment<FeedState, Unit>() {
|
|
|
|
private lateinit var viewModel: FeedViewModel
|
2020-11-17 19:23:29 +01:00
|
|
|
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
2020-03-31 19:20:15 +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 = ""
|
2019-12-16 08:36:04 +01:00
|
|
|
private var oldestSubscriptionUpdate: Calendar? = null
|
2019-04-28 22:43:54 +02:00
|
|
|
|
|
|
|
init {
|
|
|
|
setHasOptionsMenu(true)
|
2020-03-31 19:20:15 +02:00
|
|
|
setUseDefaultStateSaving(false)
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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) ?: ""
|
|
|
|
}
|
|
|
|
|
|
|
|
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?) {
|
|
|
|
super.onViewCreated(rootView, savedInstanceState)
|
2020-11-15 17:54:40 +01:00
|
|
|
swipeRefreshLayout = requireView().findViewById(R.id.swiperefresh)
|
2020-11-17 19:23:29 +01:00
|
|
|
swipeRefreshLayout.setOnRefreshListener { reloadContent() }
|
2020-08-27 22:56:12 +02:00
|
|
|
viewModel = ViewModelProvider(this, FeedViewModel.Factory(requireContext(), groupId)).get(FeedViewModel::class.java)
|
2019-04-28 22:43:54 +02:00
|
|
|
viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { it?.let(::handleResult) })
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onPause() {
|
|
|
|
super.onPause()
|
|
|
|
listState = items_list?.layoutManager?.onSaveInstanceState()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onResume() {
|
|
|
|
super.onResume()
|
|
|
|
updateRelativeTimeViews()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
|
|
|
super.setUserVisibleHint(isVisibleToUser)
|
|
|
|
|
|
|
|
if (!isVisibleToUser && view != null) {
|
|
|
|
updateRelativeTimeViews()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun initListeners() {
|
|
|
|
super.initListeners()
|
|
|
|
refresh_root_view.setOnClickListener {
|
|
|
|
triggerUpdate()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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)
|
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)
|
|
|
|
|
|
|
|
if (useAsFrontPage) {
|
2020-03-31 19:20:15 +02:00
|
|
|
menu.findItem(R.id.menu_item_feed_help).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
|
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())
|
|
|
|
|
|
|
|
val usingDedicatedMethod = sharedPreferences.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
|
|
|
|
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
|
|
|
}
|
|
|
|
.setPositiveButton(resources.getString(R.string.finish), null)
|
|
|
|
.create()
|
|
|
|
.show()
|
2020-03-14 04:11:30 +01:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
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() {
|
|
|
|
super.onDestroy()
|
|
|
|
activity?.supportActionBar?.subtitle = null
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
2020-05-01 20:13:21 +02:00
|
|
|
// /////////////////////////////////////////////////////////////////////////
|
2019-04-28 22:43:54 +02:00
|
|
|
// Handling
|
2020-05-01 20:13:21 +02:00
|
|
|
// /////////////////////////////////////////////////////////////////////////
|
2019-04-28 22:43:54 +02:00
|
|
|
|
|
|
|
override fun showLoading() {
|
|
|
|
animateView(refresh_root_view, false, 0)
|
|
|
|
animateView(items_list, false, 0)
|
|
|
|
|
|
|
|
animateView(loading_progress_bar, true, 200)
|
|
|
|
animateView(loading_progress_text, true, 200)
|
|
|
|
|
|
|
|
empty_state_view?.let { animateView(it, false, 0) }
|
|
|
|
animateView(error_panel, false, 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun hideLoading() {
|
|
|
|
animateView(refresh_root_view, true, 200)
|
|
|
|
animateView(items_list, true, 300)
|
|
|
|
|
|
|
|
animateView(loading_progress_bar, false, 0)
|
|
|
|
animateView(loading_progress_text, false, 0)
|
|
|
|
|
|
|
|
empty_state_view?.let { animateView(it, false, 0) }
|
|
|
|
animateView(error_panel, false, 0)
|
2020-11-17 19:23:29 +01:00
|
|
|
swipeRefreshLayout.isRefreshing = false
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun showEmptyState() {
|
|
|
|
animateView(refresh_root_view, true, 200)
|
|
|
|
animateView(items_list, false, 0)
|
|
|
|
|
|
|
|
animateView(loading_progress_bar, false, 0)
|
|
|
|
animateView(loading_progress_text, false, 0)
|
|
|
|
|
|
|
|
empty_state_view?.let { animateView(it, true, 800) }
|
|
|
|
animateView(error_panel, false, 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun showError(message: String, showRetryButton: Boolean) {
|
|
|
|
infoListAdapter.clearStreamItemList()
|
|
|
|
animateView(refresh_root_view, false, 120)
|
|
|
|
animateView(items_list, false, 120)
|
|
|
|
|
|
|
|
animateView(loading_progress_bar, false, 120)
|
|
|
|
animateView(loading_progress_text, false, 120)
|
|
|
|
|
|
|
|
error_message_view.text = message
|
|
|
|
animateView(error_button_retry, showRetryButton, if (showRetryButton) 600 else 0)
|
|
|
|
animateView(error_panel, true, 300)
|
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
if (!isIndeterminate) {
|
|
|
|
loading_progress_text.text = "${progressState.currentProgress}/${progressState.maxProgress}"
|
|
|
|
} else if (progressState.progressMessage > 0) {
|
|
|
|
loading_progress_text?.setText(progressState.progressMessage)
|
|
|
|
} else {
|
|
|
|
loading_progress_text?.text = "∞/∞"
|
|
|
|
}
|
|
|
|
|
|
|
|
loading_progress_bar.isIndeterminate = isIndeterminate ||
|
2020-10-31 21:55:45 +01:00
|
|
|
(progressState.maxProgress > 0 && progressState.currentProgress == 0)
|
2019-12-16 08:36:04 +01:00
|
|
|
loading_progress_bar.progress = progressState.currentProgress
|
2019-04-28 22:43:54 +02:00
|
|
|
|
|
|
|
loading_progress_bar.max = progressState.maxProgress
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun handleLoadedState(loadedState: FeedState.LoadedState) {
|
|
|
|
infoListAdapter.setInfoItemList(loadedState.items)
|
|
|
|
listState?.run {
|
|
|
|
items_list.layoutManager?.onRestoreInstanceState(listState)
|
|
|
|
listState = null
|
|
|
|
}
|
|
|
|
|
2019-12-16 08:36:04 +01:00
|
|
|
oldestSubscriptionUpdate = loadedState.oldestUpdate
|
|
|
|
|
2020-11-18 23:29:58 +01:00
|
|
|
val loadedCount = loadedState.notLoadedCount > 0
|
|
|
|
refresh_subtitle_text.isVisible = loadedCount
|
|
|
|
if (loadedCount) {
|
2019-12-16 08:36:04 +01:00
|
|
|
refresh_subtitle_text.text = getString(R.string.feed_subscription_not_loaded_count, loadedState.notLoadedCount)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (loadedState.itemsErrors.isNotEmpty()) {
|
2020-10-31 21:55:45 +01:00
|
|
|
showSnackBarError(
|
|
|
|
loadedState.itemsErrors, UserAction.REQUESTED_FEED,
|
|
|
|
"none", "Loading feed", R.string.general_error
|
|
|
|
)
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (loadedState.items.isEmpty()) {
|
|
|
|
showEmptyState()
|
|
|
|
} else {
|
|
|
|
hideLoading()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun handleErrorState(errorState: FeedState.ErrorState): Boolean {
|
|
|
|
hideLoading()
|
|
|
|
errorState.error?.let {
|
|
|
|
onError(errorState.error)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun updateRelativeTimeViews() {
|
|
|
|
updateRefreshViewState()
|
|
|
|
infoListAdapter.notifyDataSetChanged()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun updateRefreshViewState() {
|
2019-12-16 08:36:04 +01:00
|
|
|
val oldestSubscriptionUpdateText = when {
|
|
|
|
oldestSubscriptionUpdate != null -> Localization.relativeTime(oldestSubscriptionUpdate!!)
|
2019-04-28 22:43:54 +02:00
|
|
|
else -> "—"
|
|
|
|
}
|
|
|
|
|
2019-12-16 08:36:04 +01:00
|
|
|
refresh_text?.text = getString(R.string.feed_oldest_subscription_update, oldestSubscriptionUpdateText)
|
2019-04-28 22:43:54 +02:00
|
|
|
}
|
|
|
|
|
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() {}
|
|
|
|
override fun reloadContent() = triggerUpdate()
|
|
|
|
override fun loadMoreItems() {}
|
|
|
|
override fun hasMoreItems() = false
|
|
|
|
|
|
|
|
private fun triggerUpdate() {
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onError(exception: Throwable): Boolean {
|
|
|
|
if (super.onError(exception)) return true
|
|
|
|
|
|
|
|
if (useAsFrontPage) {
|
|
|
|
showSnackBarError(exception, UserAction.REQUESTED_FEED, "none", "Loading Feed", 0)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
onUnrecoverableError(exception, UserAction.REQUESTED_FEED, "none", "Loading Feed", 0)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
}
|