Library on TV (#663)

* implementation for Library on TV
This commit is contained in:
KingLucius 2023-10-10 18:16:35 +03:00 committed by GitHub
parent 5b4fd8d77d
commit b120a7bce2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 305 additions and 27 deletions

View file

@ -9,6 +9,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding
import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
import com.lagradost.cloudstream3.databinding.FragmentLibraryTvBinding
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding
import com.lagradost.cloudstream3.databinding.FragmentResultBinding import com.lagradost.cloudstream3.databinding.FragmentResultBinding
@ -120,6 +122,8 @@ class ExampleInstrumentedTest {
testAllLayouts<HomepageParentTvBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent) testAllLayouts<HomepageParentTvBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent)
testAllLayouts<HomepageParentBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent) testAllLayouts<HomepageParentBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent)
testAllLayouts<FragmentLibraryTvBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
testAllLayouts<FragmentLibraryBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
} }
} }
} }

View file

@ -543,9 +543,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
navRailView.isVisible = isNavVisible && landscape navRailView.isVisible = isNavVisible && landscape
// Hide library on TV since it is not supported yet :( // Hide library on TV since it is not supported yet :(
val isTrueTv = isTrueTvSettings() //val isTrueTv = isTrueTvSettings()
navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv //navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv //navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
// Hide downloads on TV // Hide downloads on TV
//navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv //navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv

View file

@ -1,38 +1,52 @@
package com.lagradost.cloudstream3.ui.library package com.lagradost.cloudstream3.ui.library
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
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 android.view.ViewGroup.FOCUS_AFTER_DESCENDANTS
import android.view.ViewGroup.FOCUS_BLOCK_DESCENDANTS
import android.view.animation.AlphaAnimation import android.view.animation.AlphaAnimation
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.allViews
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.APIHolder.allProviders
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity
@ -80,9 +94,21 @@ class LibraryFragment : Fragment() {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View { ): View {
val localBinding = FragmentLibraryBinding.inflate(inflater, container, false) val layout =
binding = localBinding if (SettingsFragment.isTvSettings()) R.layout.fragment_library_tv else R.layout.fragment_library
return localBinding.root val root = inflater.inflate(layout, container, false)
binding = try {
FragmentLibraryBinding.bind(root)
} catch (t: Throwable) {
CommonActivity.showToast(
txt(R.string.unable_to_inflate, t.message ?: ""),
Toast.LENGTH_LONG
)
logError(t)
null
}
return root
//return inflater.inflate(R.layout.fragment_library, container, false) //return inflater.inflate(R.layout.fragment_library, container, false)
} }
@ -99,24 +125,16 @@ class LibraryFragment : Fragment() {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
@SuppressLint("ResourceType", "CutPasteId")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
fixPaddingStatusbar(binding?.searchStatusBarPadding) fixPaddingStatusbar(binding?.searchStatusBarPadding)
binding?.sortFab?.setOnClickListener { binding?.sortFab?.setOnClickListener(sortChangeClickListener)
val methods = libraryViewModel.sortingMethods.map { binding?.librarySort?.setOnClickListener(sortChangeClickListener)
txt(it.stringRes).asString(view.context)
}
activity?.showBottomDialog(methods, binding?.libraryRoot?.findViewById<TextView>(R.id.search_src_text)?.apply {
libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod), tag = "tv_no_focus_tag"
txt(R.string.sort_by).asString(view.context),
false,
{},
{
val method = libraryViewModel.sortingMethods[it]
libraryViewModel.sort(method)
})
} }
binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
@ -266,7 +284,10 @@ class LibraryFragment : Fragment() {
// selects the whole list opener // selects the whole list opener
val savedListSelection = val savedListSelection =
getKey<LibraryOpener>("$currentAccount/$LIBRARY_FOLDER", syncName.name) getKey<LibraryOpener>("$currentAccount/$LIBRARY_FOLDER", syncName.name)
val savedSelection = getKey<LibraryOpener>("$currentAccount/$LIBRARY_FOLDER", syncId).takeIf { val savedSelection = getKey<LibraryOpener>(
"$currentAccount/$LIBRARY_FOLDER",
syncId
).takeIf {
it?.openType != LibraryOpenerType.Default it?.openType != LibraryOpenerType.Default
} ?: savedListSelection } ?: savedListSelection
@ -354,6 +375,12 @@ class LibraryFragment : Fragment() {
} }
(viewpager.adapter as? ViewpagerAdapter)?.pages = pages (viewpager.adapter as? ViewpagerAdapter)?.pages = pages
//fix focus on the viewpager itself
(viewpager.getChildAt(0) as RecyclerView).apply {
tag = "tv_no_focus_tag"
//isFocusable = false
}
// Using notifyItemRangeChanged keeps the animations when sorting // Using notifyItemRangeChanged keeps the animations when sorting
viewpager.adapter?.notifyItemRangeChanged( viewpager.adapter?.notifyItemRangeChanged(
0, 0,
@ -396,6 +423,9 @@ class LibraryFragment : Fragment() {
viewpager, viewpager,
) { tab, position -> ) { tab, position ->
tab.text = pages.getOrNull(position)?.title?.asStringNull(context) tab.text = pages.getOrNull(position)?.title?.asStringNull(context)
tab.view.tag = "tv_no_focus_tag"
tab.view.nextFocusDownId = R.id.search_result_root
tab.view.setOnClickListener { tab.view.setOnClickListener {
val currentItem = val currentItem =
binding?.viewpager?.currentItem ?: return@setOnClickListener binding?.viewpager?.currentItem ?: return@setOnClickListener
@ -418,17 +448,45 @@ class LibraryFragment : Fragment() {
} }
} }
} }
binding?.viewpager?.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { binding?.viewpager?.registerOnPageChangeCallback(object :
ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
libraryViewModel.currentPage = position val all = binding?.viewpager?.allViews?.toList()
?.filterIsInstance<AutofitRecyclerView>()
all?.forEach { view ->
view.isVisible = view.tag == position
view.isFocusable = view.tag == position
if (view.tag == position)
view.descendantFocusability = FOCUS_AFTER_DESCENDANTS
else
view.descendantFocusability = FOCUS_BLOCK_DESCENDANTS
}
super.onPageSelected(position)
} }
}) })
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
(binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind() (binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind()
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
} }
private val sortChangeClickListener = View.OnClickListener { view ->
val methods = libraryViewModel.sortingMethods.map {
txt(it.stringRes).asString(view.context)
}
activity?.showBottomDialog(methods,
libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod),
txt(R.string.sort_by).asString(view.context),
false,
{},
{
val method = libraryViewModel.sortingMethods[it]
libraryViewModel.sort(method)
})
}
} }
class MenuSearchView(context: Context) : SearchView(context) { class MenuSearchView(context: Context) : SearchView(context) {

View file

@ -25,7 +25,7 @@ class ViewpagerAdapter(
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) { when (holder) {
is PageViewHolder -> { is PageViewHolder -> {
holder.bind(pages[position], unbound.remove(position)) holder.bind(pages[position], position, unbound.remove(position))
} }
} }
} }
@ -43,7 +43,8 @@ class ViewpagerAdapter(
inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) : inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun bind(page: SyncAPI.Page, rebind: Boolean) { fun bind(page: SyncAPI.Page, position: Int, rebind: Boolean) {
binding.pageRecyclerview.tag = position
binding.pageRecyclerview.apply { binding.pageRecyclerview.apply {
spanCount = spanCount =
this@PageViewHolder.itemView.context.getSpanCount() ?: 3 this@PageViewHolder.itemView.context.getSpanCount() ?: 3

View file

@ -41,6 +41,20 @@
android:src="@drawable/ic_baseline_extension_24" android:src="@drawable/ic_baseline_extension_24"
app:tint="?attr/textColor" /> app:tint="?attr/textColor" />
<ImageView
android:id="@+id/library_sort"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_gravity="end|center_vertical"
android:layout_marginStart="10dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/change_providers_img_des"
android:padding="5dp"
android:visibility="gone"
tools:visibility="visible"
android:src="@drawable/ic_baseline_sort_24"
app:tint="?attr/textColor" />
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="40dp" android:layout_height="40dp"
@ -160,8 +174,7 @@
android:layout_height="40dp" android:layout_height="40dp"
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:background="?attr/primaryGrayBackground" android:background="?attr/primaryGrayBackground"
android:descendantFocusability="blocksDescendants" android:focusable="true"
android:focusable="false"
android:paddingHorizontal="5dp" android:paddingHorizontal="5dp"
app:layout_scrollFlags="noScroll" app:layout_scrollFlags="noScroll"
app:tabGravity="center" app:tabGravity="center"

View file

@ -0,0 +1,200 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:id="@+id/library_root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/empty_list_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="30dp"
android:gravity="center"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/primaryGrayBackground">
<LinearLayout
android:id="@+id/search_status_bar_padding"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/provider_selector"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_gravity="end|center_vertical"
android:layout_marginStart="10dp"
android:padding="2dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/change_providers_img_des"
android:nextFocusLeft="@id/nav_rail_view"
android:nextFocusRight="@id/library_sort"
android:tag="@string/tv_no_focus_tag"
android:src="@drawable/ic_baseline_extension_24"
app:tint="?attr/textColor" />
<ImageView
android:id="@+id/library_sort"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_gravity="end|center_vertical"
android:layout_marginStart="10dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/change_providers_img_des"
android:nextFocusLeft="@id/provider_selector"
android:nextFocusRight="@id/list_selector"
android:tag="@string/tv_no_focus_tag"
android:src="@drawable/ic_baseline_sort_24"
app:tint="?attr/textColor" />
<ImageView
android:id="@+id/list_selector"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_gravity="end|center_vertical"
android:padding="1dp"
android:layout_marginStart="10dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/change_providers_img_des"
android:nextFocusLeft="@id/library_sort"
android:nextFocusRight="@id/main_search"
android:tag="@string/tv_no_focus_tag"
android:src="@drawable/ic_baseline_filter_list_24"
app:tint="?attr/textColor" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_margin="10dp"
android:background="@drawable/search_background"
android:visibility="visible"
app:layout_scrollFlags="scroll|enterAlways">
<androidx.appcompat.widget.SearchView
android:id="@+id/main_search"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="end"
android:iconifiedByDefault="false"
android:imeOptions="actionSearch"
android:inputType="text"
android:focusable="true"
android:tag="tv_no_focus_tag"
android:nextFocusLeft="@id/list_selector"
android:nextFocusDown="@id/search_result_root"
android:paddingStart="-10dp"
app:iconifiedByDefault="false"
app:queryBackground="@color/transparent"
app:queryHint="@string/search_hint"
app:searchIcon="@drawable/search_icon"
tools:ignore="RtlSymmetry">
</androidx.appcompat.widget.SearchView>
</FrameLayout>
</LinearLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/library_tab_layout"
style="@style/Theme.Widget.Tabs"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_gravity="top"
android:nextFocusDown="@id/search_result_root"
android:background="?attr/primaryGrayBackground"
android:paddingHorizontal="5dp"
app:layout_scrollFlags="noScroll"
app:tabGravity="center"
app:tabIndicator="@drawable/indicator_background"
app:tabIndicatorColor="?attr/white"
app:tabIndicatorGravity="center"
app:tabIndicatorHeight="30dp"
app:tabMode="scrollable"
app:tabSelectedTextColor="?attr/primaryBlackBackground"
app:tabTextAppearance="@style/TabNoCaps"
app:tabTextColor="?attr/textColor" />
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="40dp"
android:focusable="true"
android:tag="@string/tv_no_focus_tag"
tools:listitem="@layout/library_viewpager_page" />
<LinearLayout
android:id="@+id/library_loading_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="false"
android:tag="@string/tv_no_focus_tag"
android:background="?attr/primaryBlackBackground"
android:visibility="gone"
tools:visibility="visible">
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/library_loading_shimmer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_margin="2dp"
app:shimmer_auto_start="true"
app:shimmer_base_alpha="0.2"
android:focusable="false"
android:tag="@string/tv_no_focus_tag"
app:shimmer_duration="@integer/loading_time"
app:shimmer_highlight_alpha="0.3">
<GridView
android:id="@+id/gridview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:gravity="center"
android:horizontalSpacing="10dp"
android:numColumns="3"
android:paddingBottom="120dp"
android:focusable="false"
android:tag="@string/tv_no_focus_tag"
android:verticalSpacing="10dp"
tools:listitem="@layout/loading_poster_dynamic" />
</com.facebook.shimmer.ShimmerFrameLayout>
</LinearLayout>
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="40dp">
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/sort_fab"
style="@style/ExtendedFloatingActionButton"
android:text="@string/sort"
android:textColor="?attr/textColor"
android:visibility="gone"
tools:visibility="visible"
app:icon="@drawable/ic_baseline_sort_24"
tools:ignore="ContentDescription" />
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -5,5 +5,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:focusable="false"
android:tag="tv_no_focus_tag"
tools:listitem="@layout/home_result_grid_expanded" /> tools:listitem="@layout/home_result_grid_expanded" />