Merge pull request #1684 from Bnyro/master

Support channel tabs
This commit is contained in:
Bnyro 2022-10-29 12:17:22 +02:00 committed by GitHub
commit b1fa827a3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 171 additions and 15 deletions

View File

@ -1,6 +1,7 @@
package com.github.libretube.api package com.github.libretube.api
import com.github.libretube.api.obj.Channel import com.github.libretube.api.obj.Channel
import com.github.libretube.api.obj.ChannelTabResponse
import com.github.libretube.api.obj.CommentsPage import com.github.libretube.api.obj.CommentsPage
import com.github.libretube.api.obj.DeleteUserRequest import com.github.libretube.api.obj.DeleteUserRequest
import com.github.libretube.api.obj.Login import com.github.libretube.api.obj.Login
@ -63,6 +64,12 @@ interface PipedApi {
@GET("channel/{channelId}") @GET("channel/{channelId}")
suspend fun getChannel(@Path("channelId") channelId: String): Channel suspend fun getChannel(@Path("channelId") channelId: String): Channel
@GET("channels/tabs")
suspend fun getChannelTab(
@Query("data") data: String,
@Query("nextpage") nextPage: String? = null
): ChannelTabResponse
@GET("user/{name}") @GET("user/{name}")
suspend fun getChannelByName(@Path("name") channelName: String): Channel suspend fun getChannelByName(@Path("name") channelName: String): Channel

View File

@ -12,5 +12,6 @@ data class Channel(
var nextpage: String? = null, var nextpage: String? = null,
var subscriberCount: Long = 0, var subscriberCount: Long = 0,
var verified: Boolean = false, var verified: Boolean = false,
var relatedStreams: List<StreamItem>? = null var relatedStreams: List<StreamItem>? = listOf(),
var tabs: List<ChannelTab>? = listOf()
) )

View File

@ -0,0 +1,9 @@
package com.github.libretube.api.obj
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class ChannelTab(
val name: String? = null,
val data: String? = null
)

View File

@ -0,0 +1,6 @@
package com.github.libretube.api.obj
data class ChannelTabResponse(
val content: List<ContentItem> = listOf(),
val nextpage: String? = null
)

View File

@ -3,7 +3,7 @@ package com.github.libretube.api.obj
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class SearchItem( data class ContentItem(
var url: String? = null, var url: String? = null,
var thumbnail: String? = null, var thumbnail: String? = null,
var uploaderName: String? = null, var uploaderName: String? = null,

View File

@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class SearchResult( data class SearchResult(
val items: MutableList<SearchItem>? = arrayListOf(), val items: MutableList<ContentItem>? = arrayListOf(),
val nextpage: String? = "", val nextpage: String? = "",
val suggestion: String? = "", val suggestion: String? = "",
val corrected: Boolean? = null val corrected: Boolean? = null

View File

@ -8,7 +8,7 @@ import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.SubscriptionHelper import com.github.libretube.api.SubscriptionHelper
import com.github.libretube.api.obj.SearchItem import com.github.libretube.api.obj.ContentItem
import com.github.libretube.databinding.ChannelRowBinding import com.github.libretube.databinding.ChannelRowBinding
import com.github.libretube.databinding.PlaylistSearchRowBinding import com.github.libretube.databinding.PlaylistSearchRowBinding
import com.github.libretube.databinding.VideoRowBinding import com.github.libretube.databinding.VideoRowBinding
@ -26,12 +26,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class SearchAdapter( class SearchAdapter(
private val searchItems: MutableList<SearchItem>, private val searchItems: MutableList<ContentItem>,
private val childFragmentManager: FragmentManager private val childFragmentManager: FragmentManager
) : ) :
RecyclerView.Adapter<SearchViewHolder>() { RecyclerView.Adapter<SearchViewHolder>() {
fun updateItems(newItems: List<SearchItem>) { fun updateItems(newItems: List<ContentItem>) {
val searchItemsSize = searchItems.size val searchItemsSize = searchItems.size
searchItems.addAll(newItems) searchItems.addAll(newItems)
notifyItemRangeInserted(searchItemsSize, newItems.size) notifyItemRangeInserted(searchItemsSize, newItems.size)
@ -81,7 +81,7 @@ class SearchAdapter(
} }
} }
private fun bindWatch(item: SearchItem, binding: VideoRowBinding) { private fun bindWatch(item: ContentItem, binding: VideoRowBinding) {
binding.apply { binding.apply {
ImageHelper.loadImage(item.thumbnail, thumbnail) ImageHelper.loadImage(item.thumbnail, thumbnail)
thumbnailDuration.setFormattedDuration(item.duration!!) thumbnailDuration.setFormattedDuration(item.duration!!)
@ -114,7 +114,7 @@ class SearchAdapter(
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private fun bindChannel( private fun bindChannel(
item: SearchItem, item: ContentItem,
binding: ChannelRowBinding binding: ChannelRowBinding
) { ) {
binding.apply { binding.apply {
@ -164,7 +164,7 @@ class SearchAdapter(
} }
private fun bindPlaylist( private fun bindPlaylist(
item: SearchItem, item: ContentItem,
binding: PlaylistSearchRowBinding binding: PlaylistSearchRowBinding
) { ) {
binding.apply { binding.apply {

View File

@ -6,11 +6,13 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.core.view.children
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.SubscriptionHelper import com.github.libretube.api.SubscriptionHelper
import com.github.libretube.api.obj.ChannelTab
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
import com.github.libretube.constants.ShareObjectType import com.github.libretube.constants.ShareObjectType
import com.github.libretube.databinding.FragmentChannelBinding import com.github.libretube.databinding.FragmentChannelBinding
@ -18,9 +20,14 @@ import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.formatShort import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.toID import com.github.libretube.extensions.toID
import com.github.libretube.ui.adapters.ChannelAdapter import com.github.libretube.ui.adapters.ChannelAdapter
import com.github.libretube.ui.adapters.SearchAdapter
import com.github.libretube.ui.base.BaseFragment import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.ui.dialogs.ShareDialog import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.util.ImageHelper import com.github.libretube.util.ImageHelper
import com.google.android.material.chip.Chip
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException import java.io.IOException
@ -35,6 +42,10 @@ class ChannelFragment : BaseFragment() {
private var isLoading = true private var isLoading = true
private var isSubscribed: Boolean? = false private var isSubscribed: Boolean? = false
private var onScrollEnd: () -> Unit = {}
val scope = CoroutineScope(Dispatchers.IO)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
arguments?.let { arguments?.let {
@ -73,11 +84,10 @@ class ChannelFragment : BaseFragment() {
if (binding.channelScrollView.getChildAt(0).bottom if (binding.channelScrollView.getChildAt(0).bottom
== (binding.channelScrollView.height + binding.channelScrollView.scrollY) == (binding.channelScrollView.height + binding.channelScrollView.scrollY)
) { ) {
// scroll view is at bottom try {
if (nextPage != null && !isLoading) { onScrollEnd.invoke()
isLoading = true } catch (e: Exception) {
binding.channelRefresh.isRefreshing = true Log.e("tabs failed", e.toString())
fetchChannelNextPage()
} }
} }
} }
@ -103,6 +113,14 @@ class ChannelFragment : BaseFragment() {
// needed if the channel gets loaded by the ID // needed if the channel gets loaded by the ID
channelId = response.id channelId = response.id
onScrollEnd = {
if (nextPage != null && !isLoading) {
isLoading = true
binding.channelRefresh.isRefreshing = true
fetchChannelNextPage()
}
}
// fetch and update the subscription status // fetch and update the subscription status
isSubscribed = SubscriptionHelper.isSubscribed(channelId!!) isSubscribed = SubscriptionHelper.isSubscribed(channelId!!)
if (isSubscribed == null) return@launchWhenCreated if (isSubscribed == null) return@launchWhenCreated
@ -166,11 +184,74 @@ class ChannelFragment : BaseFragment() {
// recyclerview of the videos by the channel // recyclerview of the videos by the channel
channelAdapter = ChannelAdapter( channelAdapter = ChannelAdapter(
response.relatedStreams!!.toMutableList(), response.relatedStreams.orEmpty().toMutableList(),
childFragmentManager childFragmentManager
) )
binding.channelRecView.adapter = channelAdapter binding.channelRecView.adapter = channelAdapter
} }
binding.videos.setOnClickListener {
binding.channelRecView.adapter = channelAdapter
}
response.tabs?.firstOrNull { it.name == "Playlists" }?.let {
setupTab(binding.playlists, it)
}
response.tabs?.firstOrNull { it.name == "Channels" }?.let {
setupTab(binding.channels, it)
}
response.tabs?.firstOrNull { it.name == "Livestreams" }?.let {
setupTab(binding.livestreams, it)
}
response.tabs?.firstOrNull { it.name == "Shorts" }?.let {
setupTab(binding.shorts, it)
}
}
}
private fun setupTab(chip: Chip, tab: ChannelTab) {
chip.visibility = View.VISIBLE
chip.setOnClickListener {
binding.tabChips.children.forEach {
if (it != chip) (it as Chip).isChecked = false
}
scope.launch {
val response = try {
RetrofitInstance.api.getChannelTab(tab.data!!)
} catch (e: Exception) {
return@launch
}
val adapter = SearchAdapter(
response.content.toMutableList(),
childFragmentManager
)
runOnUiThread {
binding.channelRecView.adapter = adapter
}
onScrollEnd = {
if (response.nextpage != null) {
scope.launch {
val newContent = try {
RetrofitInstance.api.getChannelTab(tab.data, response.nextpage)
} catch (e: Exception) {
e.printStackTrace()
null
}
runOnUiThread {
newContent?.content?.let {
adapter.updateItems(it)
}
}
}
}
}
}
} }
} }

View File

@ -115,6 +115,48 @@
android:maxLines="2" android:maxLines="2"
android:padding="10dp" /> android:padding="10dp" />
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:scrollbars="none">
<com.google.android.material.chip.ChipGroup
android:id="@+id/tab_chips"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:id="@+id/videos"
style="@style/channelChip"
android:checked="true"
android:text="@string/videos"
android:visibility="visible" />
<com.google.android.material.chip.Chip
android:id="@+id/playlists"
style="@style/channelChip"
android:text="@string/playlists" />
<com.google.android.material.chip.Chip
android:id="@+id/shorts"
style="@style/channelChip"
android:text="@string/yt_shorts" />
<com.google.android.material.chip.Chip
android:id="@+id/channels"
style="@style/channelChip"
android:text="@string/channels" />
<com.google.android.material.chip.Chip
android:id="@+id/livestreams"
style="@style/channelChip"
android:text="@string/livestreams" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -352,6 +352,7 @@
<string name="queue">Queue</string> <string name="queue">Queue</string>
<string name="sb_markers">Markers</string> <string name="sb_markers">Markers</string>
<string name="sb_markers_summary">Mark the segments on the time bar.</string> <string name="sb_markers_summary">Mark the segments on the time bar.</string>
<string name="livestreams">Livestreams</string>
<!-- Notification channel strings --> <!-- Notification channel strings -->
<string name="download_channel_name">Download Service</string> <string name="download_channel_name">Download Service</string>

View File

@ -179,4 +179,13 @@
<item name="animationMode">slide</item> <item name="animationMode">slide</item>
</style> </style>
<style name="channelChip" parent="@style/Widget.Material3.Chip.Filter.Elevated">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:padding">10dp</item>
<item name="android:visibility">gone</item>
</style>
</resources> </resources>