Add ability to hide played items in a feed

- Use components from the new Groupie list library for displaying the
feed list.
This commit is contained in:
Mauricio Colli 2020-04-05 11:11:03 -03:00 committed by Stypox
parent 56cd84c1fe
commit e846f69e38
No known key found for this signature in database
GPG key ID: 4BDF1B40A49FDD23
21 changed files with 668 additions and 63 deletions

View file

@ -9,7 +9,7 @@ import androidx.room.Update
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
import org.schabi.newpipe.database.feed.model.FeedEntity import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.database.subscription.SubscriptionEntity
import java.time.OffsetDateTime import java.time.OffsetDateTime
@ -20,21 +20,34 @@ abstract class FeedDAO {
@Query( @Query(
""" """
SELECT s.* FROM streams s SELECT s.*, sst.progress_time, (sh.stream_id IS NOT NULL) AS is_stream_in_history
FROM streams s
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
INNER JOIN feed f INNER JOIN feed f
ON s.uid = f.stream_id ON s.uid = f.stream_id
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500 LIMIT 500
""" """
) )
abstract fun getAllStreams(): Flowable<List<StreamEntity>> abstract fun getAllStreams(): Flowable<List<StreamWithState>>
@Query( @Query(
""" """
SELECT s.* FROM streams s SELECT s.*, sst.progress_time, (sh.stream_id IS NOT NULL) AS is_stream_in_history
FROM streams s
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
INNER JOIN feed f INNER JOIN feed f
ON s.uid = f.stream_id ON s.uid = f.stream_id
@ -42,16 +55,69 @@ abstract class FeedDAO {
INNER JOIN feed_group_subscription_join fgs INNER JOIN feed_group_subscription_join fgs
ON fgs.subscription_id = f.subscription_id ON fgs.subscription_id = f.subscription_id
INNER JOIN feed_group fg
ON fg.uid = fgs.group_id
WHERE fgs.group_id = :groupId WHERE fgs.group_id = :groupId
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500 LIMIT 500
""" """
) )
abstract fun getAllStreamsFromGroup(groupId: Long): Flowable<List<StreamEntity>> abstract fun getAllStreamsForGroup(groupId: Long): Flowable<List<StreamWithState>>
@Query(
"""
SELECT s.*, sst.progress_time, (sh.stream_id IS NOT NULL) AS is_stream_in_history
FROM streams s
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
INNER JOIN feed f
ON s.uid = f.stream_id
WHERE (
sh.stream_id IS NULL
OR s.stream_type = 'LIVE_STREAM'
OR s.stream_type = 'AUDIO_LIVE_STREAM'
)
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
"""
)
abstract fun getLiveOrNotPlayedStreams(): Flowable<List<StreamWithState>>
@Query(
"""
SELECT s.*, sst.progress_time, (sh.stream_id IS NOT NULL) AS is_stream_in_history
FROM streams s
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
INNER JOIN feed f
ON s.uid = f.stream_id
INNER JOIN feed_group_subscription_join fgs
ON fgs.subscription_id = f.subscription_id
WHERE fgs.group_id = :groupId
AND (
sh.stream_id IS NULL
OR s.stream_type = 'LIVE_STREAM'
OR s.stream_type = 'AUDIO_LIVE_STREAM'
)
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
"""
)
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Flowable<List<StreamWithState>>
@Query( @Query(
""" """

View file

@ -0,0 +1,17 @@
package org.schabi.newpipe.database.stream
import androidx.room.ColumnInfo
import androidx.room.Embedded
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamStateEntity
data class StreamWithState(
@Embedded
val stream: StreamEntity,
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_TIME)
val stateProgressTime: Long?,
@ColumnInfo(name = "is_stream_in_history")
val isInHistory: Boolean = false
)

View file

@ -12,6 +12,7 @@ import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.database.feed.model.FeedEntity import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType
@ -38,16 +39,19 @@ class FeedDatabaseManager(context: Context) {
fun database() = database fun database() = database
fun asStreamItems(groupId: Long = FeedGroupEntity.GROUP_ALL_ID): Flowable<List<StreamInfoItem>> { fun getStreams(
val streams = when (groupId) { groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
FeedGroupEntity.GROUP_ALL_ID -> feedTable.getAllStreams() getPlayedStreams: Boolean = true
else -> feedTable.getAllStreamsFromGroup(groupId) ): Flowable<List<StreamWithState>> {
return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> {
if (getPlayedStreams) feedTable.getAllStreams()
else feedTable.getLiveOrNotPlayedStreams()
}
else -> {
if (getPlayedStreams) feedTable.getAllStreamsForGroup(groupId)
else feedTable.getLiveOrNotPlayedStreamsForGroup(groupId)
} }
return streams.map {
val items = ArrayList<StreamInfoItem>(it.size)
it.mapTo(items) { stream -> stream.toStreamInfoItem() }
return@map items
} }
} }
@ -60,8 +64,10 @@ class FeedDatabaseManager(context: Context) {
} }
} }
fun outdatedSubscriptionsForGroup(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, outdatedThreshold: OffsetDateTime) = fun outdatedSubscriptionsForGroup(
feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold) groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
outdatedThreshold: OffsetDateTime
) = feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold)
fun markAsOutdated(subscriptionId: Long) = feedTable fun markAsOutdated(subscriptionId: Long) = feedTable
.setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, null)) .setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, null))
@ -93,10 +99,7 @@ class FeedDatabaseManager(context: Context) {
} }
feedTable.setLastUpdatedForSubscription( feedTable.setLastUpdatedForSubscription(
FeedLastUpdatedEntity( FeedLastUpdatedEntity(subscriptionId, OffsetDateTime.now(ZoneOffset.UTC))
subscriptionId,
OffsetDateTime.now(ZoneOffset.UTC)
)
) )
} }
@ -108,7 +111,12 @@ class FeedDatabaseManager(context: Context) {
fun clear() { fun clear() {
feedTable.deleteAll() feedTable.deleteAll()
val deletedOrphans = streamTable.deleteOrphans() val deletedOrphans = streamTable.deleteOrphans()
if (DEBUG) Log.d(this::class.java.simpleName, "clear() → streamTable.deleteOrphans() → $deletedOrphans") if (DEBUG) {
Log.d(
this::class.java.simpleName,
"clear() → streamTable.deleteOrphans() → $deletedOrphans"
)
}
} }
// ///////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////
@ -122,7 +130,8 @@ class FeedDatabaseManager(context: Context) {
} }
fun updateSubscriptionsForGroup(groupId: Long, subscriptionIds: List<Long>): Completable { fun updateSubscriptionsForGroup(groupId: Long, subscriptionIds: List<Long>): Completable {
return Completable.fromCallable { feedGroupTable.updateSubscriptionsForGroup(groupId, subscriptionIds) } return Completable
.fromCallable { feedGroupTable.updateSubscriptionsForGroup(groupId, subscriptionIds) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
} }

View file

@ -19,7 +19,10 @@
package org.schabi.newpipe.local.feed package org.schabi.newpipe.local.feed
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
@ -30,11 +33,18 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.Nullable import androidx.annotation.Nullable
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.GroupieViewHolder
import com.xwray.groupie.Item
import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.OnItemLongClickListener
import icepick.State import icepick.State
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
@ -49,33 +59,43 @@ import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.UserAction import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty
import org.schabi.newpipe.fragments.list.BaseListFragment import org.schabi.newpipe.fragments.BaseStateFragment
import org.schabi.newpipe.info_list.InfoItemDialog
import org.schabi.newpipe.ktx.animate import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
import org.schabi.newpipe.local.feed.item.StreamItem
import org.schabi.newpipe.local.feed.service.FeedLoadService import org.schabi.newpipe.local.feed.service.FeedLoadService
import org.schabi.newpipe.local.subscription.SubscriptionManager import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.player.helper.PlayerHolder
import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.StreamDialogEntry
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.ArrayList
import kotlin.math.floor
import kotlin.math.max
class FeedFragment : BaseListFragment<FeedState, Unit>() { class FeedFragment : BaseStateFragment<FeedState>() {
private var _feedBinding: FragmentFeedBinding? = null private var _feedBinding: FragmentFeedBinding? = null
private val feedBinding get() = _feedBinding!! private val feedBinding get() = _feedBinding!!
private val disposables = CompositeDisposable() private val disposables = CompositeDisposable()
private lateinit var viewModel: FeedViewModel private lateinit var viewModel: FeedViewModel
@State @State @JvmField var listState: Parcelable? = null
@JvmField
var listState: Parcelable? = null
private var groupId = FeedGroupEntity.GROUP_ALL_ID private var groupId = FeedGroupEntity.GROUP_ALL_ID
private var groupName = "" private var groupName = ""
private var oldestSubscriptionUpdate: OffsetDateTime? = null private var oldestSubscriptionUpdate: OffsetDateTime? = null
private lateinit var groupAdapter: GroupAdapter<GroupieViewHolder>
@State @JvmField var showPlayedItems: Boolean = true
init { init {
setHasOptionsMenu(true) setHasOptionsMenu(true)
setUseDefaultStateSaving(false)
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -95,8 +115,22 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
_feedBinding = FragmentFeedBinding.bind(rootView) _feedBinding = FragmentFeedBinding.bind(rootView)
super.onViewCreated(rootView, savedInstanceState) super.onViewCreated(rootView, savedInstanceState)
viewModel = ViewModelProvider(this, FeedViewModel.Factory(requireContext(), groupId)).get(FeedViewModel::class.java) val factory = FeedViewModel.Factory(requireContext(), groupId, showPlayedItems)
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) } viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) })
groupAdapter = GroupAdapter<GroupieViewHolder>().apply {
setOnItemClickListener(listenerStreamItem)
setOnItemLongClickListener(listenerStreamItem)
spanCount = if (shouldUseGridLayout()) getGridSpanCount() else 1
}
feedBinding.itemsList.apply {
layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
spanSizeLookup = groupAdapter.spanSizeLookup
}
adapter = groupAdapter
}
} }
override fun onPause() { override fun onPause() {
@ -129,13 +163,18 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
activity.supportActionBar?.setDisplayShowTitleEnabled(true)
activity.supportActionBar?.setTitle(R.string.fragment_feed_title) activity.supportActionBar?.setTitle(R.string.fragment_feed_title)
activity.supportActionBar?.subtitle = groupName activity.supportActionBar?.subtitle = groupName
inflater.inflate(R.menu.menu_feed_fragment, menu) inflater.inflate(R.menu.menu_feed_fragment, menu)
menu.findItem(R.id.menu_item_feed_toggle_played_items).apply {
updateTogglePlayedItemsButton(this)
if (useAsFrontPage) { if (useAsFrontPage) {
menu.findItem(R.id.menu_item_feed_help).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER) setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
} }
} }
@ -143,7 +182,8 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
if (item.itemId == R.id.menu_item_feed_help) { if (item.itemId == R.id.menu_item_feed_help) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val usingDedicatedMethod = sharedPreferences.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false) val usingDedicatedMethod = sharedPreferences
.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
val enableDisableButtonText = when { val enableDisableButtonText = when {
usingDedicatedMethod -> R.string.feed_use_dedicated_fetch_method_disable_button usingDedicatedMethod -> R.string.feed_use_dedicated_fetch_method_disable_button
else -> R.string.feed_use_dedicated_fetch_method_enable_button else -> R.string.feed_use_dedicated_fetch_method_enable_button
@ -160,6 +200,10 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
.create() .create()
.show() .show()
return true return true
} else if (item.itemId == R.id.menu_item_feed_toggle_played_items) {
showPlayedItems = !item.isChecked
updateTogglePlayedItemsButton(item)
viewModel.togglePlayedItems(showPlayedItems)
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
@ -177,13 +221,22 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
} }
override fun onDestroyView() { override fun onDestroyView() {
feedBinding.itemsList.adapter = null
_feedBinding = null _feedBinding = null
super.onDestroyView() super.onDestroyView()
} }
// ///////////////////////////////////////////////////////////////////////// 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
)
}
// //////////////////////////////////////////////////////////////////////////
// Handling // Handling
// ///////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////
override fun showLoading() { override fun showLoading() {
super.showLoading() super.showLoading()
@ -195,6 +248,7 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
override fun hideLoading() { override fun hideLoading() {
super.hideLoading() super.hideLoading()
feedBinding.itemsList.animate(true, 0)
feedBinding.refreshRootView.animate(true, 200) feedBinding.refreshRootView.animate(true, 200)
feedBinding.loadingProgressText.animate(false, 0) feedBinding.loadingProgressText.animate(false, 0)
feedBinding.swipeRefreshLayout.isRefreshing = false feedBinding.swipeRefreshLayout.isRefreshing = false
@ -220,7 +274,6 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
override fun handleError() { override fun handleError() {
super.handleError() super.handleError()
infoListAdapter.clearStreamItemList()
feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling() feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling()
feedBinding.refreshRootView.animate(false, 0) feedBinding.refreshRootView.animate(false, 0)
feedBinding.loadingProgressText.animate(false, 0) feedBinding.loadingProgressText.animate(false, 0)
@ -248,8 +301,71 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
feedBinding.loadingProgressBar.max = progressState.maxProgress feedBinding.loadingProgressBar.max = progressState.maxProgress
} }
private fun showStreamDialog(item: StreamInfoItem) {
val context = context
val activity: Activity? = getActivity()
if (context == null || context.resources == null || activity == null) return
val entries = ArrayList<StreamDialogEntry>()
if (PlayerHolder.getType() != null) {
entries.add(StreamDialogEntry.enqueue)
}
if (item.streamType == StreamType.AUDIO_STREAM) {
entries.addAll(
listOf(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share
)
)
} else {
entries.addAll(
listOf(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.start_here_on_popup,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share
)
)
}
InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which ->
StreamDialogEntry.clickOn(which, this, item)
}.show()
}
private val listenerStreamItem = object : OnItemClickListener, OnItemLongClickListener {
override fun onItemClick(item: Item<*>, view: View) {
if (item is StreamItem) {
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 {
if (item is StreamItem) {
showStreamDialog(item.streamWithState.stream.toStreamInfoItem())
return true
}
return false
}
}
@SuppressLint("StringFormatMatches")
private fun handleLoadedState(loadedState: FeedState.LoadedState) { private fun handleLoadedState(loadedState: FeedState.LoadedState) {
infoListAdapter.setInfoItemList(loadedState.items)
val itemVersion = if (shouldUseGridLayout()) {
StreamItem.ItemVersion.GRID
} else {
StreamItem.ItemVersion.NORMAL
}
loadedState.items.forEach { it.itemVersion = itemVersion }
groupAdapter.updateAsync(loadedState.items, false, null)
listState?.run { listState?.run {
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState) feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
listState = null listState = null
@ -357,7 +473,10 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
private fun updateRelativeTimeViews() { private fun updateRelativeTimeViews() {
updateRefreshViewState() updateRefreshViewState()
infoListAdapter.notifyDataSetChanged() groupAdapter.notifyItemRangeChanged(
0, groupAdapter.itemCount,
StreamItem.UPDATE_RELATIVE_TIME
)
} }
private fun updateRefreshViewState() { private fun updateRefreshViewState() {
@ -372,8 +491,6 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
// ///////////////////////////////////////////////////////////////////////// // /////////////////////////////////////////////////////////////////////////
override fun doInitialLoadLogic() {} override fun doInitialLoadLogic() {}
override fun loadMoreItems() {}
override fun hasMoreItems() = false
override fun reloadContent() { override fun reloadContent() {
getActivity()?.startService( getActivity()?.startService(
@ -384,6 +501,35 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
listState = null listState = null
} }
// /////////////////////////////////////////////////////////////////////////
// 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.video_item_grid_thumbnail_image_width)
return max(1, floor(resources.displayMetrics.widthPixels / minWidth.toDouble()).toInt())
}
companion object { companion object {
const val KEY_GROUP_ID = "ARG_GROUP_ID" const val KEY_GROUP_ID = "ARG_GROUP_ID"
const val KEY_GROUP_NAME = "ARG_GROUP_NAME" const val KEY_GROUP_NAME = "ARG_GROUP_NAME"

View file

@ -1,7 +1,7 @@
package org.schabi.newpipe.local.feed package org.schabi.newpipe.local.feed
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.local.feed.item.StreamItem
import java.time.OffsetDateTime import java.time.OffsetDateTime
sealed class FeedState { sealed class FeedState {
@ -12,7 +12,7 @@ sealed class FeedState {
) : FeedState() ) : FeedState()
data class LoadedState( data class LoadedState(
val items: List<StreamInfoItem>, val items: List<StreamItem>,
val oldestUpdate: OffsetDateTime? = null, val oldestUpdate: OffsetDateTime? = null,
val notLoadedCount: Long, val notLoadedCount: Long,
val itemsErrors: List<Throwable> = emptyList() val itemsErrors: List<Throwable> = emptyList()

View file

@ -8,9 +8,11 @@ import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.functions.Function4 import io.reactivex.rxjava3.functions.Function4
import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.local.feed.item.StreamItem
import org.schabi.newpipe.local.feed.service.FeedEventManager import org.schabi.newpipe.local.feed.service.FeedEventManager
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
@ -20,26 +22,33 @@ import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModel() { class FeedViewModel(
class Factory(val context: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModelProvider.Factory { applicationContext: Context,
@Suppress("UNCHECKED_CAST") groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
override fun <T : ViewModel?> create(modelClass: Class<T>): T { initialShowPlayedItems: Boolean = true
return FeedViewModel(context.applicationContext, groupId) as T ) : ViewModel() {
}
}
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext) private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>()
private val streamItems = toggleShowPlayedItems
.startWithItem(initialShowPlayedItems)
.distinctUntilChanged()
.switchMap { showPlayedItems ->
feedDatabaseManager.getStreams(groupId, showPlayedItems)
}
private val mutableStateLiveData = MutableLiveData<FeedState>() private val mutableStateLiveData = MutableLiveData<FeedState>()
val stateLiveData: LiveData<FeedState> = mutableStateLiveData val stateLiveData: LiveData<FeedState> = mutableStateLiveData
private var combineDisposable = Flowable private var combineDisposable = Flowable
.combineLatest( .combineLatest(
FeedEventManager.events(), FeedEventManager.events(),
feedDatabaseManager.asStreamItems(groupId), streamItems,
feedDatabaseManager.notLoadedCount(groupId), feedDatabaseManager.notLoadedCount(groupId),
feedDatabaseManager.oldestSubscriptionUpdate(groupId), feedDatabaseManager.oldestSubscriptionUpdate(groupId),
Function4 { t1: FeedEventManager.Event, t2: List<StreamInfoItem>, t3: Long, t4: List<OffsetDateTime> ->
Function4 { t1: FeedEventManager.Event, t2: List<StreamWithState>,
t3: Long, t4: List<OffsetDateTime> ->
return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull()) return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull())
} }
) )
@ -49,9 +58,9 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEn
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) -> .subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
mutableStateLiveData.postValue( mutableStateLiveData.postValue(
when (event) { when (event) {
is IdleEvent -> FeedState.LoadedState(listFromDB, oldestUpdate, notLoadedCount) is IdleEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount)
is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage) is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage)
is SuccessResultEvent -> FeedState.LoadedState(listFromDB, oldestUpdate, notLoadedCount, event.itemsErrors) is SuccessResultEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount, event.itemsErrors)
is ErrorResultEvent -> FeedState.ErrorState(event.error) is ErrorResultEvent -> FeedState.ErrorState(event.error)
} }
) )
@ -66,5 +75,20 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEn
combineDisposable.dispose() combineDisposable.dispose()
} }
private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List<StreamInfoItem>, val t3: Long, val t4: OffsetDateTime?) private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List<StreamWithState>, val t3: Long, val t4: OffsetDateTime?)
fun togglePlayedItems(showPlayedItems: Boolean) {
toggleShowPlayedItems.onNext(showPlayedItems)
}
class Factory(
private val context: Context,
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
private val showPlayedItems: Boolean
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return FeedViewModel(context.applicationContext, groupId, showPlayedItems) as T
}
}
} }

View file

@ -0,0 +1,157 @@
package org.schabi.newpipe.local.feed.item
import android.content.Context
import android.text.TextUtils
import android.view.View
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import com.nostra13.universalimageloader.core.ImageLoader
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.databinding.ListStreamItemBinding
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
import org.schabi.newpipe.util.ImageDisplayConstants
import org.schabi.newpipe.util.Localization
import java.util.concurrent.TimeUnit
data class StreamItem(
val streamWithState: StreamWithState,
var itemVersion: ItemVersion = ItemVersion.NORMAL
) : BindableItem<ListStreamItemBinding>() {
companion object {
const val UPDATE_RELATIVE_TIME = 1
}
private val stream: StreamEntity = streamWithState.stream
private val stateProgressTime: Long? = streamWithState.stateProgressTime
private val isInHistory: Boolean = streamWithState.isInHistory
override fun getId(): Long = stream.uid
enum class ItemVersion { NORMAL, MINI, GRID }
override fun getLayout(): Int = when (itemVersion) {
ItemVersion.NORMAL -> R.layout.list_stream_item
ItemVersion.MINI -> R.layout.list_stream_mini_item
ItemVersion.GRID -> R.layout.list_stream_grid_item
}
override fun initializeViewBinding(view: View) = ListStreamItemBinding.bind(view)
override fun bind(viewBinding: ListStreamItemBinding, position: Int, payloads: MutableList<Any>) {
if (payloads.contains(UPDATE_RELATIVE_TIME)) {
if (itemVersion != ItemVersion.MINI) {
viewBinding.itemAdditionalDetails.text =
getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context)
}
return
}
super.bind(viewBinding, position, payloads)
}
override fun bind(viewBinding: ListStreamItemBinding, position: Int) {
viewBinding.itemVideoTitleView.text = stream.title
viewBinding.itemUploaderView.text = stream.uploader
val isLiveStream = stream.streamType == LIVE_STREAM || stream.streamType == AUDIO_LIVE_STREAM
if (stream.duration > 0) {
viewBinding.itemDurationView.text = Localization.getDurationString(stream.duration)
viewBinding.itemDurationView.setBackgroundColor(
ContextCompat.getColor(
viewBinding.itemDurationView.context,
R.color.duration_background_color
)
)
viewBinding.itemDurationView.visibility = View.VISIBLE
if (stateProgressTime != null) {
viewBinding.itemProgressView.visibility = View.VISIBLE
viewBinding.itemProgressView.max = stream.duration.toInt()
viewBinding.itemProgressView.progress = TimeUnit.MILLISECONDS.toSeconds(stateProgressTime).toInt()
} else {
viewBinding.itemProgressView.visibility = View.GONE
}
} else if (isLiveStream) {
viewBinding.itemDurationView.setText(R.string.duration_live)
viewBinding.itemDurationView.setBackgroundColor(
ContextCompat.getColor(
viewBinding.itemDurationView.context,
R.color.live_duration_background_color
)
)
viewBinding.itemDurationView.visibility = View.VISIBLE
viewBinding.itemProgressView.visibility = View.GONE
} else {
viewBinding.itemDurationView.visibility = View.GONE
viewBinding.itemProgressView.visibility = View.GONE
}
viewBinding.itemInHistoryIndicatorView.visibility =
if (isInHistory && !isLiveStream) View.VISIBLE else View.GONE
ImageLoader.getInstance().displayImage(
stream.thumbnailUrl, viewBinding.itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
)
if (itemVersion != ItemVersion.MINI) {
viewBinding.itemAdditionalDetails.text =
getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context)
}
}
override fun isLongClickable() = when (stream.streamType) {
AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM -> true
else -> false
}
private fun getStreamInfoDetailLine(context: Context): String {
var viewsAndDate = ""
val viewCount = stream.viewCount
if (viewCount != null && viewCount >= 0) {
viewsAndDate = when (stream.streamType) {
AUDIO_LIVE_STREAM -> Localization.listeningCount(context, viewCount)
LIVE_STREAM -> Localization.shortWatchingCount(context, viewCount)
else -> Localization.shortViewCount(context, viewCount)
}
}
val uploadDate = getFormattedRelativeUploadDate(context)
return when {
!TextUtils.isEmpty(uploadDate) -> when {
viewsAndDate.isEmpty() -> uploadDate!!
else -> Localization.concatenateStrings(viewsAndDate, uploadDate)
}
else -> viewsAndDate
}
}
private fun getFormattedRelativeUploadDate(context: Context): String? {
val uploadDate = stream.uploadDate
return if (uploadDate != null) {
var formattedRelativeTime = Localization.relativeTime(uploadDate)
if (MainActivity.DEBUG) {
val key = context.getString(R.string.show_original_time_ago_key)
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(key, false)) {
formattedRelativeTime += " (" + stream.textualUploadDate + ")"
}
}
formattedRelativeTime
} else {
stream.textualUploadDate
}
}
override fun getSpanSize(spanCount: Int, position: Int): Int {
return if (itemVersion == ItemVersion.GRID) 1 else spanCount
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
</vector>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:angle="-90"
android:endColor="#B2000000"
android:startColor="#E6000000"/>
</shape>

View file

@ -20,6 +20,32 @@
android:src="@drawable/dummy_thumbnail" android:src="@drawable/dummy_thumbnail"
tools:ignore="RtlHardcoded" /> tools:ignore="RtlHardcoded" />
<TextView
android:id="@+id/itemInHistoryIndicatorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignStart="@id/itemThumbnailView"
android:layout_alignTop="@id/itemThumbnailView"
android:layout_alignEnd="@id/itemThumbnailView"
android:layout_alignBottom="@id/itemThumbnailView"
android:layout_gravity="top|left"
android:background="@drawable/item_in_history_indicator_background"
android:ellipsize="end"
android:maxLines="1"
android:paddingLeft="@dimen/item_in_history_indicator_horizontal_margin"
android:paddingTop="@dimen/item_in_history_indicator_vertical_margin"
android:paddingRight="@dimen/item_in_history_indicator_horizontal_margin"
android:paddingBottom="@dimen/item_in_history_indicator_vertical_margin"
android:text="@string/item_in_history"
android:textAllCaps="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/item_in_history_indicator_text_color"
android:textSize="@dimen/item_in_history_indicator_text_size"
android:textStyle="bold"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/itemDurationView" android:id="@+id/itemDurationView"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -25,6 +25,32 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
tools:ignore="RtlHardcoded" /> tools:ignore="RtlHardcoded" />
<TextView
android:id="@+id/itemInHistoryIndicatorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignStart="@id/itemThumbnailView"
android:layout_alignTop="@id/itemThumbnailView"
android:layout_alignEnd="@id/itemThumbnailView"
android:layout_alignBottom="@id/itemThumbnailView"
android:layout_gravity="top|left"
android:background="@drawable/item_in_history_indicator_background"
android:ellipsize="end"
android:maxLines="1"
android:paddingLeft="@dimen/item_in_history_indicator_horizontal_margin"
android:paddingTop="@dimen/item_in_history_indicator_vertical_margin"
android:paddingRight="@dimen/item_in_history_indicator_horizontal_margin"
android:paddingBottom="@dimen/item_in_history_indicator_vertical_margin"
android:text="@string/item_in_history"
android:textAllCaps="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/item_in_history_indicator_text_color"
android:textSize="@dimen/item_in_history_indicator_text_size"
android:textStyle="bold"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/itemDurationView" android:id="@+id/itemDurationView"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -22,6 +22,32 @@
android:src="@drawable/dummy_thumbnail" android:src="@drawable/dummy_thumbnail"
tools:ignore="RtlHardcoded" /> tools:ignore="RtlHardcoded" />
<TextView
android:id="@+id/itemInHistoryIndicatorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignStart="@id/itemThumbnailView"
android:layout_alignTop="@id/itemThumbnailView"
android:layout_alignEnd="@id/itemThumbnailView"
android:layout_alignBottom="@id/itemThumbnailView"
android:layout_gravity="top|left"
android:background="@drawable/item_in_history_indicator_background"
android:ellipsize="end"
android:maxLines="1"
android:paddingLeft="@dimen/item_in_history_indicator_horizontal_margin"
android:paddingTop="@dimen/item_in_history_indicator_vertical_margin"
android:paddingRight="@dimen/item_in_history_indicator_horizontal_margin"
android:paddingBottom="@dimen/item_in_history_indicator_vertical_margin"
android:text="@string/item_in_history"
android:textAllCaps="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/item_in_history_indicator_text_color"
android:textSize="@dimen/item_in_history_indicator_text_size"
android:textStyle="bold"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/itemDurationView" android:id="@+id/itemDurationView"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -22,6 +22,32 @@
android:src="@drawable/dummy_thumbnail" android:src="@drawable/dummy_thumbnail"
tools:ignore="RtlHardcoded" /> tools:ignore="RtlHardcoded" />
<TextView
android:id="@+id/itemInHistoryIndicatorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignStart="@id/itemThumbnailView"
android:layout_alignTop="@id/itemThumbnailView"
android:layout_alignEnd="@id/itemThumbnailView"
android:layout_alignBottom="@id/itemThumbnailView"
android:layout_gravity="top|left"
android:background="@drawable/item_in_history_indicator_background"
android:ellipsize="end"
android:maxLines="1"
android:paddingLeft="@dimen/item_in_history_indicator_horizontal_margin"
android:paddingTop="@dimen/item_in_history_indicator_vertical_margin"
android:paddingRight="@dimen/item_in_history_indicator_horizontal_margin"
android:paddingBottom="@dimen/item_in_history_indicator_vertical_margin"
android:text="@string/item_in_history"
android:textAllCaps="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/item_in_history_indicator_text_color"
android:textSize="@dimen/item_in_history_indicator_text_size"
android:textStyle="bold"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/itemDurationView" android:id="@+id/itemDurationView"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -23,6 +23,32 @@
android:src="@drawable/dummy_thumbnail" android:src="@drawable/dummy_thumbnail"
tools:ignore="RtlHardcoded" /> tools:ignore="RtlHardcoded" />
<TextView
android:id="@+id/itemInHistoryIndicatorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignStart="@id/itemThumbnailView"
android:layout_alignTop="@id/itemThumbnailView"
android:layout_alignEnd="@id/itemThumbnailView"
android:layout_alignBottom="@id/itemThumbnailView"
android:layout_gravity="top|left"
android:background="@drawable/item_in_history_indicator_background"
android:ellipsize="end"
android:maxLines="1"
android:paddingLeft="@dimen/item_in_history_indicator_horizontal_margin"
android:paddingTop="@dimen/item_in_history_indicator_vertical_margin"
android:paddingRight="@dimen/item_in_history_indicator_horizontal_margin"
android:paddingBottom="@dimen/item_in_history_indicator_vertical_margin"
android:text="@string/item_in_history"
android:textAllCaps="true"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/item_in_history_indicator_text_color"
android:textSize="@dimen/item_in_history_indicator_text_size"
android:textStyle="bold"
android:visibility="gone"
tools:ignore="RtlHardcoded"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/itemDurationView" android:id="@+id/itemDurationView"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -2,9 +2,17 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_item_feed_toggle_played_items"
android:checkable="true"
android:checked="true"
android:icon="@drawable/ic_visibility_on"
android:title="@string/feed_toggle_show_played_items"
app:showAsAction="ifRoom" />
<item <item
android:id="@+id/menu_item_feed_help" android:id="@+id/menu_item_feed_help"
android:icon="@drawable/ic_help" android:icon="@drawable/ic_help"
android:title="@string/help" android:title="@string/help"
app:showAsAction="always" /> app:showAsAction="never" />
</menu> </menu>

View file

@ -56,6 +56,7 @@
<color name="duration_text_color">#EEFFFFFF</color> <color name="duration_text_color">#EEFFFFFF</color>
<color name="playlist_stream_count_text_color">#ffffff</color> <color name="playlist_stream_count_text_color">#ffffff</color>
<color name="video_overlay_color">#64000000</color> <color name="video_overlay_color">#64000000</color>
<color name="item_in_history_indicator_text_color">#E6FFFFFF</color>
<color name="background_notification_color">#323232</color> <color name="background_notification_color">#323232</color>
<color name="background_title_color">#ffffff</color> <color name="background_title_color">#ffffff</color>

View file

@ -20,6 +20,7 @@
<dimen name="video_item_search_uploader_text_size">11sp</dimen> <dimen name="video_item_search_uploader_text_size">11sp</dimen>
<dimen name="video_item_search_upload_date_text_size">12sp</dimen> <dimen name="video_item_search_upload_date_text_size">12sp</dimen>
<dimen name="header_footer_text_size">16sp</dimen> <dimen name="header_footer_text_size">16sp</dimen>
<dimen name="item_in_history_indicator_text_size">12sp</dimen>
<!-- Elements Size --> <!-- Elements Size -->
<!-- 16 / 9 ratio--> <!-- 16 / 9 ratio-->
<dimen name="video_item_search_thumbnail_image_width">124dp</dimen> <dimen name="video_item_search_thumbnail_image_width">124dp</dimen>
@ -52,6 +53,8 @@
<dimen name="player_main_buttons_min_width">40dp</dimen> <dimen name="player_main_buttons_min_width">40dp</dimen>
<dimen name="player_notification_thumbnail_width">200dp</dimen> <dimen name="player_notification_thumbnail_width">200dp</dimen>
<dimen name="item_in_history_indicator_vertical_margin">2dp</dimen>
<dimen name="item_in_history_indicator_horizontal_margin">8dp</dimen>
<!-- Miscellaneous --> <!-- Miscellaneous -->
<dimen name="popup_default_width">180dp</dimen> <dimen name="popup_default_width">180dp</dimen>
<dimen name="popup_minimum_width">150dp</dimen> <dimen name="popup_minimum_width">150dp</dimen>

View file

@ -228,6 +228,7 @@
<string name="delete_search_history_alert">Delete entire search history?</string> <string name="delete_search_history_alert">Delete entire search history?</string>
<string name="search_history_deleted">Search history deleted.</string> <string name="search_history_deleted">Search history deleted.</string>
<string name="help">Help</string> <string name="help">Help</string>
<string name="item_in_history">In History</string>
<!-- error strings --> <!-- error strings -->
<string name="general_error">Error</string> <string name="general_error">Error</string>
<string name="download_to_sdcard_error_title">External storage unavailable</string> <string name="download_to_sdcard_error_title">External storage unavailable</string>
@ -702,7 +703,7 @@
<string name="feed_use_dedicated_fetch_method_enable_button">Enable fast mode</string> <string name="feed_use_dedicated_fetch_method_enable_button">Enable fast mode</string>
<string name="feed_use_dedicated_fetch_method_disable_button">Disable fast mode</string> <string name="feed_use_dedicated_fetch_method_disable_button">Disable fast mode</string>
<string name="feed_use_dedicated_fetch_method_help_text">Do you think feed loading is too slow? If so, try enabling fast loading (you can change it in settings or by pressing the button below).\n\nNewPipe offers two feed loading strategies:\n• Fetching the whole subscription channel, which is slow but complete.\n• Using a dedicated service endpoint, which is fast but usually not complete.\n\nThe difference between the two is that the fast one usually lacks some information, like the item\'s duration or type (can\'t distinguish between live videos and normal ones) and it may return less items.\n\nYouTube is an example of a service that offers this fast method with its RSS feed.\n\nSo the choice boils down to what you prefer: speed or precise information.</string> <string name="feed_use_dedicated_fetch_method_help_text">Do you think feed loading is too slow? If so, try enabling fast loading (you can change it in settings or by pressing the button below).\n\nNewPipe offers two feed loading strategies:\n• Fetching the whole subscription channel, which is slow but complete.\n• Using a dedicated service endpoint, which is fast but usually not complete.\n\nThe difference between the two is that the fast one usually lacks some information, like the item\'s duration or type (can\'t distinguish between live videos and normal ones) and it may return less items.\n\nYouTube is an example of a service that offers this fast method with its RSS feed.\n\nSo the choice boils down to what you prefer: speed or precise information.</string>
<string name="feed_toggle_show_played_items">Show played items</string>
<string name="content_not_supported">This content is not yet supported by NewPipe.\n\nIt will hopefully be supported in a future version.</string> <string name="content_not_supported">This content is not yet supported by NewPipe.\n\nIt will hopefully be supported in a future version.</string>
<string name="detail_sub_channel_thumbnail_view_description">Channel\'s avatar thumbnail</string> <string name="detail_sub_channel_thumbnail_view_description">Channel\'s avatar thumbnail</string>
<string name="channel_created_by">Created by %s</string> <string name="channel_created_by">Created by %s</string>