diff --git a/app/src/main/java/com/github/libretube/api/PipedApi.kt b/app/src/main/java/com/github/libretube/api/PipedApi.kt index 061ea5691..f182fcae6 100644 --- a/app/src/main/java/com/github/libretube/api/PipedApi.kt +++ b/app/src/main/java/com/github/libretube/api/PipedApi.kt @@ -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 diff --git a/app/src/main/java/com/github/libretube/api/obj/Channel.kt b/app/src/main/java/com/github/libretube/api/obj/Channel.kt index d255187dd..e1b261df4 100644 --- a/app/src/main/java/com/github/libretube/api/obj/Channel.kt +++ b/app/src/main/java/com/github/libretube/api/obj/Channel.kt @@ -12,5 +12,6 @@ data class Channel( var nextpage: String? = null, var subscriberCount: Long = 0, var verified: Boolean = false, - var relatedStreams: List? = null + var relatedStreams: List? = listOf(), + var tabs: List? = listOf() ) diff --git a/app/src/main/java/com/github/libretube/api/obj/ChannelTab.kt b/app/src/main/java/com/github/libretube/api/obj/ChannelTab.kt new file mode 100644 index 000000000..c7e2d63de --- /dev/null +++ b/app/src/main/java/com/github/libretube/api/obj/ChannelTab.kt @@ -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 +) diff --git a/app/src/main/java/com/github/libretube/api/obj/ChannelTabResponse.kt b/app/src/main/java/com/github/libretube/api/obj/ChannelTabResponse.kt new file mode 100644 index 000000000..6c6fd7bae --- /dev/null +++ b/app/src/main/java/com/github/libretube/api/obj/ChannelTabResponse.kt @@ -0,0 +1,6 @@ +package com.github.libretube.api.obj + +data class ChannelTabResponse( + val content: List = listOf(), + val nextpage: String? = null +) diff --git a/app/src/main/java/com/github/libretube/api/obj/SearchItem.kt b/app/src/main/java/com/github/libretube/api/obj/ContentItem.kt similarity index 97% rename from app/src/main/java/com/github/libretube/api/obj/SearchItem.kt rename to app/src/main/java/com/github/libretube/api/obj/ContentItem.kt index f55604d8b..af7181e5c 100644 --- a/app/src/main/java/com/github/libretube/api/obj/SearchItem.kt +++ b/app/src/main/java/com/github/libretube/api/obj/ContentItem.kt @@ -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, diff --git a/app/src/main/java/com/github/libretube/api/obj/SearchResult.kt b/app/src/main/java/com/github/libretube/api/obj/SearchResult.kt index 42ff66005..8ec7699f0 100644 --- a/app/src/main/java/com/github/libretube/api/obj/SearchResult.kt +++ b/app/src/main/java/com/github/libretube/api/obj/SearchResult.kt @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties @JsonIgnoreProperties(ignoreUnknown = true) data class SearchResult( - val items: MutableList? = arrayListOf(), + val items: MutableList? = arrayListOf(), val nextpage: String? = "", val suggestion: String? = "", val corrected: Boolean? = null diff --git a/app/src/main/java/com/github/libretube/ui/adapters/SearchAdapter.kt b/app/src/main/java/com/github/libretube/ui/adapters/SearchAdapter.kt index fcc71b98e..9043446b1 100644 --- a/app/src/main/java/com/github/libretube/ui/adapters/SearchAdapter.kt +++ b/app/src/main/java/com/github/libretube/ui/adapters/SearchAdapter.kt @@ -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, + private val searchItems: MutableList, private val childFragmentManager: FragmentManager ) : RecyclerView.Adapter() { - fun updateItems(newItems: List) { + fun updateItems(newItems: List) { 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 { diff --git a/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt index 4f9584f2e..9ddb36bc3 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt @@ -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) + } + } + } + } + } + } } } diff --git a/app/src/main/res/layout/fragment_channel.xml b/app/src/main/res/layout/fragment_channel.xml index 70bdf6509..ab26d98aa 100644 --- a/app/src/main/res/layout/fragment_channel.xml +++ b/app/src/main/res/layout/fragment_channel.xml @@ -115,6 +115,48 @@ android:maxLines="2" android:padding="10dp" /> + + + + + + + + + + + + + + + + + + Queue Markers Mark the segments on the time bar. + Livestreams Download Service diff --git a/app/src/main/res/values/style.xml b/app/src/main/res/values/style.xml index a1858f15e..e5837b1e8 100644 --- a/app/src/main/res/values/style.xml +++ b/app/src/main/res/values/style.xml @@ -179,4 +179,13 @@ slide + + \ No newline at end of file