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:
parent
56cd84c1fe
commit
e846f69e38
21 changed files with 668 additions and 63 deletions
|
@ -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(
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
9
app/src/main/res/drawable-night/ic_visibility_off.xml
Normal file
9
app/src/main/res/drawable-night/ic_visibility_off.xml
Normal 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>
|
9
app/src/main/res/drawable-night/ic_visibility_on.xml
Normal file
9
app/src/main/res/drawable-night/ic_visibility_on.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_visibility_off.xml
Normal file
9
app/src/main/res/drawable/ic_visibility_off.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_visibility_on.xml
Normal file
9
app/src/main/res/drawable/ic_visibility_on.xml
Normal 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>
|
|
@ -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>
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue