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
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.DeleteUserRequest
import com.github.libretube.api.obj.Login
@ -63,6 +64,12 @@ interface PipedApi {
@GET("channel/{channelId}")
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}")
suspend fun getChannelByName(@Path("name") channelName: String): Channel

View File

@ -12,5 +12,6 @@ data class Channel(
var nextpage: String? = null,
var subscriberCount: Long = 0,
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
@JsonIgnoreProperties(ignoreUnknown = true)
data class SearchItem(
data class ContentItem(
var url: String? = null,
var thumbnail: String? = null,
var uploaderName: String? = null,

View File

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

View File

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

View File

@ -6,11 +6,13 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.children
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.SubscriptionHelper
import com.github.libretube.api.obj.ChannelTab
import com.github.libretube.constants.IntentData
import com.github.libretube.constants.ShareObjectType
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.toID
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.dialogs.ShareDialog
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 java.io.IOException
@ -35,6 +42,10 @@ class ChannelFragment : BaseFragment() {
private var isLoading = true
private var isSubscribed: Boolean? = false
private var onScrollEnd: () -> Unit = {}
val scope = CoroutineScope(Dispatchers.IO)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
@ -73,11 +84,10 @@ class ChannelFragment : BaseFragment() {
if (binding.channelScrollView.getChildAt(0).bottom
== (binding.channelScrollView.height + binding.channelScrollView.scrollY)
) {
// scroll view is at bottom
if (nextPage != null && !isLoading) {
isLoading = true
binding.channelRefresh.isRefreshing = true
fetchChannelNextPage()
try {
onScrollEnd.invoke()
} catch (e: Exception) {
Log.e("tabs failed", e.toString())
}
}
}
@ -103,6 +113,14 @@ class ChannelFragment : BaseFragment() {
// needed if the channel gets loaded by the ID
channelId = response.id
onScrollEnd = {
if (nextPage != null && !isLoading) {
isLoading = true
binding.channelRefresh.isRefreshing = true
fetchChannelNextPage()
}
}
// fetch and update the subscription status
isSubscribed = SubscriptionHelper.isSubscribed(channelId!!)
if (isSubscribed == null) return@launchWhenCreated
@ -166,11 +184,74 @@ class ChannelFragment : BaseFragment() {
// recyclerview of the videos by the channel
channelAdapter = ChannelAdapter(
response.relatedStreams!!.toMutableList(),
response.relatedStreams.orEmpty().toMutableList(),
childFragmentManager
)
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: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
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

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

View File

@ -179,4 +179,13 @@
<item name="animationMode">slide</item>
</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>