mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-13 22:00:30 +05:30
Merge pull request #6053 from Bnyro/channel-tabs
feat: changed chips in channels to swipable tabs
This commit is contained in:
commit
45fbef920a
@ -43,4 +43,7 @@ object IntentData {
|
||||
const val audioLanguage = "audioLanguage"
|
||||
const val captionLanguage = "captionLanguage"
|
||||
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
|
||||
|
||||
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.os.bundleOf
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.github.libretube.R
|
||||
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.FragmentChannelBinding
|
||||
import com.github.libretube.enums.ShareObjectType
|
||||
import com.github.libretube.extensions.TAG
|
||||
import com.github.libretube.extensions.ceilHalf
|
||||
import com.github.libretube.extensions.formatShort
|
||||
import com.github.libretube.extensions.toID
|
||||
import com.github.libretube.helpers.ImageHelper
|
||||
import com.github.libretube.helpers.NavigationHelper
|
||||
import com.github.libretube.obj.ChannelTabs
|
||||
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.base.DynamicLayoutManagerFragment
|
||||
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.sheets.AddChannelToGroupSheet
|
||||
import com.github.libretube.util.deArrow
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
@ -53,18 +52,21 @@ class ChannelFragment : DynamicLayoutManagerFragment() {
|
||||
private var channelAdapter: VideosAdapter? = null
|
||||
private var isLoading = true
|
||||
|
||||
private val possibleTabs = arrayOf(
|
||||
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 lateinit var channelContentAdapter: ChannelContentAdapter
|
||||
|
||||
private var nextPages = Array<String?>(5) { null }
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -83,21 +85,17 @@ class ChannelFragment : DynamicLayoutManagerFragment() {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun setLayoutManagers(gridItems: Int) {
|
||||
_binding?.channelRecView?.layoutManager = GridLayoutManager(
|
||||
context,
|
||||
gridItems.ceilHalf()
|
||||
)
|
||||
}
|
||||
override fun setLayoutManagers(gridItems: Int) {}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// Check if the AppBarLayout is fully expanded
|
||||
binding.channelAppBar.addOnOffsetChangedListener { _, verticalOffset ->
|
||||
isAppBarFullyExpanded = verticalOffset == 0
|
||||
}
|
||||
|
||||
binding.pager.reduceDragSensitivity()
|
||||
|
||||
// Determine if the child can scroll up
|
||||
binding.channelRefresh.setOnChildScrollUpCallback { _, _ ->
|
||||
!isAppBarFullyExpanded
|
||||
@ -107,58 +105,26 @@ class ChannelFragment : DynamicLayoutManagerFragment() {
|
||||
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()
|
||||
}
|
||||
|
||||
// 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() {
|
||||
super.onDestroyView()
|
||||
_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 {
|
||||
isLoading = 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(
|
||||
response.relatedStreams.toMutableList(),
|
||||
forceMode = VideosAdapter.Companion.LayoutMode.CHANNEL_ROW
|
||||
)
|
||||
binding.channelRecView.adapter = channelAdapter
|
||||
|
||||
setupTabs(response.tabs)
|
||||
tabList.removeAll { tab ->
|
||||
tab.name != VIDEOS_TAB_KEY
|
||||
}
|
||||
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>) {
|
||||
this.channelTabs = tabs
|
||||
|
||||
val binding = _binding ?: return
|
||||
|
||||
binding.tabChips.children.forEach { chip ->
|
||||
val resourceTab = possibleTabs.firstOrNull { it.chipId == chip.id }
|
||||
resourceTab?.let { resTab ->
|
||||
if (tabs.any { it.name == resTab.identifierName }) chip.isVisible = true
|
||||
companion object {
|
||||
private const val VIDEOS_TAB_KEY = "videos"
|
||||
}
|
||||
}
|
||||
|
||||
binding.tabChips.setOnCheckedStateChangeListener { _, _ ->
|
||||
when (binding.tabChips.checkedChipId) {
|
||||
binding.videos.id -> {
|
||||
binding.channelRecView.adapter = channelAdapter
|
||||
}
|
||||
class ChannelContentAdapter(
|
||||
private val list: List<ChannelTab>,
|
||||
private val videos: List<StreamItem>,
|
||||
private val nextPage: String?,
|
||||
private val channelId: String?,
|
||||
fragment: Fragment
|
||||
) : FragmentStateAdapter(fragment) {
|
||||
override fun getItemCount() = list.size
|
||||
|
||||
else -> {
|
||||
possibleTabs.first { binding.tabChips.checkedChipId == it.chipId }.let {
|
||||
val tab = tabs.first { tab -> tab.name == it.identifierName }
|
||||
loadChannelTab(tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
val fragment = ChannelContentFragment()
|
||||
fragment.arguments = bundleOf(
|
||||
IntentData.tabData to Json.encodeToString(list[position]),
|
||||
IntentData.videoList to Json.encodeToString(videos),
|
||||
IntentData.channelId to channelId,
|
||||
IntentData.nextPage to nextPage
|
||||
)
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
|
@ -119,62 +119,24 @@
|
||||
android:autoLink="web"
|
||||
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>
|
||||
|
||||
</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>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/channel_recView"
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="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>
|
||||
</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="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>
|
||||
|
||||
</resources>
|
Loading…
Reference in New Issue
Block a user