mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-28 07:50:31 +05:30
commit
b1fa827a3e
@ -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
|
||||||
|
|
||||||
|
@ -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()
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
)
|
@ -0,0 +1,6 @@
|
|||||||
|
package com.github.libretube.api.obj
|
||||||
|
|
||||||
|
data class ChannelTabResponse(
|
||||||
|
val content: List<ContentItem> = listOf(),
|
||||||
|
val nextpage: String? = null
|
||||||
|
)
|
@ -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,
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
Loading…
x
Reference in New Issue
Block a user