diff --git a/app/src/main/assets/licenses/Inter-OFL.txt b/app/src/main/assets/licenses/Inter-OFL.txt new file mode 100644 index 00000000000..9b2ca37b3ff --- /dev/null +++ b/app/src/main/assets/licenses/Inter-OFL.txt @@ -0,0 +1,92 @@ +Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 8daf168c1c1..ab7837ad61e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -356,6 +356,7 @@ object CommonActivity { "Dracula" -> R.style.DraculaMode "Lavender" -> R.style.LavenderMode "SilentBlue" -> R.style.SilentBlueMode + "TvOS" -> R.style.TvOSMode else -> R.style.AppTheme } @@ -381,6 +382,7 @@ object CommonActivity { "Party" -> R.style.OverlayPrimaryColorParty "Pink" -> R.style.OverlayPrimaryColorPink "Lavender" -> R.style.OverlayPrimaryColorLavender + "AppleBlue" -> R.style.OverlayPrimaryColorAppleBlue "Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 00e18158b0f..f353ddc939d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -102,6 +102,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.localLi import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.SyncWatchType +import com.lagradost.cloudstream3.ui.TvLiftFocus import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLinear import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO @@ -490,6 +491,27 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa navHostFragment.navController.currentDestination?.let { updateNavBar(it) } } + private var isTabBarShown = true + + /** The tab bar scrolls away with the home page, called on every scrolled frame */ + private fun updateTabBarVisibility() { + val tabBar = tvTabBarBinding?.navTopBar ?: return + if (!tabBar.isVisible) return + + val homeRecycler = binding?.root?.findViewById(R.id.home_master_recycler) + val show = tabBar.hasFocus() || homeRecycler == null || + homeRecycler.computeVerticalScrollOffset() < 40.toPx + if (show == isTabBarShown) return + isTabBarShown = show + + tabBar.animate().cancel() + tabBar.animate() + .translationY(if (show) 0.0f else -(tabBar.bottom + 10.toPx).toFloat()) + .alpha(if (show) 1.0f else 0.0f) + .setDuration(200) + .start() + } + private fun updateNavBar(destination: NavDestination) { this.hideKeyboard() @@ -557,17 +579,31 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa }*/ binding?.apply { - navRailView.isVisible = isNavVisible && isLandscape() + // On TV the top tab bar replaces the nav rail + navRailView.isVisible = isNavVisible && isLandscape() && !isLayout(TV) navView.isVisible = isNavVisible && !isLandscape() navHostFragment.apply { val marginPx = resources.getDimensionPixelSize(R.dimen.nav_rail_view_width) layoutParams = (navHostFragment.layoutParams as ViewGroup.MarginLayoutParams).apply { marginStart = - if (isNavVisible && isLandscape() && isLayout(TV or EMULATOR)) marginPx else 0 + if (isNavVisible && isLandscape() && isLayout(EMULATOR)) marginPx else 0 } } + tvTabBarBinding?.apply { + navTopBar.isVisible = destination.id == R.id.navigation_home + if (navTopBar.isVisible) { + updateTabBarVisibility() + // Focus "Home" on app start and whenever nothing is focused + val isFirstFocus = !hasDoneInitialTvFocus + hasDoneInitialTvFocus = true + navTopBar.postDelayed({ + if (isFirstFocus || currentFocus?.isShown != true) navTopHome.requestFocus() + }, 500) + } + } + /** * We need to make sure if we return to a sub-fragment, * the correct navigation item is selected so that it does not @@ -908,6 +944,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa var binding: ActivityMainBinding? = null + /** Only set on TV, holds the top tab bar */ + private var tvTabBarBinding: ActivityMainTvBinding? = null + private var hasDoneInitialTvFocus = false + object TvFocus { data class FocusTarget( val width: Int, @@ -1251,6 +1291,16 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } if (isLayout(TV)) { + tvTabBarBinding = newLocalBinding + newLocalBinding.navTopHome.isSelected = true + newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> + TvLiftFocus.onFocusChanged(newFocus) + updateTabBarVisibility() + } + newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { + updateTabBarVisibility() + } + // Put here any button you don't want focusing it to center the view val exceptionButtons = listOf( //R.id.home_preview_play_btt, @@ -1837,6 +1887,31 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } } + tvTabBarBinding?.apply { + for ((tab, destinationId) in listOf( + navTopHome to R.id.navigation_home, + navTopSearch to R.id.navigation_search, + navTopLibrary to R.id.navigation_library, + navTopDownloads to R.id.navigation_downloads, + navTopSettings to R.id.navigation_settings, + )) { + tab.setOnClickListener { + binding?.navRailView?.menu?.findItem(destinationId)?.let { item -> + onNavDestinationSelected(item, navController) + } + } + } + + navTopProfileCard.setOnClickListener { + showAccountSelectLinear() + } + val homeViewModel = ViewModelProvider(this@MainActivity)[HomeViewModel::class.java] + observe(homeViewModel.currentAccount) { currentAccount -> + navTopProfileCard.isVisible = currentAccount != null + if (currentAccount != null) navTopProfilePic.loadImage(currentAccount.image) + } + } + // Navigation button long click functionality to scroll to top for (view in listOf(binding?.navView, binding?.navRailView)) { view?.findViewById(R.id.navigation_home)?.setOnLongClickListener { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/TvLiftFocus.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/TvLiftFocus.kt new file mode 100644 index 00000000000..974c9b99a45 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/TvLiftFocus.kt @@ -0,0 +1,78 @@ +package com.lagradost.cloudstream3.ui + +import android.graphics.drawable.Drawable +import android.view.View +import android.view.animation.DecelerateInterpolator +import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.utils.UIHelper.toPx +import java.lang.ref.WeakReference + +/** + * tvOS-like focus: the focused view is scaled up with a shadow instead of being outlined. + * Large views would overflow the screen, so they keep their focus drawable. + * The lifted view must fit in the padding of its parent (tv_lift_room), as unclipping + * the parents doubles the GPU time of every frame. + */ +object TvLiftFocus { + private const val CARD_SCALE = 1.10f + private const val BUTTON_SCALE = 1.06f + private const val DURATION = 180L + + /** Max size of a liftable view, relative to the screen */ + private const val MAX_WIDTH_FRACTION = 0.4f + private const val MAX_HEIGHT_FRACTION = 0.6f + + private val interpolator = DecelerateInterpolator(1.5f) + private var lifted: WeakReference = WeakReference(null) + private var liftedForeground: Drawable? = null + + /** Hides the title below [card] until it is focused, like the posters on tvOS */ + fun hideTitleUntilFocused(card: View) { + val title = card.findViewById(R.id.imageText) ?: return + card.setTag(R.id.tv_hide_title, true) + title.animate().cancel() + title.alpha = if (card.hasFocus()) 1.0f else 0.0f + } + + fun onFocusChanged(newFocus: View?) { + lifted.get()?.let { old -> if (old != newFocus) lower(old) } + lifted = WeakReference(null) + if (newFocus == null || !canLift(newFocus)) return + + // RecyclerView items are cards (posters, episodes), everything else is a button + animate(newFocus, if (newFocus.parent is RecyclerView) CARD_SCALE else BUTTON_SCALE, 12.toPx) + lifted = WeakReference(newFocus) + liftedForeground = newFocus.foreground + newFocus.foreground = null + } + + private fun lower(view: View) { + if (view.foreground == null) view.foreground = liftedForeground + animate(view, 1.0f, 0) + } + + private fun animate(view: View, scale: Float, elevation: Int) { + view.animate() + .scaleX(scale) + .scaleY(scale) + .translationZ(elevation.toFloat()) + .setDuration(DURATION) + .setInterpolator(interpolator) + .start() + if (view.getTag(R.id.tv_hide_title) == true) { + view.findViewById(R.id.imageText)?.animate() + ?.alpha(if (scale > 1.0f) 1.0f else 0.0f) + ?.setDuration(DURATION) + ?.start() + } + } + + private fun canLift(view: View): Boolean { + if (view.tag == view.context.getString(R.string.tv_no_focus_tag)) return false + val root = view.rootView ?: return false + return view.width > 0 && view.height > 0 && + view.width <= root.width * MAX_WIDTH_FRACTION && + view.height <= root.height * MAX_HEIGHT_FRACTION + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt index 43f6d19ff6a..04b0e79ef87 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt @@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.databinding.HomeResultGridBinding import com.lagradost.cloudstream3.databinding.HomeResultGridExpandedBinding import com.lagradost.cloudstream3.ui.BaseAdapter import com.lagradost.cloudstream3.ui.BaseDiffCallback +import com.lagradost.cloudstream3.ui.TvLiftFocus import com.lagradost.cloudstream3.ui.ViewHolderState import com.lagradost.cloudstream3.ui.newSharedPool import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD @@ -226,5 +227,6 @@ open class HomeChildItemAdapter( ) holder.itemView.tag = position + if (isLayout(TV)) TvLiftFocus.hideTitleUntilFocused(holder.itemView) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 04f8b0b26ff..44f2f313307 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.home import android.annotation.SuppressLint import android.app.Activity +import android.app.Dialog import android.content.Context import android.content.DialogInterface import android.content.Intent @@ -87,6 +88,8 @@ import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.UIHelper.toPx +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.createTvSidePanelDialog +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.layoutAsTvSidePanel private const val TAG = "HomeFragment" @@ -388,10 +391,14 @@ class HomeFragment : BaseFragment( validAPIs.add(0, noneApi) //val builder: AlertDialog.Builder = AlertDialog.Builder(this) //builder.setView(R.layout.home_select_mainpage) - val builder = - BottomSheetDialog(this) + val builder: Dialog = if (isLayout(TV)) { + createTvSidePanelDialog() + } else { + BottomSheetDialog(this).apply { + behavior.state = BottomSheetBehavior.STATE_EXPANDED + } + } - builder.behavior.state = BottomSheetBehavior.STATE_EXPANDED val binding: HomeSelectMainpageBinding = HomeSelectMainpageBinding.inflate( builder.layoutInflater, null, @@ -400,6 +407,7 @@ class HomeFragment : BaseFragment( builder.setContentView(binding.root) builder.show() + if (isLayout(TV)) builder.layoutAsTvSidePanel() builder.let { dialog -> val isMultiLang = getApiProviderLangSettings().let { set -> set.size > 1 || set.contains(AllLanguagesName) @@ -689,6 +697,15 @@ class HomeFragment : BaseFragment( true } homeChangeApi.setOnClickListener(apiChangeClickListener) + topBarProvider?.apply { + setOnClickListener(apiChangeClickListener) + setOnLongClickListener { + if (currentApiName == noneApi.name) return@setOnLongClickListener false + homeViewModel.loadAndCancel(currentApiName, forceReload = true, fromUI = true) + showToast(R.string.action_reload, Toast.LENGTH_SHORT) + true + } + } homeSwitchAccount.setOnClickListener { activity?.showAccountSelectLinear() } @@ -700,6 +717,7 @@ class HomeFragment : BaseFragment( homeMasterRecycler.adapter = homeMasterAdapter homeApiFab.isVisible = isLayout(PHONE) + homeApiHolder.isGone = isLayout(TV) homePreviewReloadProvider.setOnClickListener { homeViewModel.loadAndCancel( @@ -747,6 +765,11 @@ class HomeFragment : BaseFragment( } + // On TV the clock is placed below the top tab bar + val headerButtonsOffset = if (isLayout(TV)) { + resources.getDimensionPixelSize(R.dimen.tv_tab_bar_content_offset) - 10.toPx + } else -60.toPx + homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if (isLayout(PHONE)) { @@ -784,8 +807,8 @@ class HomeFragment : BaseFragment( // Hopefully getLocationInWindow acts correctly on all devices val rect = IntArray(2) view.getLocationInWindow(rect) - scrollParent.isVisible = true - scrollParent.translationY = rect[1].toFloat() - 60.toPx + scrollParent.isVisible = !isLayout(TV) + scrollParent.translationY = rect[1].toFloat() + headerButtonsOffset // Move the TV layout real time clock out of the way too // We check if we have the correct layout and if the clock is enabled @@ -795,7 +818,7 @@ class HomeFragment : BaseFragment( val rect = IntArray(2) view.getLocationInWindow(rect) scrollParent.isVisible = true - scrollParent.translationY = rect[1].toFloat() - 60.toPx + scrollParent.translationY = rect[1].toFloat() + headerButtonsOffset } } } @@ -826,6 +849,7 @@ class HomeFragment : BaseFragment( binding.apply { homeApiFab.text = displayApiName homeChangeApi.text = displayApiName + topBarProvider?.text = displayApiName homePreviewReloadProvider.isGone = (apiName == noneApi.name) homePreviewSearchButton.isGone = (apiName == noneApi.name) } @@ -979,6 +1003,10 @@ class HomeFragment : BaseFragment( }*/ } + /** The provider picker in the top tab bar of MainActivity, replaces home_api_holder on TV */ + private val topBarProvider: TextView? + get() = if (isLayout(TV)) activity?.findViewById(R.id.nav_top_provider) else null + private fun updateTvRailFocus() { if (!isLayout(TV or EMULATOR)) return // home_api_holder is glued to the header row's top by the mirror logic in @@ -1028,7 +1056,7 @@ class HomeFragment : BaseFragment( binding?.homeMasterRecycler?.scrollToPosition(0) // Defer focus request until after scroll ends binding?.homeChangeApi?.post { - binding?.homeChangeApi?.requestFocus() + (topBarProvider ?: binding?.homeChangeApi)?.requestFocus() } } // Case 2: Focus is on plugin selector or nearby buttons -> Move to home navigation diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 959806e566c..d6bec31cc9b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -7,9 +7,11 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView +import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView import androidx.core.content.ContextCompat +import androidx.core.view.children import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.lifecycle.LifecycleOwner @@ -58,10 +60,11 @@ import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectStringRes import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarMargin import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView -import com.lagradost.cloudstream3.utils.UIHelper.populateChips -import androidx.core.graphics.toColorInt +import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.ui.setRecycledViewPool +private const val MAX_TOP_SHELF_DOTS = 10 + class HomeParentItemAdapterPreview( private val viewModel: HomeViewModel, private val accountViewModel: AccountViewModel @@ -84,6 +87,8 @@ class HomeParentItemAdapterPreview( ) else FragmentHomeHeadBinding.inflate(inflater, parent, false) if (binding is FragmentHomeHeadTvBinding && isLayout(EMULATOR)) { + // Room for the provider buttons, which are drawn over the header + binding.root.setPadding(0, 60.toPx, 0, 0) binding.homeBookmarkParentItemMoreInfo.isVisible = true val marginInDp = 50 @@ -342,55 +347,7 @@ class HomeParentItemAdapterPreview( homePreviewDescription.isGone = item.plot.isNullOrBlank() homePreviewDescription.text = item.plot?.html() ?: "" - val scoreText = item.score?.toStringNull(0.1, 10, 1, false) - - scoreText?.let { score -> - homePreviewScore.text = - homePreviewScore.context.getString(R.string.extension_rating, score) - - // while it should never fail, we do this just in case - val rating = score.toDoubleOrNull() ?: item.score?.toDouble() ?: 0.0 - - val color = when { - rating < 5.0 -> "#eb2f2f".toColorInt() // Red - rating < 8.0 -> "#eda009".toColorInt() // Yellow - else -> "#3bb33b".toColorInt() // Green - } - homePreviewScore.backgroundTintList = - android.content.res.ColorStateList.valueOf(color) - } - homePreviewScore.isGone = scoreText == null - - item.year?.let { year -> - homePreviewYear.text = year.toString() - } - homePreviewYear.isGone = item.year == null - - val duration = item.duration - duration?.let { min -> - homePreviewDuration.text = - homePreviewDuration.context.getString(R.string.duration_format, min) - } - homePreviewDuration.isGone = duration == null || duration <= 0 - - val castText = item.actors?.take(3)?.joinToString(", ") { it.actor.name } - if (!castText.isNullOrBlank()) { - homePreviewCast.text = - homePreviewCast.context.getString(R.string.cast_format, castText) - homePreviewCast.isVisible = true - } else { - homePreviewCast.isVisible = false - } - homePreviewText.text = item.name.html() - populateChips( - homePreviewTags, - item.tags?.take(6) ?: emptyList(), - R.style.ChipFilledSemiTransparent, - null - ) - - bindLogo( url = item.logoUrl, headers = item.posterHeaders, @@ -398,8 +355,16 @@ class HomeParentItemAdapterPreview( logoView = homeBackgroundPosterWatermarkBadgeHolder ) - homePreviewTags.isGone = - item.tags.isNullOrEmpty() + homePreviewMeta.text = listOfNotNull( + item.year?.toString(), + item.duration?.takeIf { it > 0 }?.let { + homePreviewMeta.context.getString(R.string.duration_format, it) + }, + item.score?.toStringNull(0.1, 10, 1, false)?.let { "★ $it" }, + item.tags?.take(2)?.joinToString(", ")?.takeIf { it.isNotBlank() }, + ).joinToString(" · ") + homePreviewMeta.isGone = homePreviewMeta.text.isNullOrBlank() + updateTopShelfDots(homePreviewDots, position) homePreviewInfoBtt.setOnClickListener { view -> viewModel.click( @@ -476,6 +441,26 @@ class HomeParentItemAdapterPreview( } } + /** Page indicator under the top shelf, shows at most [MAX_TOP_SHELF_DOTS] dots */ + private fun updateTopShelfDots(dots: LinearLayout, position: Int) { + val count = minOf(previewAdapter.itemCount, MAX_TOP_SHELF_DOTS) + dots.isVisible = count > 1 + if (dots.childCount != count) { + dots.removeAllViews() + repeat(count) { + dots.addView(View(dots.context).apply { + setBackgroundResource(R.drawable.tv_top_shelf_dot) + layoutParams = LinearLayout.LayoutParams(7.toPx, 7.toPx).apply { + marginStart = 4.toPx + marginEnd = 4.toPx + } + }) + } + } + val selected = position.coerceAtMost(count - 1) + dots.children.forEachIndexed { index, dot -> dot.isSelected = index == selected } + } + private val previewCallback: ViewPager2.OnPageChangeCallback = object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { @@ -605,6 +590,8 @@ class HomeParentItemAdapterPreview( // as that causes higher android versions to stretch the ui when switching between shows var lastFocusTimeoutMs = 0L homePreviewInfoBtt.setOnFocusChangeListener { view, hasFocus -> + // The whole shelf is the button, the pill only shows that it is focused + homePreviewMoreInfo.isSelected = hasFocus val lastFocusMs = lastFocusTimeoutMs // Always reset timer, as we only want to update // it if we have not interacted in half a second @@ -625,9 +612,10 @@ class HomeParentItemAdapterPreview( if (!hasFocus) return@setOnFocusChangeListener if (previewViewpager.currentItem <= 0) { //Focus the Home item as the default focus will be the header item - (activity as? MainActivity)?.binding?.navRailView?.findViewById( - R.id.navigation_home - )?.requestFocus() + (activity?.findViewById(R.id.nav_top_home)?.takeIf { it.isShown } + ?: (activity as? MainActivity)?.binding?.navRailView?.findViewById( + R.id.navigation_home + ))?.requestFocus() } else { previewViewpager.setCurrentItem(previewViewpager.currentItem - 1, true) binding.homePreviewInfoBtt.requestFocus() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index c316fae1715..cc753f89217 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -62,11 +62,16 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.populateChips import com.lagradost.cloudstream3.utils.UIHelper.setNavigationBarColorCompat +import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.getImageFromDrawable import com.lagradost.cloudstream3.utils.setText import com.lagradost.cloudstream3.utils.setTextHtml import com.lagradost.cloudstream3.utils.txt +/** Scroll distance in dp after which the backdrop is fully dimmed */ +private const val BACKDROP_DIM_DISTANCE = 300 +private const val BACKDROP_MAX_DIM = 0.85f + class ResultFragmentTv : BaseFragment( BindingCreator.Inflate(FragmentResultTvBinding::inflate) ) { @@ -276,9 +281,11 @@ class ResultFragmentTv : BaseFragment( binding.apply { //episodesShadow.rotationX = 180.0f//if(episodesShadow.isRtl()) 180.0f else 0.0f - // parallax on background + // The backdrop stays in place and is darkened, the logo scrolls with the content resultFinishLoading.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { view, _, scrollY, _, oldScrollY -> - backgroundPosterHolder.translationY = -scrollY.toFloat() * 0.8f + backgroundPosterWatermarkBadgeHolder.translationY = -scrollY.toFloat() + backgroundPosterDim.alpha = + (scrollY / BACKDROP_DIM_DISTANCE.toPx.toFloat()).coerceIn(0.0f, BACKDROP_MAX_DIM) }) redirectToPlay.setOnFocusChangeListener { _, hasFocus -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt index fd99b8d4b06..69942e6b5c5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt @@ -324,7 +324,13 @@ object SearchResultBuilder { boxes[boxes.size - 1].setBackgroundResource(R.drawable.bg_color_bottom) } textIsDub?.apply { - backgroundTintList = ColorStateList.valueOf(context.colorFromAttribute(R.attr.textColor)) + if (isLayout(TV)) { + // Subtle translucent badges on TV, like on tvOS + backgroundTintList = ColorStateList.valueOf(context.getColor(R.color.tvBadgeBackground)) + setTextColor(context.getColor(R.color.whiteText)) + } else { + backgroundTintList = ColorStateList.valueOf(context.colorFromAttribute(R.attr.textColor)) + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragmentScreen.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragmentScreen.kt index c906b098279..8559432a5b2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragmentScreen.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragmentScreen.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.os.Bundle import androidx.compose.animation.Crossfade import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -16,6 +17,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState @@ -24,6 +26,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Scaffold @@ -71,9 +74,16 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.txt import com.lagradost.cloudstream4.compose.Screen import com.lagradost.cloudstream4.compose.TV +import com.lagradost.cloudstream4.compose.TvGlass +import com.lagradost.cloudstream4.compose.TvGlassCard +import com.lagradost.cloudstream4.compose.TvPageTitle import com.lagradost.cloudstream4.compose.circleBorder +import com.lagradost.cloudstream4.compose.editOnSelect import com.lagradost.cloudstream4.compose.focusOutline import com.lagradost.cloudstream4.compose.isLayout +import com.lagradost.cloudstream4.compose.tvContentWidth +import com.lagradost.cloudstream4.compose.tvPageBackground +import com.lagradost.cloudstream4.compose.tvScaffoldContainerColor import com.lagradost.cloudstream4.theme.CloudStreamPreviewTheme import com.mihon.material.padding import com.mihon.presentation.settings.SearchableSettings @@ -197,10 +207,27 @@ object SettingsFragmentScreen : Screen { } } - Scaffold { _ -> - Column(modifier = Modifier.verticalScroll(outerListState)) { + val isTv = isLayout(TV) + Scaffold( + modifier = Modifier.tvPageBackground(), + containerColor = tvScaffoldContainerColor(), + contentColor = MaterialTheme.colorScheme.onBackground, + ) { _ -> + Column(modifier = Modifier.tvContentWidth().verticalScroll(outerListState)) { Spacer(modifier = Modifier.height(MaterialTheme.padding.small)) + if (isTv) { + Text( + text = stringResource(R.string.title_settings), + style = LocalTextStyle.current.merge(TvPageTitle), + modifier = Modifier.padding( + start = MaterialTheme.padding.medium, + top = 28.dp, + bottom = 16.dp + ), + ) + } + val default = DataStoreHelper.getDefaultAccount( LocalContext.current ) @@ -212,7 +239,12 @@ object SettingsFragmentScreen : Screen { Row( modifier = Modifier .fillMaxSize() - .focusOutline() + .then( + if (isTv) { + val card = RoundedCornerShape(20.dp) + Modifier.focusOutline(shape = card).background(TvGlassCard, card) + } else Modifier.focusOutline() + ) .clickable { activity.navigate( R.id.accountSelectActivity, @@ -241,7 +273,8 @@ object SettingsFragmentScreen : Screen { style = MaterialTheme.typography.titleLarge, ) Text( - text = stringResource(R.string.title_settings), + // The page title already says settings on TV + text = stringResource(if (isTv) R.string.switch_account else R.string.title_settings), style = MaterialTheme.typography.bodyMedium, ) } @@ -290,6 +323,9 @@ object SettingsFragmentScreen : Screen { @Composable fun SettingsSearch(textFieldState : TextFieldState) { var hasFocus by remember { mutableStateOf(false) } + val isTv = isLayout(TV) + val isEditing = remember { mutableStateOf(!isTv) } + val containerColor = if (isTv) TvGlass else MaterialTheme.colorScheme.surfaceVariant val focusProgress by animateFloatAsState(targetValue = if (hasFocus) 1.0f else 0.0f) val focusManager = LocalFocusManager.current val focusRequester = remember { FocusRequester() } @@ -306,14 +342,17 @@ object SettingsFragmentScreen : Screen { .focusOutline(enabled = isLayout(TV), CircleShape) .onFocusChanged { newFocus -> hasFocus = newFocus.hasFocus - }.focusRequester(focusRequester), + } + .editOnSelect(isEditing) + .focusRequester(focusRequester), + readOnly = !isEditing.value, placeholder = { Text(text = stringResource(R.string.search_hint)) }, shape = CircleShape, colors = TextFieldDefaults.colors( - focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant, - unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant, + focusedContainerColor = containerColor, + unfocusedContainerColor = containerColor, unfocusedLeadingIconColor = MaterialTheme.colorScheme.onBackground, unfocusedTrailingIconColor = MaterialTheme.colorScheme.onBackground, focusedTrailingIconColor = MaterialTheme.colorScheme.onBackground, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt index 26c710103fa..785b634952e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt @@ -2,9 +2,12 @@ package com.lagradost.cloudstream3.utils import android.app.Activity import android.app.Dialog +import android.content.Context import android.text.Spanned +import android.view.Gravity import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import android.widget.AbsListView import android.widget.ArrayAdapter import android.widget.LinearLayout @@ -288,6 +291,23 @@ object SingleSelectionHelper { ) } + /** Width of the side panel dialogs on TV, relative to the screen */ + private const val TV_SIDE_PANEL_WIDTH = 0.36f + + /** A tvOS-like floating panel on the right side of the screen, used instead of bottom sheets on TV */ + fun Context.createTvSidePanelDialog(): Dialog = Dialog(this, R.style.TvSidePanelDialog) + + /** Must be called after [Dialog.show] as the window is only laid out then */ + fun Dialog.layoutAsTvSidePanel() { + window?.apply { + setGravity(Gravity.END) + setLayout( + (context.resources.displayMetrics.widthPixels * TV_SIDE_PANEL_WIDTH).toInt(), + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + } + /** Only for a low amount of items */ fun Activity?.showBottomDialog( items: List, @@ -303,11 +323,11 @@ object SingleSelectionHelper { LayoutInflater.from(this) ) - val builder = - BottomSheetDialog(this) + val builder = if (isLayout(TV)) createTvSidePanelDialog() else BottomSheetDialog(this) builder.setContentView(binding.root) builder.show() + if (isLayout(TV)) builder.layoutAsTvSidePanel() showDialog( binding, builder, diff --git a/app/src/main/res/anim/tv_panel_enter.xml b/app/src/main/res/anim/tv_panel_enter.xml new file mode 100644 index 00000000000..ca48210bc28 --- /dev/null +++ b/app/src/main/res/anim/tv_panel_enter.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/anim/tv_panel_exit.xml b/app/src/main/res/anim/tv_panel_exit.xml new file mode 100644 index 00000000000..3410b945f4a --- /dev/null +++ b/app/src/main/res/anim/tv_panel_exit.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/color-television/chip_color.xml b/app/src/main/res/color-television/chip_color.xml new file mode 100644 index 00000000000..cadde8e4c64 --- /dev/null +++ b/app/src/main/res/color-television/chip_color.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/color-television/chip_color_text.xml b/app/src/main/res/color-television/chip_color_text.xml new file mode 100644 index 00000000000..b8bd1fc579e --- /dev/null +++ b/app/src/main/res/color-television/chip_color_text.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/color/tv_tab_item_text.xml b/app/src/main/res/color/tv_tab_item_text.xml new file mode 100644 index 00000000000..76e7c80aac7 --- /dev/null +++ b/app/src/main/res/color/tv_tab_item_text.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/color/tv_top_shelf_button_text.xml b/app/src/main/res/color/tv_top_shelf_button_text.xml new file mode 100644 index 00000000000..c5ce76af8f6 --- /dev/null +++ b/app/src/main/res/color/tv_top_shelf_button_text.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable-television/dialog__window_background.xml b/app/src/main/res/drawable-television/dialog__window_background.xml new file mode 100644 index 00000000000..14334ef9948 --- /dev/null +++ b/app/src/main/res/drawable-television/dialog__window_background.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/app/src/main/res/drawable-television/outline_drawable.xml b/app/src/main/res/drawable-television/outline_drawable.xml new file mode 100644 index 00000000000..448629aaa47 --- /dev/null +++ b/app/src/main/res/drawable-television/outline_drawable.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable-television/outline_drawable_forced.xml b/app/src/main/res/drawable-television/outline_drawable_forced.xml new file mode 100644 index 00000000000..bc6a9b78cb8 --- /dev/null +++ b/app/src/main/res/drawable-television/outline_drawable_forced.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable-television/outline_drawable_less_inset.xml b/app/src/main/res/drawable-television/outline_drawable_less_inset.xml new file mode 100644 index 00000000000..5f70fe1cbfe --- /dev/null +++ b/app/src/main/res/drawable-television/outline_drawable_less_inset.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/drawable/tv_app_background.xml b/app/src/main/res/drawable/tv_app_background.xml new file mode 100644 index 00000000000..0af8ad20026 --- /dev/null +++ b/app/src/main/res/drawable/tv_app_background.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/tv_focus_highlight.xml b/app/src/main/res/drawable/tv_focus_highlight.xml new file mode 100644 index 00000000000..6baa25f37cd --- /dev/null +++ b/app/src/main/res/drawable/tv_focus_highlight.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/tv_glass_card_bg.xml b/app/src/main/res/drawable/tv_glass_card_bg.xml new file mode 100644 index 00000000000..471416a3c3d --- /dev/null +++ b/app/src/main/res/drawable/tv_glass_card_bg.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/tv_pill_button.xml b/app/src/main/res/drawable/tv_pill_button.xml new file mode 100644 index 00000000000..c1489200315 --- /dev/null +++ b/app/src/main/res/drawable/tv_pill_button.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/tv_pill_focus.xml b/app/src/main/res/drawable/tv_pill_focus.xml new file mode 100644 index 00000000000..ec8cea980d8 --- /dev/null +++ b/app/src/main/res/drawable/tv_pill_focus.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/tv_result_backdrop_gradient.xml b/app/src/main/res/drawable/tv_result_backdrop_gradient.xml new file mode 100644 index 00000000000..678198ace92 --- /dev/null +++ b/app/src/main/res/drawable/tv_result_backdrop_gradient.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/tv_search_field_bg.xml b/app/src/main/res/drawable/tv_search_field_bg.xml new file mode 100644 index 00000000000..a70bba66122 --- /dev/null +++ b/app/src/main/res/drawable/tv_search_field_bg.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/tv_search_suggestions_bg.xml b/app/src/main/res/drawable/tv_search_suggestions_bg.xml new file mode 100644 index 00000000000..30b0246a5bd --- /dev/null +++ b/app/src/main/res/drawable/tv_search_suggestions_bg.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/tv_side_panel_bg.xml b/app/src/main/res/drawable/tv_side_panel_bg.xml new file mode 100644 index 00000000000..8fee20a3667 --- /dev/null +++ b/app/src/main/res/drawable/tv_side_panel_bg.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/app/src/main/res/drawable/tv_tab_bar_bg.xml b/app/src/main/res/drawable/tv_tab_bar_bg.xml new file mode 100644 index 00000000000..981b1b2b0a3 --- /dev/null +++ b/app/src/main/res/drawable/tv_tab_bar_bg.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/tv_tab_item_bg.xml b/app/src/main/res/drawable/tv_tab_item_bg.xml new file mode 100644 index 00000000000..a0e39861031 --- /dev/null +++ b/app/src/main/res/drawable/tv_tab_item_bg.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/tv_top_glow.xml b/app/src/main/res/drawable/tv_top_glow.xml new file mode 100644 index 00000000000..c2eccd08f3c --- /dev/null +++ b/app/src/main/res/drawable/tv_top_glow.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/tv_top_shelf_button_bg.xml b/app/src/main/res/drawable/tv_top_shelf_button_bg.xml new file mode 100644 index 00000000000..2863a3bd084 --- /dev/null +++ b/app/src/main/res/drawable/tv_top_shelf_button_bg.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/tv_top_shelf_dot.xml b/app/src/main/res/drawable/tv_top_shelf_dot.xml new file mode 100644 index 00000000000..a2c89d82de6 --- /dev/null +++ b/app/src/main/res/drawable/tv_top_shelf_dot.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/tv_top_shelf_focus.xml b/app/src/main/res/drawable/tv_top_shelf_focus.xml new file mode 100644 index 00000000000..14adca89732 --- /dev/null +++ b/app/src/main/res/drawable/tv_top_shelf_focus.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/tv_top_shelf_gradient.xml b/app/src/main/res/drawable/tv_top_shelf_gradient.xml new file mode 100644 index 00000000000..7bf9160a54b --- /dev/null +++ b/app/src/main/res/drawable/tv_top_shelf_gradient.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/font-television/google_sans.xml b/app/src/main/res/font-television/google_sans.xml new file mode 100644 index 00000000000..64d0c179cb5 --- /dev/null +++ b/app/src/main/res/font-television/google_sans.xml @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/app/src/main/res/font/inter_bold.ttf b/app/src/main/res/font/inter_bold.ttf new file mode 100644 index 00000000000..15e908f2369 Binary files /dev/null and b/app/src/main/res/font/inter_bold.ttf differ diff --git a/app/src/main/res/font/inter_medium.ttf b/app/src/main/res/font/inter_medium.ttf new file mode 100644 index 00000000000..93427308530 Binary files /dev/null and b/app/src/main/res/font/inter_medium.ttf differ diff --git a/app/src/main/res/font/inter_regular.ttf b/app/src/main/res/font/inter_regular.ttf new file mode 100644 index 00000000000..c544be47844 Binary files /dev/null and b/app/src/main/res/font/inter_regular.ttf differ diff --git a/app/src/main/res/font/inter_semibold.ttf b/app/src/main/res/font/inter_semibold.ttf new file mode 100644 index 00000000000..a32e2b8d219 Binary files /dev/null and b/app/src/main/res/font/inter_semibold.ttf differ diff --git a/app/src/main/res/layout-television/fragment_download_queue.xml b/app/src/main/res/layout-television/fragment_download_queue.xml new file mode 100644 index 00000000000..48c7fe42da0 --- /dev/null +++ b/app/src/main/res/layout-television/fragment_download_queue.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-television/fragment_downloads.xml b/app/src/main/res/layout-television/fragment_downloads.xml new file mode 100644 index 00000000000..cc41c599459 --- /dev/null +++ b/app/src/main/res/layout-television/fragment_downloads.xml @@ -0,0 +1,411 @@ + + + + + + + + + + + + +