feat: add 'Continue watching' section to home tab

This commit is contained in:
Bnyro 2023-07-19 09:26:24 +02:00
parent e75bbc868e
commit 110d29c50a
8 changed files with 95 additions and 24 deletions

View File

@ -1,5 +1,6 @@
package com.github.libretube.db package com.github.libretube.db
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.api.obj.Streams import com.github.libretube.api.obj.Streams
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.db.DatabaseHolder.Database import com.github.libretube.db.DatabaseHolder.Database
@ -50,4 +51,17 @@ object DatabaseHelper {
searchHistory.removeFirst() searchHistory.removeFirst()
} }
} }
suspend fun filterUnwatched(streams: List<StreamItem>): List<StreamItem> {
return streams.filter {
withContext(Dispatchers.IO) {
val historyItem = Database.watchPositionDao()
.findById(it.url.orEmpty().toID()) ?: return@withContext true
val progress = historyItem.position / 1000
val duration = it.duration ?: 0
// show video only in feed when watched less than 90%
progress < 0.9f * duration
}
}
}
} }

View File

@ -3,6 +3,8 @@ package com.github.libretube.db.obj
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.extensions.toMillis
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -17,4 +19,16 @@ data class WatchHistoryItem(
@ColumnInfo var uploaderAvatar: String? = null, @ColumnInfo var uploaderAvatar: String? = null,
@ColumnInfo var thumbnailUrl: String? = null, @ColumnInfo var thumbnailUrl: String? = null,
@ColumnInfo val duration: Long? = null @ColumnInfo val duration: Long? = null
) ) {
fun toStreamItem() = StreamItem(
url = videoId,
type = "stream",
title = title,
thumbnail = thumbnailUrl,
uploaderName = uploader,
uploaded = uploadDate?.toMillis(),
uploaderAvatar = uploaderAvatar,
uploaderUrl = uploaderUrl,
duration = duration
)
}

View File

@ -0,0 +1,7 @@
package com.github.libretube.extensions
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
fun LocalDate.toMillis() = this.atStartOfDayIn(TimeZone.UTC).toEpochMilliseconds()

View File

@ -21,8 +21,10 @@ import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.SubscriptionHelper import com.github.libretube.api.SubscriptionHelper
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.FragmentHomeBinding import com.github.libretube.databinding.FragmentHomeBinding
import com.github.libretube.db.DatabaseHelper
import com.github.libretube.db.DatabaseHolder import com.github.libretube.db.DatabaseHolder
import com.github.libretube.helpers.LocaleHelper import com.github.libretube.helpers.LocaleHelper
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.ui.adapters.PlaylistBookmarkAdapter import com.github.libretube.ui.adapters.PlaylistBookmarkAdapter
import com.github.libretube.ui.adapters.PlaylistsAdapter import com.github.libretube.ui.adapters.PlaylistsAdapter
@ -56,6 +58,10 @@ class HomeFragment : Fragment() {
findNavController().navigate(R.id.subscriptionsFragment) findNavController().navigate(R.id.subscriptionsFragment)
} }
binding.watchingTV.setOnClickListener {
findNavController().navigate(R.id.watchHistoryFragment)
}
binding.trendingTV.setOnClickListener { binding.trendingTV.setOnClickListener {
findNavController().navigate(R.id.trendsFragment) findNavController().navigate(R.id.trendsFragment)
} }
@ -90,6 +96,7 @@ class HomeFragment : Fragment() {
.getStringSet(PreferenceKeys.HOME_TAB_CONTENT, defaultItems.toSet()) .getStringSet(PreferenceKeys.HOME_TAB_CONTENT, defaultItems.toSet())
awaitAll( awaitAll(
async { if (visibleItems.contains(TRENDING)) loadTrending() }, async { if (visibleItems.contains(TRENDING)) loadTrending() },
async { if (visibleItems.contains(WATCHING)) loadVideosToContinueWatching() },
async { if (visibleItems.contains(BOOKMARKS)) loadBookmarks() }, async { if (visibleItems.contains(BOOKMARKS)) loadBookmarks() },
async { if (visibleItems.contains(FEATURED)) loadFeed() }, async { if (visibleItems.contains(FEATURED)) loadFeed() },
async { if (visibleItems.contains(PLAYLISTS)) loadPlaylists() } async { if (visibleItems.contains(PLAYLISTS)) loadPlaylists() }
@ -200,6 +207,30 @@ class HomeFragment : Fragment() {
}) })
} }
private suspend fun loadVideosToContinueWatching() {
if (!PlayerHelper.watchHistoryEnabled) return
val videos = withContext(Dispatchers.IO) {
DatabaseHolder.Database.watchHistoryDao().getAll()
}
val unwatchedVideos = DatabaseHelper.filterUnwatched(videos.map { it.toStreamItem() })
.reversed()
.take(20)
if (unwatchedVideos.isEmpty()) return
val binding = _binding ?: return
makeVisible(binding.watchingRV, binding.watchingTV)
binding.watchingRV.layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.watchingRV.adapter = VideosAdapter(
unwatchedVideos.toMutableList(),
forceMode = VideosAdapter.Companion.ForceMode.HOME
)
}
private fun makeVisible(vararg views: View) { private fun makeVisible(vararg views: View) {
views.forEach { views.forEach {
it.isVisible = true it.isVisible = true
@ -213,6 +244,7 @@ class HomeFragment : Fragment() {
companion object { companion object {
// The values of the preference entries for the home tab content // The values of the preference entries for the home tab content
private const val FEATURED = "featured" private const val FEATURED = "featured"
private const val WATCHING = "watching"
private const val TRENDING = "trending" private const val TRENDING = "trending"
private const val BOOKMARKS = "bookmarks" private const val BOOKMARKS = "bookmarks"
private const val PLAYLISTS = "playlists" private const val PLAYLISTS = "playlists"

View File

@ -20,6 +20,7 @@ import com.github.libretube.R
import com.github.libretube.api.obj.StreamItem import com.github.libretube.api.obj.StreamItem
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.FragmentSubscriptionsBinding import com.github.libretube.databinding.FragmentSubscriptionsBinding
import com.github.libretube.db.DatabaseHelper
import com.github.libretube.db.DatabaseHolder import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.obj.SubscriptionGroup import com.github.libretube.db.obj.SubscriptionGroup
import com.github.libretube.extensions.dpToPx import com.github.libretube.extensions.dpToPx
@ -33,7 +34,6 @@ import com.github.libretube.ui.models.SubscriptionsViewModel
import com.github.libretube.ui.sheets.BaseBottomSheet import com.github.libretube.ui.sheets.BaseBottomSheet
import com.github.libretube.ui.sheets.ChannelGroupsSheet import com.github.libretube.ui.sheets.ChannelGroupsSheet
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -243,15 +243,16 @@ class SubscriptionsFragment : Fragment() {
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
} }
}.let { streams -> }.let { streams ->
runBlocking {
if (!PreferenceHelper.getBoolean( if (!PreferenceHelper.getBoolean(
PreferenceKeys.HIDE_WATCHED_FROM_FEED, PreferenceKeys.HIDE_WATCHED_FROM_FEED,
false false
) )
) { ) {
streams streams
} else { } else {
removeWatchVideosFromFeed(streams) runBlocking {
DatabaseHelper.filterUnwatched(streams)
} }
} }
} }
@ -294,19 +295,6 @@ class SubscriptionsFragment : Fragment() {
PreferenceHelper.updateLastFeedWatchedTime() PreferenceHelper.updateLastFeedWatchedTime()
} }
private fun removeWatchVideosFromFeed(streams: List<StreamItem>): List<StreamItem> {
return streams.filter {
runBlocking(Dispatchers.IO) {
val historyItem = DatabaseHolder.Database.watchPositionDao()
.findById(it.url.orEmpty().toID()) ?: return@runBlocking true
val progress = historyItem.position / 1000
val duration = it.duration ?: 0
// show video only in feed when watched less than 1/4
progress < 0.9f * duration
}
}
}
private fun showSubscriptions() { private fun showSubscriptions() {
if (viewModel.subscriptions.value == null) return if (viewModel.subscriptions.value == null) return

View File

@ -39,6 +39,19 @@
android:nestedScrollingEnabled="false" android:nestedScrollingEnabled="false"
android:visibility="gone" /> android:visibility="gone" />
<TextView
android:id="@+id/watchingTV"
style="@style/HomeCategoryTitle"
android:text="@string/continue_watching" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/watchingRV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:nestedScrollingEnabled="false"
android:visibility="gone" />
<TextView <TextView
android:id="@+id/trendingTV" android:id="@+id/trendingTV"
style="@style/HomeCategoryTitle" style="@style/HomeCategoryTitle"

View File

@ -427,6 +427,7 @@
<string-array name="homeTabItems"> <string-array name="homeTabItems">
<item>@string/featured</item> <item>@string/featured</item>
<item>@string/continue_watching</item>
<item>@string/trending</item> <item>@string/trending</item>
<item>@string/bookmarks</item> <item>@string/bookmarks</item>
<item>@string/playlists</item> <item>@string/playlists</item>
@ -434,6 +435,7 @@
<string-array name="homeTabItemsValues"> <string-array name="homeTabItemsValues">
<item>featured</item> <item>featured</item>
<item>watching</item>
<item>trending</item> <item>trending</item>
<item>bookmarks</item> <item>bookmarks</item>
<item>playlists</item> <item>playlists</item>

View File

@ -452,6 +452,7 @@
<string name="descriptive_audio_track">descriptive</string> <string name="descriptive_audio_track">descriptive</string>
<string name="default_or_unknown_audio_track">default or unknown</string> <string name="default_or_unknown_audio_track">default or unknown</string>
<string name="unknown_or_no_audio">unknown or no audio</string> <string name="unknown_or_no_audio">unknown or no audio</string>
<string name="continue_watching">Continue watching</string>
<!-- Notification channel strings --> <!-- Notification channel strings -->
<string name="download_channel_name">Download Service</string> <string name="download_channel_name">Download Service</string>