mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-01-07 18:10:31 +05:30
feat: changed chips in channels to swipable tabs
This commit is contained in:
parent
9742b8cda5
commit
f41e3b24bb
@ -43,4 +43,7 @@ object IntentData {
|
|||||||
const val audioLanguage = "audioLanguage"
|
const val audioLanguage = "audioLanguage"
|
||||||
const val captionLanguage = "captionLanguage"
|
const val captionLanguage = "captionLanguage"
|
||||||
const val wasIntentStopped = "wasIntentStopped"
|
const val wasIntentStopped = "wasIntentStopped"
|
||||||
|
const val tabData = "tabData"
|
||||||
|
const val videoList = "videoList"
|
||||||
|
const val nextPage = "nextPage"
|
||||||
}
|
}
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
package com.github.libretube.obj
|
|
||||||
|
|
||||||
import androidx.annotation.IdRes
|
|
||||||
import com.github.libretube.R
|
|
||||||
|
|
||||||
sealed class ChannelTabs(
|
|
||||||
val identifierName: String,
|
|
||||||
@IdRes val chipId: Int
|
|
||||||
) {
|
|
||||||
object Playlists : ChannelTabs("playlists", R.id.playlists)
|
|
||||||
object Shorts : ChannelTabs("shorts", R.id.shorts)
|
|
||||||
object Livestreams : ChannelTabs("livestreams", R.id.livestreams)
|
|
||||||
object Albums : ChannelTabs("albums", R.id.albums)
|
|
||||||
}
|
|
@ -0,0 +1,174 @@
|
|||||||
|
package com.github.libretube.ui.fragments
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isGone
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.github.libretube.api.RetrofitInstance
|
||||||
|
import com.github.libretube.api.obj.ChannelTab
|
||||||
|
import com.github.libretube.api.obj.StreamItem
|
||||||
|
import com.github.libretube.constants.IntentData
|
||||||
|
import com.github.libretube.databinding.FragmentChannelContentBinding
|
||||||
|
import com.github.libretube.extensions.TAG
|
||||||
|
import com.github.libretube.extensions.ceilHalf
|
||||||
|
import com.github.libretube.ui.adapters.SearchChannelAdapter
|
||||||
|
import com.github.libretube.ui.adapters.VideosAdapter
|
||||||
|
import com.github.libretube.ui.base.DynamicLayoutManagerFragment
|
||||||
|
import com.github.libretube.util.deArrow
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
class ChannelContentFragment : DynamicLayoutManagerFragment() {
|
||||||
|
private var _binding: FragmentChannelContentBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
private var channelId: String? = null
|
||||||
|
private var searchChannelAdapter: SearchChannelAdapter? = null
|
||||||
|
private var channelAdapter: VideosAdapter? = null
|
||||||
|
private var recyclerViewState: Parcelable? = null
|
||||||
|
private var nextPage: String? = null
|
||||||
|
private var isLoading: Boolean = false
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentChannelContentBinding.inflate(inflater, container, false)
|
||||||
|
return _binding!!.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setLayoutManagers(gridItems: Int) {
|
||||||
|
binding.channelRecView.layoutManager = GridLayoutManager(
|
||||||
|
requireContext(),
|
||||||
|
gridItems.ceilHalf()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchChannelNextPage(nextPage: String): String? {
|
||||||
|
val response = withContext(Dispatchers.IO) {
|
||||||
|
RetrofitInstance.api.getChannelNextPage(channelId!!, nextPage).apply {
|
||||||
|
relatedStreams = relatedStreams.deArrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channelAdapter?.insertItems(response.relatedStreams)
|
||||||
|
return response.nextpage
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchTabNextPage(nextPage: String, tab: ChannelTab): String? {
|
||||||
|
val newContent = withContext(Dispatchers.IO) {
|
||||||
|
RetrofitInstance.api.getChannelTab(tab.data, nextPage)
|
||||||
|
}.apply {
|
||||||
|
content = content.deArrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
searchChannelAdapter?.let {
|
||||||
|
it.submitList(it.currentList + newContent.content)
|
||||||
|
}
|
||||||
|
return newContent.nextpage
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
// manually restore the recyclerview state due to https://github.com/material-components/material-components-android/issues/3473
|
||||||
|
binding.channelRecView.layoutManager?.onRestoreInstanceState(recyclerViewState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadChannelTab(tab: ChannelTab) = lifecycleScope.launch {
|
||||||
|
val response = try {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
RetrofitInstance.api.getChannelTab(tab.data)
|
||||||
|
}.apply {
|
||||||
|
content = content.deArrow()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
binding.progressBar.isGone = true
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
nextPage = response.nextpage
|
||||||
|
|
||||||
|
val binding = _binding ?: return@launch
|
||||||
|
searchChannelAdapter = SearchChannelAdapter()
|
||||||
|
binding.channelRecView.adapter = searchChannelAdapter
|
||||||
|
searchChannelAdapter?.submitList(response.content)
|
||||||
|
binding.progressBar.isGone = true
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadNextPage(isVideo: Boolean, tab: ChannelTab) = lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
isLoading = true
|
||||||
|
nextPage = if (isVideo) {
|
||||||
|
fetchChannelNextPage(nextPage ?: return@launch)
|
||||||
|
} else {
|
||||||
|
fetchTabNextPage(nextPage ?: return@launch, tab)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG(), e.toString())
|
||||||
|
}
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
val tabData = kotlin.runCatching {
|
||||||
|
Json.decodeFromString<ChannelTab>(arguments?.getString(IntentData.tabData) ?: "")
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
|
channelId = arguments?.getString(IntentData.channelId)
|
||||||
|
nextPage = arguments?.getString(IntentData.nextPage)
|
||||||
|
|
||||||
|
binding.channelRecView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
|
super.onScrollStateChanged(recyclerView, newState)
|
||||||
|
recyclerViewState = binding.channelRecView.layoutManager?.onSaveInstanceState()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
val visibleItemCount = recyclerView.layoutManager!!.childCount
|
||||||
|
val totalItemCount = recyclerView.layoutManager!!.getItemCount()
|
||||||
|
val firstVisibleItemPosition =
|
||||||
|
(recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
|
||||||
|
|
||||||
|
if (_binding == null || isLoading) return
|
||||||
|
if (firstVisibleItemPosition + visibleItemCount >= totalItemCount) {
|
||||||
|
loadNextPage(tabData?.data!!.isEmpty(), tabData)
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (tabData?.data.isNullOrEmpty()) {
|
||||||
|
val videoDataString = arguments?.getString(IntentData.videoList)
|
||||||
|
val videos = runCatching {
|
||||||
|
Json.decodeFromString<List<StreamItem>>(videoDataString!!)
|
||||||
|
}.getOrElse { mutableListOf() }
|
||||||
|
channelAdapter = VideosAdapter(
|
||||||
|
videos.toMutableList(),
|
||||||
|
forceMode = VideosAdapter.Companion.LayoutMode.CHANNEL_ROW
|
||||||
|
)
|
||||||
|
binding.channelRecView.adapter = channelAdapter
|
||||||
|
binding.progressBar.isGone = true
|
||||||
|
|
||||||
|
} else {
|
||||||
|
loadChannelTab(tabData ?: return)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
}
|
@ -1,45 +1,44 @@
|
|||||||
package com.github.libretube.ui.fragments
|
package com.github.libretube.ui.fragments
|
||||||
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.children
|
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
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.obj.ChannelTab
|
import com.github.libretube.api.obj.ChannelTab
|
||||||
|
import com.github.libretube.api.obj.StreamItem
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.databinding.FragmentChannelBinding
|
import com.github.libretube.databinding.FragmentChannelBinding
|
||||||
import com.github.libretube.enums.ShareObjectType
|
import com.github.libretube.enums.ShareObjectType
|
||||||
import com.github.libretube.extensions.TAG
|
import com.github.libretube.extensions.TAG
|
||||||
import com.github.libretube.extensions.ceilHalf
|
|
||||||
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.helpers.ImageHelper
|
import com.github.libretube.helpers.ImageHelper
|
||||||
import com.github.libretube.helpers.NavigationHelper
|
import com.github.libretube.helpers.NavigationHelper
|
||||||
import com.github.libretube.obj.ChannelTabs
|
|
||||||
import com.github.libretube.obj.ShareData
|
import com.github.libretube.obj.ShareData
|
||||||
import com.github.libretube.ui.adapters.SearchChannelAdapter
|
|
||||||
import com.github.libretube.ui.adapters.VideosAdapter
|
import com.github.libretube.ui.adapters.VideosAdapter
|
||||||
import com.github.libretube.ui.base.DynamicLayoutManagerFragment
|
import com.github.libretube.ui.base.DynamicLayoutManagerFragment
|
||||||
import com.github.libretube.ui.dialogs.ShareDialog
|
import com.github.libretube.ui.dialogs.ShareDialog
|
||||||
import com.github.libretube.ui.extensions.addOnBottomReachedListener
|
|
||||||
import com.github.libretube.ui.extensions.setupSubscriptionButton
|
import com.github.libretube.ui.extensions.setupSubscriptionButton
|
||||||
import com.github.libretube.ui.sheets.AddChannelToGroupSheet
|
import com.github.libretube.ui.sheets.AddChannelToGroupSheet
|
||||||
import com.github.libretube.util.deArrow
|
import com.github.libretube.util.deArrow
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
@ -53,18 +52,21 @@ class ChannelFragment : DynamicLayoutManagerFragment() {
|
|||||||
private var channelAdapter: VideosAdapter? = null
|
private var channelAdapter: VideosAdapter? = null
|
||||||
private var isLoading = true
|
private var isLoading = true
|
||||||
|
|
||||||
private val possibleTabs = arrayOf(
|
private lateinit var channelContentAdapter: ChannelContentAdapter
|
||||||
ChannelTabs.Shorts,
|
|
||||||
ChannelTabs.Livestreams,
|
|
||||||
ChannelTabs.Playlists,
|
|
||||||
ChannelTabs.Albums
|
|
||||||
)
|
|
||||||
private var channelTabs: List<ChannelTab> = emptyList()
|
|
||||||
private var nextPages = Array<String?>(5) { null }
|
|
||||||
private var searchChannelAdapter: SearchChannelAdapter? = null
|
|
||||||
|
|
||||||
|
private var nextPages = Array<String?>(5) { null }
|
||||||
private var isAppBarFullyExpanded: Boolean = true
|
private var isAppBarFullyExpanded: Boolean = true
|
||||||
private var recyclerViewState: Parcelable? = null
|
private val tabList = mutableListOf(
|
||||||
|
ChannelTab(VIDEOS_TAB_KEY, "")
|
||||||
|
)
|
||||||
|
|
||||||
|
private val tabNamesMap = mapOf(
|
||||||
|
VIDEOS_TAB_KEY to R.string.videos,
|
||||||
|
"shorts" to R.string.yt_shorts,
|
||||||
|
"livestreams" to R.string.livestreams,
|
||||||
|
"playlists" to R.string.playlists,
|
||||||
|
"albums" to R.string.albums
|
||||||
|
)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@ -83,21 +85,17 @@ class ChannelFragment : DynamicLayoutManagerFragment() {
|
|||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setLayoutManagers(gridItems: Int) {
|
override fun setLayoutManagers(gridItems: Int) {}
|
||||||
_binding?.channelRecView?.layoutManager = GridLayoutManager(
|
|
||||||
context,
|
|
||||||
gridItems.ceilHalf()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
// Check if the AppBarLayout is fully expanded
|
// Check if the AppBarLayout is fully expanded
|
||||||
binding.channelAppBar.addOnOffsetChangedListener { _, verticalOffset ->
|
binding.channelAppBar.addOnOffsetChangedListener { _, verticalOffset ->
|
||||||
isAppBarFullyExpanded = verticalOffset == 0
|
isAppBarFullyExpanded = verticalOffset == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.pager.reduceDragSensitivity()
|
||||||
|
|
||||||
// Determine if the child can scroll up
|
// Determine if the child can scroll up
|
||||||
binding.channelRefresh.setOnChildScrollUpCallback { _, _ ->
|
binding.channelRefresh.setOnChildScrollUpCallback { _, _ ->
|
||||||
!isAppBarFullyExpanded
|
!isAppBarFullyExpanded
|
||||||
@ -107,58 +105,26 @@ class ChannelFragment : DynamicLayoutManagerFragment() {
|
|||||||
fetchChannel()
|
fetchChannel()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.channelRecView.addOnBottomReachedListener {
|
|
||||||
if (_binding == null || isLoading) return@addOnBottomReachedListener
|
|
||||||
|
|
||||||
loadNextPage()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.channelRecView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
|
||||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
||||||
super.onScrollStateChanged(recyclerView, newState)
|
|
||||||
recyclerViewState = binding.channelRecView.layoutManager?.onSaveInstanceState()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
fetchChannel()
|
fetchChannel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// adjust sensitivity due to the issue of viewpager2 with SwipeToRefresh https://issuetracker.google.com/issues/138314213
|
||||||
|
private fun ViewPager2.reduceDragSensitivity() {
|
||||||
|
val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView")
|
||||||
|
recyclerViewField.isAccessible = true
|
||||||
|
val recyclerView = recyclerViewField.get(this) as RecyclerView
|
||||||
|
|
||||||
|
val touchSlopField = RecyclerView::class.java.getDeclaredField("mTouchSlop")
|
||||||
|
touchSlopField.isAccessible = true
|
||||||
|
val touchSlop = touchSlopField.get(recyclerView) as Int
|
||||||
|
touchSlopField.set(recyclerView, touchSlop * 3)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadNextPage() = lifecycleScope.launch {
|
|
||||||
val binding = _binding ?: return@launch
|
|
||||||
|
|
||||||
binding.channelRefresh.isRefreshing = true
|
|
||||||
isLoading = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (binding.tabChips.checkedChipId == binding.videos.id) {
|
|
||||||
fetchChannelNextPage(nextPages[0] ?: return@launch).let {
|
|
||||||
nextPages[0] = it
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val currentTabIndex = binding.tabChips.children.indexOfFirst {
|
|
||||||
it.id == binding.tabChips.checkedChipId
|
|
||||||
}
|
|
||||||
val channelTab = channelTabs.first { tab ->
|
|
||||||
tab.name == possibleTabs[currentTabIndex - 1].identifierName
|
|
||||||
}
|
|
||||||
val nextPage = nextPages[currentTabIndex] ?: return@launch
|
|
||||||
fetchTabNextPage(nextPage, channelTab).let {
|
|
||||||
nextPages[currentTabIndex] = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("error fetching tabs", e.toString())
|
|
||||||
}
|
|
||||||
}.invokeOnCompletion {
|
|
||||||
_binding?.channelRefresh?.isRefreshing = false
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchChannel() = lifecycleScope.launch {
|
private fun fetchChannel() = lifecycleScope.launch {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
binding.channelRefresh.isRefreshing = true
|
binding.channelRefresh.isRefreshing = true
|
||||||
@ -255,105 +221,55 @@ class ChannelFragment : DynamicLayoutManagerFragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// recyclerview of the videos by the channel
|
channelContentAdapter = ChannelContentAdapter(
|
||||||
|
tabList,
|
||||||
|
response.relatedStreams,
|
||||||
|
response.nextpage,
|
||||||
|
channelId,
|
||||||
|
this@ChannelFragment
|
||||||
|
)
|
||||||
|
binding.pager.adapter = channelContentAdapter
|
||||||
|
TabLayoutMediator(binding.tabParent, binding.pager) { tab, position ->
|
||||||
|
tab.text = tabList[position].name
|
||||||
|
}.attach()
|
||||||
|
|
||||||
channelAdapter = VideosAdapter(
|
channelAdapter = VideosAdapter(
|
||||||
response.relatedStreams.toMutableList(),
|
response.relatedStreams.toMutableList(),
|
||||||
forceMode = VideosAdapter.Companion.LayoutMode.CHANNEL_ROW
|
forceMode = VideosAdapter.Companion.LayoutMode.CHANNEL_ROW
|
||||||
)
|
)
|
||||||
binding.channelRecView.adapter = channelAdapter
|
tabList.removeAll { tab ->
|
||||||
|
tab.name != VIDEOS_TAB_KEY
|
||||||
setupTabs(response.tabs)
|
}
|
||||||
|
response.tabs.forEach { channelTab ->
|
||||||
|
val tabName = tabNamesMap[channelTab.name]?.let { getString(it) }
|
||||||
|
?: channelTab.name.replaceFirstChar(Char::titlecase)
|
||||||
|
tabList.add(ChannelTab(tabName, channelTab.data))
|
||||||
|
}
|
||||||
|
channelContentAdapter.notifyItemRangeChanged(0, tabList.size - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupTabs(tabs: List<ChannelTab>) {
|
companion object {
|
||||||
this.channelTabs = tabs
|
private const val VIDEOS_TAB_KEY = "videos"
|
||||||
|
}
|
||||||
val binding = _binding ?: return
|
}
|
||||||
|
|
||||||
binding.tabChips.children.forEach { chip ->
|
class ChannelContentAdapter(
|
||||||
val resourceTab = possibleTabs.firstOrNull { it.chipId == chip.id }
|
private val list: List<ChannelTab>,
|
||||||
resourceTab?.let { resTab ->
|
private val videos: List<StreamItem>,
|
||||||
if (tabs.any { it.name == resTab.identifierName }) chip.isVisible = true
|
private val nextPage: String?,
|
||||||
}
|
private val channelId: String?,
|
||||||
}
|
fragment: Fragment
|
||||||
|
) : FragmentStateAdapter(fragment) {
|
||||||
binding.tabChips.setOnCheckedStateChangeListener { _, _ ->
|
override fun getItemCount() = list.size
|
||||||
when (binding.tabChips.checkedChipId) {
|
|
||||||
binding.videos.id -> {
|
override fun createFragment(position: Int): Fragment {
|
||||||
binding.channelRecView.adapter = channelAdapter
|
val fragment = ChannelContentFragment()
|
||||||
}
|
fragment.arguments = bundleOf(
|
||||||
|
IntentData.tabData to Json.encodeToString(list[position]),
|
||||||
else -> {
|
IntentData.videoList to Json.encodeToString(videos),
|
||||||
possibleTabs.first { binding.tabChips.checkedChipId == it.chipId }.let {
|
IntentData.channelId to channelId,
|
||||||
val tab = tabs.first { tab -> tab.name == it.identifierName }
|
IntentData.nextPage to nextPage
|
||||||
loadChannelTab(tab)
|
)
|
||||||
}
|
return fragment
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load selected chip content if it's not videos tab.
|
|
||||||
possibleTabs.firstOrNull { binding.tabChips.checkedChipId == it.chipId }?.let {
|
|
||||||
val tab = tabs.first { tab -> tab.name == it.identifierName }
|
|
||||||
loadChannelTab(tab)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadChannelTab(tab: ChannelTab) = lifecycleScope.launch {
|
|
||||||
binding.channelRefresh.isRefreshing = true
|
|
||||||
isLoading = true
|
|
||||||
|
|
||||||
val response = try {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
RetrofitInstance.api.getChannelTab(tab.data)
|
|
||||||
}.apply {
|
|
||||||
content = content.deArrow()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
nextPages[channelTabs.indexOf(tab) + 1] = response.nextpage
|
|
||||||
|
|
||||||
val binding = _binding ?: return@launch
|
|
||||||
|
|
||||||
searchChannelAdapter = SearchChannelAdapter()
|
|
||||||
binding.channelRecView.adapter = searchChannelAdapter
|
|
||||||
searchChannelAdapter?.submitList(response.content)
|
|
||||||
|
|
||||||
binding.channelRefresh.isRefreshing = false
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun fetchChannelNextPage(nextPage: String): String? {
|
|
||||||
val response = withContext(Dispatchers.IO) {
|
|
||||||
RetrofitInstance.api.getChannelNextPage(channelId!!, nextPage).apply {
|
|
||||||
relatedStreams = relatedStreams.deArrow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
channelAdapter?.insertItems(response.relatedStreams)
|
|
||||||
|
|
||||||
return response.nextpage
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun fetchTabNextPage(nextPage: String, tab: ChannelTab): String? {
|
|
||||||
val newContent = withContext(Dispatchers.IO) {
|
|
||||||
RetrofitInstance.api.getChannelTab(tab.data, nextPage)
|
|
||||||
}.apply {
|
|
||||||
content = content.deArrow()
|
|
||||||
}
|
|
||||||
|
|
||||||
searchChannelAdapter?.let {
|
|
||||||
it.submitList(it.currentList + newContent.content)
|
|
||||||
}
|
|
||||||
|
|
||||||
return newContent.nextpage
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
||||||
super.onConfigurationChanged(newConfig)
|
|
||||||
// manually restore the recyclerview state due to https://github.com/material-components/material-components-android/issues/3473
|
|
||||||
binding.channelRecView.layoutManager?.onRestoreInstanceState(recyclerViewState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,62 +119,24 @@
|
|||||||
android:autoLink="web"
|
android:autoLink="web"
|
||||||
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"
|
|
||||||
app:checkedChip="@+id/videos"
|
|
||||||
app:selectionRequired="true"
|
|
||||||
app:singleLine="true"
|
|
||||||
app:singleSelection="true">
|
|
||||||
|
|
||||||
<com.google.android.material.chip.Chip
|
|
||||||
android:id="@id/videos"
|
|
||||||
style="@style/channelChip"
|
|
||||||
android:text="@string/videos"
|
|
||||||
android:visibility="visible" />
|
|
||||||
|
|
||||||
<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/livestreams"
|
|
||||||
style="@style/channelChip"
|
|
||||||
android:text="@string/livestreams" />
|
|
||||||
|
|
||||||
<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/albums"
|
|
||||||
style="@style/channelChip"
|
|
||||||
android:text="@string/albums" />
|
|
||||||
|
|
||||||
</com.google.android.material.chip.ChipGroup>
|
|
||||||
|
|
||||||
</HorizontalScrollView>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||||
|
<com.google.android.material.tabs.TabLayout
|
||||||
|
android:id="@+id/tab_parent"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:tabMode="scrollable"
|
||||||
|
/>
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.viewpager2.widget.ViewPager2
|
||||||
android:id="@+id/channel_recView"
|
android:id="@+id/pager"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
|
/>
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
</com.github.libretube.ui.views.CustomSwipeToRefresh>
|
</com.github.libretube.ui.views.CustomSwipeToRefresh>
|
||||||
|
28
app/src/main/res/layout/fragment_channel_content.xml
Normal file
28
app/src/main/res/layout/fragment_channel_content.xml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".ui.fragments.ChannelContentFragment">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/channel_recView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:id="@+id/progress_bar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -551,4 +551,5 @@
|
|||||||
<string name="also_clear_watch_positions">Also clear watch positions</string>
|
<string name="also_clear_watch_positions">Also clear watch positions</string>
|
||||||
<string name="import_temp_playlist">Import temporary playlist?</string>
|
<string name="import_temp_playlist">Import temporary playlist?</string>
|
||||||
<string name="import_temp_playlist_summary">Do you want to create a new playlist named \'%1$s\'? The playlist will contain %2$d videos.</string>
|
<string name="import_temp_playlist_summary">Do you want to create a new playlist named \'%1$s\'? The playlist will contain %2$d videos.</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in New Issue
Block a user