Bring Android TV back (#141)

* Merge MainActivity and MainActivityTv

* Merge AddServerFragment and TvAddServerFragment

* Merge LoginFragment and TvLoginFragment

* Add new focus effect

* Add libraries to tv home

* Fix home empty when navigating back on mobile

* Add loading indicator to home fragment

* Add empty LibraryFragment

* Add focus outline to settings button

* Use DiffCallback for updating home fragment

* Visually upgrade MediaDetailFragment

* Make all home items focusable in touch mode

* Add new focus border to person item

* Add LibraryFragment layout for TV

(Whilst also making a clusterfuck of the navigation)

* Add missing try-catch in HomeViewModel

* Don't show CancellationException on AddServerFragment

* Fix a few crashes plus errors
This commit is contained in:
Jarne Demeulemeester 2022-08-20 14:41:38 +02:00 committed by GitHub
parent bde1e44174
commit 3b7473b7a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1060 additions and 771 deletions

View file

@ -36,19 +36,6 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivityTv"
android:exported="true"
android:theme="@style/Theme.Jellyfin.Tv"
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </intent-filter>

View file

@ -1,69 +1,86 @@
package dev.jdtech.jellyfin package dev.jdtech.jellyfin
import android.app.UiModeManager
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.NavigationUI
import androidx.navigation.ui.NavigationUiSaveStateControl
import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupActionBarWithNavController
import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationBarView
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.database.ServerDatabaseDao import dev.jdtech.jellyfin.database.ServerDatabaseDao
import dev.jdtech.jellyfin.databinding.ActivityMainAppBinding import dev.jdtech.jellyfin.databinding.ActivityMainBinding
import dev.jdtech.jellyfin.utils.loadDownloadLocation import dev.jdtech.jellyfin.utils.loadDownloadLocation
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainAppBinding private lateinit var binding: ActivityMainBinding
private lateinit var uiModeManager: UiModeManager
@Inject @Inject
lateinit var database: ServerDatabaseDao lateinit var database: ServerDatabaseDao
@OptIn(NavigationUiSaveStateControl::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMainAppBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
uiModeManager = getSystemService(UI_MODE_SERVICE) as UiModeManager
setContentView(binding.root) setContentView(binding.root)
val navView: NavigationBarView = binding.navView as NavigationBarView
val navHostFragment = val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment_activity_main) as NavHostFragment supportFragmentManager.findFragmentById(R.id.nav_host_fragment_activity_main) as NavHostFragment
setSupportActionBar(binding.mainToolbar)
val navController = navHostFragment.navController val navController = navHostFragment.navController
val inflater = navController.navInflater
val graph = inflater.inflate(R.navigation.app_navigation)
if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
graph.setStartDestination(R.id.homeFragmentTv)
}
val nServers = database.getServersCount() val nServers = database.getServersCount()
if (nServers < 1) { if (nServers < 1) {
val inflater = navController.navInflater
val graph = inflater.inflate(R.navigation.app_navigation)
graph.setStartDestination(R.id.addServerFragment) graph.setStartDestination(R.id.addServerFragment)
navController.setGraph(graph, intent.extras)
} }
// Passing each menu ID as a set of Ids because each navController.setGraph(graph, intent.extras)
// menu should be considered as top level destinations.
val appBarConfiguration = AppBarConfiguration( if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_TELEVISION) {
setOf( val navView: NavigationBarView = binding.navView as NavigationBarView
R.id.homeFragment, R.id.mediaFragment, R.id.favoriteFragment, R.id.downloadFragment
setSupportActionBar(binding.mainToolbar)
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
val appBarConfiguration = AppBarConfiguration(
setOf(
R.id.homeFragment,
R.id.mediaFragment,
R.id.favoriteFragment,
R.id.downloadFragment
)
) )
)
setupActionBarWithNavController(navController, appBarConfiguration) setupActionBarWithNavController(navController, appBarConfiguration)
// navView.setupWithNavController(navController) // navView.setupWithNavController(navController)
// Don't save the state of other main navigation items, only this experimental function allows turning off this behavior // Don't save the state of other main navigation items, only this experimental function allows turning off this behavior
NavigationUI.setupWithNavController(navView, navController, false) NavigationUI.setupWithNavController(navView, navController, false)
navController.addOnDestinationChangedListener { _, destination, _ -> navController.addOnDestinationChangedListener { _, destination, _ ->
binding.navView.visibility = when (destination.id) { binding.navView!!.visibility = when (destination.id) {
R.id.twoPaneSettingsFragment, R.id.serverSelectFragment, R.id.addServerFragment, R.id.loginFragment, R.id.about_libraries_dest -> View.GONE R.id.twoPaneSettingsFragment, R.id.serverSelectFragment, R.id.addServerFragment, R.id.loginFragment, R.id.about_libraries_dest -> View.GONE
else -> View.VISIBLE else -> View.VISIBLE
}
if (destination.id == R.id.about_libraries_dest) binding.mainToolbar?.title =
getString(R.string.app_info)
} }
if (destination.id == R.id.about_libraries_dest) binding.mainToolbar.title = getString(R.string.app_info)
} }
loadDownloadLocation(applicationContext) loadDownloadLocation(applicationContext)

View file

@ -1,37 +0,0 @@
package dev.jdtech.jellyfin
import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.NavHostFragment
import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.database.ServerDatabaseDao
import dev.jdtech.jellyfin.databinding.ActivityMainTvBinding
import dev.jdtech.jellyfin.utils.loadDownloadLocation
import javax.inject.Inject
@AndroidEntryPoint
internal class MainActivityTv : FragmentActivity() {
private lateinit var binding: ActivityMainTvBinding
@Inject
lateinit var database: ServerDatabaseDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainTvBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.tv_nav_host) as NavHostFragment
val navController = navHostFragment.navController
val nServers = database.getServersCount()
if (nServers < 1) {
val inflater = navController.navInflater
val graph = inflater.inflate(R.navigation.tv_navigation)
graph.setStartDestination(R.id.addServerTvFragment)
navController.setGraph(graph, intent.extras)
}
loadDownloadLocation(applicationContext)
}
}

View file

@ -94,6 +94,7 @@ class ViewListAdapter(
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
return when (getItem(position)) { return when (getItem(position)) {
is HomeItem.Libraries -> -1
is HomeItem.Section -> ITEM_VIEW_TYPE_NEXT_UP is HomeItem.Section -> ITEM_VIEW_TYPE_NEXT_UP
is HomeItem.ViewItem -> ITEM_VIEW_TYPE_VIEW is HomeItem.ViewItem -> ITEM_VIEW_TYPE_VIEW
} }
@ -105,6 +106,10 @@ class ViewListAdapter(
} }
sealed class HomeItem { sealed class HomeItem {
data class Libraries(val section: HomeSection) : HomeItem() {
override val id = section.id
}
data class Section(val homeSection: HomeSection) : HomeItem() { data class Section(val homeSection: HomeSection) : HomeItem() {
override val id = homeSection.id override val id = homeSection.id
} }

View file

@ -1,10 +1,14 @@
package dev.jdtech.jellyfin.fragments package dev.jdtech.jellyfin.fragments
import android.app.UiModeManager
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
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.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
@ -22,6 +26,7 @@ import timber.log.Timber
class AddServerFragment : Fragment() { class AddServerFragment : Fragment() {
private lateinit var binding: FragmentAddServerBinding private lateinit var binding: FragmentAddServerBinding
private lateinit var uiModeManager: UiModeManager
private val viewModel: AddServerViewModel by viewModels() private val viewModel: AddServerViewModel by viewModels()
override fun onCreateView( override fun onCreateView(
@ -29,8 +34,10 @@ class AddServerFragment : Fragment() {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = FragmentAddServerBinding.inflate(inflater) binding = FragmentAddServerBinding.inflate(inflater)
uiModeManager =
requireContext().getSystemService(AppCompatActivity.UI_MODE_SERVICE) as UiModeManager
binding.editTextServerAddress.setOnEditorActionListener { _, actionId, _ -> (binding.editTextServerAddress as AppCompatEditText).setOnEditorActionListener { _, actionId, _ ->
return@setOnEditorActionListener when (actionId) { return@setOnEditorActionListener when (actionId) {
EditorInfo.IME_ACTION_GO -> { EditorInfo.IME_ACTION_GO -> {
connectToServer() connectToServer()
@ -56,6 +63,7 @@ class AddServerFragment : Fragment() {
} }
} }
} }
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.navigateToLogin.collect { viewModel.navigateToLogin.collect {
@ -77,17 +85,25 @@ class AddServerFragment : Fragment() {
private fun bindUiStateError(uiState: AddServerViewModel.UiState.Error) { private fun bindUiStateError(uiState: AddServerViewModel.UiState.Error) {
binding.buttonConnect.isEnabled = true binding.buttonConnect.isEnabled = true
binding.progressCircular.isVisible = false binding.progressCircular.isVisible = false
binding.editTextServerAddressLayout.error = uiState.message if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
(binding.editTextServerAddress as AppCompatEditText).error = uiState.message
} else {
binding.editTextServerAddressLayout!!.error = uiState.message
}
} }
private fun bindUiStateLoading() { private fun bindUiStateLoading() {
binding.buttonConnect.isEnabled = false binding.buttonConnect.isEnabled = false
binding.progressCircular.isVisible = true binding.progressCircular.isVisible = true
binding.editTextServerAddressLayout.error = null if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
(binding.editTextServerAddress as AppCompatEditText).error = null
} else {
binding.editTextServerAddressLayout!!.error = null
}
} }
private fun connectToServer() { private fun connectToServer() {
val serverAddress = binding.editTextServerAddress.text.toString() val serverAddress = (binding.editTextServerAddress as AppCompatEditText).text.toString()
viewModel.checkServer(serverAddress.removeSuffix("/")) viewModel.checkServer(serverAddress.removeSuffix("/"))
} }

View file

@ -77,12 +77,12 @@ class HomeFragment : Fragment() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
viewModel.refreshData() viewModel.loadData()
} }
private fun setupView() { private fun setupView() {
binding.refreshLayout.setOnRefreshListener { binding.refreshLayout.setOnRefreshListener {
viewModel.refreshData() viewModel.loadData()
} }
binding.viewsRecyclerView.adapter = ViewListAdapter( binding.viewsRecyclerView.adapter = ViewListAdapter(
@ -100,7 +100,7 @@ class HomeFragment : Fragment() {
}) })
binding.errorLayout.errorRetryButton.setOnClickListener { binding.errorLayout.errorRetryButton.setOnClickListener {
viewModel.refreshData() viewModel.loadData()
} }
binding.errorLayout.errorDetailsButton.setOnClickListener { binding.errorLayout.errorDetailsButton.setOnClickListener {
@ -187,7 +187,7 @@ class HomeFragment : Fragment() {
private fun navigateToSettingsFragment() { private fun navigateToSettingsFragment() {
findNavController().navigate( findNavController().navigate(
HomeFragmentDirections.actionNavigationHomeToNavigationSettings() HomeFragmentDirections.actionHomeFragmentToSettingsFragment()
) )
} }
} }

View file

@ -1,8 +1,11 @@
package dev.jdtech.jellyfin.fragments package dev.jdtech.jellyfin.fragments
import android.app.UiModeManager
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.MenuHost import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -14,6 +17,7 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.recyclerview.widget.LinearSnapHelper
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.viewmodels.LibraryViewModel import dev.jdtech.jellyfin.viewmodels.LibraryViewModel
@ -33,6 +37,7 @@ import javax.inject.Inject
class LibraryFragment : Fragment() { class LibraryFragment : Fragment() {
private lateinit var binding: FragmentLibraryBinding private lateinit var binding: FragmentLibraryBinding
private lateinit var uiModeManager: UiModeManager
private val viewModel: LibraryViewModel by viewModels() private val viewModel: LibraryViewModel by viewModels()
private val args: LibraryFragmentArgs by navArgs() private val args: LibraryFragmentArgs by navArgs()
@ -46,6 +51,8 @@ class LibraryFragment : Fragment() {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = FragmentLibraryBinding.inflate(inflater, container, false) binding = FragmentLibraryBinding.inflate(inflater, container, false)
uiModeManager =
requireContext().getSystemService(AppCompatActivity.UI_MODE_SERVICE) as UiModeManager
return binding.root return binding.root
} }
@ -91,6 +98,8 @@ class LibraryFragment : Fragment() {
}, viewLifecycleOwner, Lifecycle.State.RESUMED }, viewLifecycleOwner, Lifecycle.State.RESUMED
) )
binding.title?.text = args.libraryName
binding.errorLayout.errorRetryButton.setOnClickListener { binding.errorLayout.errorRetryButton.setOnClickListener {
viewModel.loadItems(args.libraryId, args.libraryType) viewModel.loadItems(args.libraryId, args.libraryType)
} }
@ -102,6 +111,11 @@ class LibraryFragment : Fragment() {
) )
} }
if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
val snapHelper = LinearSnapHelper()
snapHelper.attachToRecyclerView(binding.itemsRecyclerView)
}
binding.itemsRecyclerView.adapter = binding.itemsRecyclerView.adapter =
ViewItemPagingAdapter(ViewItemPagingAdapter.OnClickListener { item -> ViewItemPagingAdapter(ViewItemPagingAdapter.OnClickListener { item ->
navigateToMediaInfoFragment(item) navigateToMediaInfoFragment(item)
@ -182,12 +196,22 @@ class LibraryFragment : Fragment() {
} }
private fun navigateToMediaInfoFragment(item: BaseItemDto) { private fun navigateToMediaInfoFragment(item: BaseItemDto) {
findNavController().navigate( if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
LibraryFragmentDirections.actionLibraryFragmentToMediaInfoFragment( findNavController().navigate(
item.id, LibraryFragmentDirections.actionLibraryFragmentToMediaDetailFragment(
item.name, item.id,
item.type item.name,
item.type
)
) )
) } else {
findNavController().navigate(
LibraryFragmentDirections.actionLibraryFragmentToMediaInfoFragment(
item.id,
item.name,
item.type
)
)
}
} }
} }

View file

@ -1,10 +1,14 @@
package dev.jdtech.jellyfin.fragments package dev.jdtech.jellyfin.fragments
import android.app.UiModeManager
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
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.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
@ -22,6 +26,7 @@ import timber.log.Timber
class LoginFragment : Fragment() { class LoginFragment : Fragment() {
private lateinit var binding: FragmentLoginBinding private lateinit var binding: FragmentLoginBinding
private lateinit var uiModeManager: UiModeManager
private val viewModel: LoginViewModel by viewModels() private val viewModel: LoginViewModel by viewModels()
override fun onCreateView( override fun onCreateView(
@ -29,8 +34,10 @@ class LoginFragment : Fragment() {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = FragmentLoginBinding.inflate(inflater) binding = FragmentLoginBinding.inflate(inflater)
uiModeManager =
requireContext().getSystemService(AppCompatActivity.UI_MODE_SERVICE) as UiModeManager
binding.editTextPassword.setOnEditorActionListener { _, actionId, _ -> (binding.editTextPassword as AppCompatEditText).setOnEditorActionListener { _, actionId, _ ->
return@setOnEditorActionListener when (actionId) { return@setOnEditorActionListener when (actionId) {
EditorInfo.IME_ACTION_GO -> { EditorInfo.IME_ACTION_GO -> {
login() login()
@ -61,7 +68,7 @@ class LoginFragment : Fragment() {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.navigateToMain.collect { viewModel.navigateToMain.collect {
if (it) { if (it) {
navigateToMainActivity() navigateToHomeFragment()
} }
} }
} }
@ -78,22 +85,34 @@ class LoginFragment : Fragment() {
private fun bindUiStateError(uiState: LoginViewModel.UiState.Error) { private fun bindUiStateError(uiState: LoginViewModel.UiState.Error) {
binding.buttonLogin.isEnabled = true binding.buttonLogin.isEnabled = true
binding.progressCircular.isVisible = false binding.progressCircular.isVisible = false
binding.editTextUsernameLayout.error = uiState.message if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
(binding.editTextUsername as AppCompatEditText).error = uiState.message
} else {
binding.editTextUsernameLayout!!.error = uiState.message
}
} }
private fun bindUiStateLoading() { private fun bindUiStateLoading() {
binding.buttonLogin.isEnabled = false binding.buttonLogin.isEnabled = false
binding.progressCircular.isVisible = true binding.progressCircular.isVisible = true
binding.editTextUsernameLayout.error = null if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
(binding.editTextUsername as AppCompatEditText).error = null
} else {
binding.editTextUsernameLayout!!.error = null
}
} }
private fun login() { private fun login() {
val username = binding.editTextUsername.text.toString() val username = (binding.editTextUsername as AppCompatEditText).text.toString()
val password = binding.editTextPassword.text.toString() val password = (binding.editTextPassword as AppCompatEditText).text.toString()
viewModel.login(username, password) viewModel.login(username, password)
} }
private fun navigateToMainActivity() { private fun navigateToHomeFragment() {
findNavController().navigate(LoginFragmentDirections.actionLoginFragmentToNavigationHome()) if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
findNavController().navigate(LoginFragmentDirections.actionLoginFragmentToHomeFragmentTv())
} else {
findNavController().navigate(LoginFragmentDirections.actionLoginFragmentToHomeFragment())
}
} }
} }

View file

@ -1,16 +1,16 @@
package dev.jdtech.jellyfin.tv.ui package dev.jdtech.jellyfin.tv.ui
import android.app.UiModeManager
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent.KEYCODE_DPAD_DOWN import android.view.KeyEvent.KEYCODE_DPAD_DOWN
import android.view.KeyEvent.KEYCODE_DPAD_DOWN_LEFT import android.view.KeyEvent.KEYCODE_DPAD_DOWN_LEFT
import android.view.View import android.view.View
import android.widget.ImageButton import android.widget.ImageButton
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.leanback.app.BrowseSupportFragment import androidx.leanback.app.BrowseSupportFragment
import androidx.leanback.widget.ArrayObjectAdapter import androidx.leanback.widget.*
import androidx.leanback.widget.HeaderItem
import androidx.leanback.widget.ListRow
import androidx.leanback.widget.ListRowPresenter
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
@ -18,6 +18,7 @@ import androidx.navigation.fragment.findNavController
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.adapters.HomeItem import dev.jdtech.jellyfin.adapters.HomeItem
import dev.jdtech.jellyfin.fragments.HomeFragmentDirections
import dev.jdtech.jellyfin.viewmodels.HomeViewModel import dev.jdtech.jellyfin.viewmodels.HomeViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
@ -29,12 +30,21 @@ internal class HomeFragment : BrowseSupportFragment() {
private val viewModel: HomeViewModel by viewModels() private val viewModel: HomeViewModel by viewModels()
private lateinit var rowsAdapter: ArrayObjectAdapter private lateinit var rowsAdapter: ArrayObjectAdapter
private lateinit var uiModeManager: UiModeManager
private val adapterMap = mutableMapOf<String, ArrayObjectAdapter>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
uiModeManager =
requireContext().getSystemService(AppCompatActivity.UI_MODE_SERVICE) as UiModeManager
val rowPresenter = ListRowPresenter()
rowPresenter.selectEffectEnabled = false
headersState = HEADERS_ENABLED headersState = HEADERS_ENABLED
rowsAdapter = ArrayObjectAdapter(ListRowPresenter()) rowsAdapter = ArrayObjectAdapter(rowPresenter)
adapter = rowsAdapter adapter = rowsAdapter
} }
@ -59,21 +69,42 @@ internal class HomeFragment : BrowseSupportFragment() {
Timber.d("$uiState") Timber.d("$uiState")
when (uiState) { when (uiState) {
is HomeViewModel.UiState.Normal -> bindUiStateNormal(uiState) is HomeViewModel.UiState.Normal -> bindUiStateNormal(uiState)
is HomeViewModel.UiState.Loading -> Unit is HomeViewModel.UiState.Loading -> bindUiStateLoading()
is HomeViewModel.UiState.Error -> Unit is HomeViewModel.UiState.Error -> Unit
} }
} }
} }
} }
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.loadData(includeLibraries = true)
}
}
}
private val diffCallbackListRow = object : DiffCallback<ListRow>() {
override fun areItemsTheSame(oldItem: ListRow, newItem: ListRow): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ListRow, newItem: ListRow): Boolean {
Timber.d((oldItem.adapter.size() == newItem.adapter.size()).toString())
return oldItem.adapter.size() == newItem.adapter.size()
}
} }
private fun bindUiStateNormal(uiState: HomeViewModel.UiState.Normal) { private fun bindUiStateNormal(uiState: HomeViewModel.UiState.Normal) {
progressBarManager.hide()
uiState.apply { uiState.apply {
rowsAdapter.clear() rowsAdapter.setItems(homeItems.map { homeItem -> homeItem.toListRow() }, diffCallbackListRow)
homeItems.map { section -> rowsAdapter.add(section.toListRow()) }
} }
} }
private fun bindUiStateLoading() {
progressBarManager.show()
}
private fun HomeItem.toListRow(): ListRow { private fun HomeItem.toListRow(): ListRow {
return ListRow( return ListRow(
toHeader(), toHeader(),
@ -83,6 +114,7 @@ internal class HomeFragment : BrowseSupportFragment() {
private fun HomeItem.toHeader(): HeaderItem { private fun HomeItem.toHeader(): HeaderItem {
return when (this) { return when (this) {
is HomeItem.Libraries -> HeaderItem(section.name)
is HomeItem.Section -> HeaderItem(homeSection.name) is HomeItem.Section -> HeaderItem(homeSection.name)
is HomeItem.ViewItem -> HeaderItem( is HomeItem.ViewItem -> HeaderItem(
String.format( String.format(
@ -93,14 +125,59 @@ internal class HomeFragment : BrowseSupportFragment() {
} }
} }
val diffCallback = object : DiffCallback<BaseItemDto>() {
override fun areItemsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean {
return oldItem == newItem
}
}
private fun HomeItem.toItems(): ArrayObjectAdapter { private fun HomeItem.toItems(): ArrayObjectAdapter {
return when (this) { val name = this.toHeader().name
is HomeItem.Section -> ArrayObjectAdapter(DynamicMediaItemPresenter { item -> val items = when (this) {
navigateToMediaDetailFragment(item) is HomeItem.Libraries -> section.items
}).apply { addAll(0, homeSection.items) } is HomeItem.Section -> homeSection.items
is HomeItem.ViewItem -> ArrayObjectAdapter(MediaItemPresenter { item -> is HomeItem.ViewItem -> view.items
navigateToMediaDetailFragment(item) }
}).apply { addAll(0, view.items) } if (name in adapterMap) {
adapterMap[name]?.setItems(items, diffCallback)
} else {
adapterMap[name] = when (this) {
is HomeItem.Libraries -> ArrayObjectAdapter(LibaryItemPresenter { item ->
navigateToLibraryFragment(item)
}).apply { setItems(items, diffCallback) }
is HomeItem.Section -> ArrayObjectAdapter(DynamicMediaItemPresenter { item ->
navigateToMediaDetailFragment(item)
}).apply { setItems(items, diffCallback) }
is HomeItem.ViewItem -> ArrayObjectAdapter(MediaItemPresenter { item ->
navigateToMediaDetailFragment(item)
}).apply { setItems(items, diffCallback) }
}
}
return adapterMap[name]!!
}
private fun navigateToLibraryFragment(library: BaseItemDto) {
if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
findNavController().navigate(
dev.jdtech.jellyfin.tv.ui.HomeFragmentDirections.actionHomeFragmentToLibraryFragment(
library.id,
library.name,
library.collectionType
)
)
} else {
findNavController().navigate(
HomeFragmentDirections.actionNavigationHomeToLibraryFragment(
library.id,
library.name,
library.collectionType
)
)
} }
} }
@ -116,7 +193,7 @@ internal class HomeFragment : BrowseSupportFragment() {
private fun navigateToSettingsFragment() { private fun navigateToSettingsFragment() {
findNavController().navigate( findNavController().navigate(
HomeFragmentDirections.actionNavigationHomeToSettings() HomeFragmentDirections.actionHomeFragmentToSettingsFragment()
) )
} }
} }

View file

@ -121,11 +121,9 @@ internal class MediaDetailFragment : Fragment() {
when (viewModel.played) { when (viewModel.played) {
true -> { true -> {
viewModel.markAsUnplayed(args.itemId) viewModel.markAsUnplayed(args.itemId)
val typedValue = TypedValue()
requireActivity().theme.resolveAttribute(R.attr.colorOnSecondaryContainer, typedValue, true)
binding.checkButton.imageTintList = ColorStateList.valueOf( binding.checkButton.imageTintList = ColorStateList.valueOf(
resources.getColor( resources.getColor(
typedValue.resourceId, R.color.white,
requireActivity().theme requireActivity().theme
) )
) )
@ -147,11 +145,9 @@ internal class MediaDetailFragment : Fragment() {
true -> { true -> {
viewModel.unmarkAsFavorite(args.itemId) viewModel.unmarkAsFavorite(args.itemId)
binding.favoriteButton.setImageResource(R.drawable.ic_heart) binding.favoriteButton.setImageResource(R.drawable.ic_heart)
val typedValue = TypedValue()
requireActivity().theme.resolveAttribute(R.attr.colorOnSecondaryContainer, typedValue, true)
binding.favoriteButton.imageTintList = ColorStateList.valueOf( binding.favoriteButton.imageTintList = ColorStateList.valueOf(
resources.getColor( resources.getColor(
typedValue.resourceId, R.color.white,
requireActivity().theme requireActivity().theme
) )
) )
@ -168,16 +164,14 @@ internal class MediaDetailFragment : Fragment() {
} }
} }
} }
binding.backButton.setOnClickListener { activity?.onBackPressed() }
} }
private fun bindUiStateNormal(uiState: MediaInfoViewModel.UiState.Normal) { private fun bindUiStateNormal(uiState: MediaInfoViewModel.UiState.Normal) {
uiState.apply { uiState.apply {
binding.seasonTitle.isVisible = seasons.isNotEmpty() binding.seasonsLayout.isVisible = seasons.isNotEmpty()
val seasonsAdapter = binding.seasonsRow.gridView.adapter as ViewItemListAdapter val seasonsAdapter = binding.seasonsRow.gridView.adapter as ViewItemListAdapter
seasonsAdapter.submitList(seasons) seasonsAdapter.submitList(seasons)
binding.castTitle.isVisible = actors.isNotEmpty() binding.castLayout.isVisible = actors.isNotEmpty()
val actorsAdapter = binding.castRow.gridView.adapter as PersonListAdapter val actorsAdapter = binding.castRow.gridView.adapter as PersonListAdapter
actorsAdapter.submitList(actors) actorsAdapter.submitList(actors)
@ -217,11 +211,6 @@ internal class MediaDetailFragment : Fragment() {
) )
binding.title.text = item.name binding.title.text = item.name
binding.subtitle.text = item.seriesName
item.seriesName.let {
binding.subtitle.text = it
binding.subtitle.isVisible = true
}
binding.genres.text = genresString binding.genres.text = genresString
binding.year.text = dateString binding.year.text = dateString
binding.playtime.text = runTime binding.playtime.text = runTime

View file

@ -1,6 +1,5 @@
package dev.jdtech.jellyfin.tv.ui package dev.jdtech.jellyfin.tv.ui
import android.content.Context.LAYOUT_INFLATER_SERVICE
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -10,25 +9,52 @@ import androidx.databinding.DataBindingUtil
import androidx.leanback.widget.Presenter import androidx.leanback.widget.Presenter
import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.databinding.BaseItemBinding import dev.jdtech.jellyfin.databinding.BaseItemBinding
import dev.jdtech.jellyfin.databinding.CollectionItemBinding
import dev.jdtech.jellyfin.databinding.HomeEpisodeItemBinding import dev.jdtech.jellyfin.databinding.HomeEpisodeItemBinding
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
class LibaryItemPresenter(private val onClick: (BaseItemDto) -> Unit) : Presenter() {
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
return ViewHolder(
CollectionItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
).root
)
}
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
if (item is BaseItemDto) {
DataBindingUtil.getBinding<CollectionItemBinding>(viewHolder.view)?.apply {
this.collection = item
viewHolder.view.setOnClickListener { onClick(item) }
}
}
}
override fun onUnbindViewHolder(viewHolder: ViewHolder) = Unit
}
class MediaItemPresenter(private val onClick: (BaseItemDto) -> Unit) : Presenter() { class MediaItemPresenter(private val onClick: (BaseItemDto) -> Unit) : Presenter() {
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
val mediaView = return ViewHolder(
BaseItemBinding BaseItemBinding.inflate(
.inflate(parent.context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater) LayoutInflater.from(parent.context),
.root parent,
return ViewHolder(mediaView) false
).root
)
} }
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) { override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
if (item is BaseItemDto) { if (item is BaseItemDto) {
DataBindingUtil.getBinding<BaseItemBinding>(viewHolder.view)?.apply { DataBindingUtil.getBinding<BaseItemBinding>(viewHolder.view)?.apply {
this.item = item this.item = item
this.itemName.text = if (item.type == BaseItemKind.EPISODE) item.seriesName else item.name this.itemName.text =
if (item.type == BaseItemKind.EPISODE) item.seriesName else item.name
this.itemCount.visibility = this.itemCount.visibility =
if (item.userData?.unplayedItemCount != null && item.userData?.unplayedItemCount!! > 0) View.VISIBLE else View.GONE if (item.userData?.unplayedItemCount != null && item.userData?.unplayedItemCount!! > 0) View.VISIBLE else View.GONE
this.itemLayout.layoutParams.width = this.itemLayout.layoutParams.width =
@ -45,11 +71,13 @@ class MediaItemPresenter(private val onClick: (BaseItemDto) -> Unit) : Presenter
class DynamicMediaItemPresenter(private val onClick: (BaseItemDto) -> Unit) : Presenter() { class DynamicMediaItemPresenter(private val onClick: (BaseItemDto) -> Unit) : Presenter() {
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
val mediaView = return ViewHolder(
HomeEpisodeItemBinding HomeEpisodeItemBinding.inflate(
.inflate(parent.context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater) LayoutInflater.from(parent.context),
.root parent,
return ViewHolder(mediaView) false
).root
)
} }
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) { override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
@ -59,7 +87,8 @@ class DynamicMediaItemPresenter(private val onClick: (BaseItemDto) -> Unit) : Pr
item.userData?.playedPercentage?.toInt()?.let { item.userData?.playedPercentage?.toInt()?.let {
progressBar.layoutParams.width = TypedValue.applyDimension( progressBar.layoutParams.width = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, TypedValue.COMPLEX_UNIT_DIP,
(it.times(2.24)).toFloat(), progressBar.context.resources.displayMetrics).toInt() (it.times(2.24)).toFloat(), progressBar.context.resources.displayMetrics
).toInt()
progressBar.isVisible = true progressBar.isVisible = true
} }

View file

@ -1,99 +0,0 @@
package dev.jdtech.jellyfin.tv.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.databinding.TvAddServerFragmentBinding
import dev.jdtech.jellyfin.viewmodels.AddServerViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
@AndroidEntryPoint
internal class TvAddServerFragment : Fragment() {
private lateinit var binding: TvAddServerFragmentBinding
private val viewModel: AddServerViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = TvAddServerFragmentBinding.inflate(inflater)
binding.editTextServerAddress.setOnEditorActionListener { _, actionId, _ ->
return@setOnEditorActionListener when (actionId) {
EditorInfo.IME_ACTION_GO -> {
connectToServer()
true
}
else -> false
}
}
binding.buttonConnect.setOnClickListener {
connectToServer()
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
Timber.d("$uiState")
when (uiState) {
is AddServerViewModel.UiState.Normal -> bindUiStateNormal()
is AddServerViewModel.UiState.Error -> bindUiStateError(uiState)
is AddServerViewModel.UiState.Loading -> bindUiStateLoading()
}
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.navigateToLogin.collect {
if (it) {
navigateToLoginFragment()
}
}
}
}
return binding.root
}
private fun bindUiStateNormal() {
binding.buttonConnect.isEnabled = true
binding.progressCircular.isVisible = false
}
private fun bindUiStateError(uiState: AddServerViewModel.UiState.Error) {
binding.buttonConnect.isEnabled = true
binding.progressCircular.isVisible = false
binding.editTextServerAddress.error = uiState.message
}
private fun bindUiStateLoading() {
binding.buttonConnect.isEnabled = false
binding.progressCircular.isVisible = true
binding.editTextServerAddress.error = null
}
private fun connectToServer() {
val serverAddress = binding.editTextServerAddress.text.toString()
viewModel.checkServer(serverAddress.removeSuffix("/"))
}
private fun navigateToLoginFragment() {
findNavController().navigate(TvAddServerFragmentDirections.actionAddServerFragmentToLoginFragment())
}
}

View file

@ -1,99 +0,0 @@
package dev.jdtech.jellyfin.tv.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.databinding.TvLoginFragmentBinding
import dev.jdtech.jellyfin.viewmodels.LoginViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
@AndroidEntryPoint
class TvLoginFragment : Fragment() {
private lateinit var binding: TvLoginFragmentBinding
private val viewModel: LoginViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = TvLoginFragmentBinding.inflate(inflater)
binding.editTextPassword.setOnEditorActionListener { _, actionId, _ ->
return@setOnEditorActionListener when (actionId) {
EditorInfo.IME_ACTION_GO -> {
login()
true
}
else -> false
}
}
binding.buttonLogin.setOnClickListener {
login()
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
Timber.d("$uiState")
when(uiState) {
is LoginViewModel.UiState.Normal -> bindUiStateNormal()
is LoginViewModel.UiState.Error -> bindUiStateError(uiState)
is LoginViewModel.UiState.Loading -> bindUiStateLoading()
}
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.navigateToMain.collect {
if (it) {
navigateToMainActivity()
}
}
}
}
return binding.root
}
private fun bindUiStateNormal() {
binding.buttonLogin.isEnabled = true
binding.progressCircular.isVisible = false
}
private fun bindUiStateError(uiState: LoginViewModel.UiState.Error) {
binding.buttonLogin.isEnabled = true
binding.progressCircular.isVisible = false
binding.editTextUsername.error = uiState.message
}
private fun bindUiStateLoading() {
binding.buttonLogin.isEnabled = false
binding.progressCircular.isVisible = true
binding.editTextUsername.error = null
}
private fun login() {
val username = binding.editTextUsername.text.toString()
val password = binding.editTextPassword.text.toString()
viewModel.login(username, password)
}
private fun navigateToMainActivity() {
findNavController().navigate(TvLoginFragmentDirections.actionLoginFragmentToNavigationHome())
}
}

View file

@ -10,11 +10,8 @@ import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.database.Server import dev.jdtech.jellyfin.database.Server
import dev.jdtech.jellyfin.database.ServerDatabaseDao import dev.jdtech.jellyfin.database.ServerDatabaseDao
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.discovery.RecommendedServerInfo import org.jellyfin.sdk.discovery.RecommendedServerInfo
import org.jellyfin.sdk.discovery.RecommendedServerInfoScore import org.jellyfin.sdk.discovery.RecommendedServerInfoScore
import org.jellyfin.sdk.discovery.RecommendedServerIssue import org.jellyfin.sdk.discovery.RecommendedServerIssue
@ -68,7 +65,6 @@ constructor(
RecommendedServerInfoScore.OK RecommendedServerInfoScore.OK
) )
val greatServers = mutableListOf<RecommendedServerInfo>()
val goodServers = mutableListOf<RecommendedServerInfo>() val goodServers = mutableListOf<RecommendedServerInfo>()
val okServers = mutableListOf<RecommendedServerInfo>() val okServers = mutableListOf<RecommendedServerInfo>()
@ -76,9 +72,6 @@ constructor(
.onCompletion { .onCompletion {
if (serverFound) return@onCompletion if (serverFound) return@onCompletion
when { when {
greatServers.isNotEmpty() -> {
connectToServer(greatServers.first())
}
goodServers.isNotEmpty() -> { goodServers.isNotEmpty() -> {
val issuesString = createIssuesString(goodServers.first()) val issuesString = createIssuesString(goodServers.first())
Toast.makeText( Toast.makeText(
@ -109,6 +102,8 @@ constructor(
RecommendedServerInfoScore.BAD -> Unit RecommendedServerInfoScore.BAD -> Unit
} }
} }
} catch (e: CancellationException) {
} catch (e: Exception) { } catch (e: Exception) {
_uiState.emit( _uiState.emit(
UiState.Error( UiState.Error(

View file

@ -38,18 +38,25 @@ class HomeViewModel @Inject internal constructor(
} }
init { init {
loadData(updateCapabilities = true) viewModelScope.launch {
try {
repository.postCapabilities()
} catch (e: Exception) {
}
}
} }
fun refreshData() = loadData(updateCapabilities = false) fun loadData(includeLibraries: Boolean = false) {
private fun loadData(updateCapabilities: Boolean) {
viewModelScope.launch { viewModelScope.launch {
_uiState.emit(UiState.Loading) _uiState.emit(UiState.Loading)
try { try {
if (updateCapabilities) repository.postCapabilities() val items = mutableListOf<HomeItem>()
val updated = loadDynamicItems() + loadViews() if (includeLibraries) {
items.add(loadLibraries())
}
val updated = items + loadDynamicItems() + loadViews()
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
syncPlaybackProgress(downloadDatabase, repository) syncPlaybackProgress(downloadDatabase, repository)
@ -61,6 +68,19 @@ class HomeViewModel @Inject internal constructor(
} }
} }
private suspend fun loadLibraries(): HomeItem {
val items = repository.getItems()
val collections =
items.filter { collection -> CollectionType.unsupportedCollections.none { it.type == collection.collectionType } }
return HomeItem.Libraries(
HomeSection(
UUID.fromString("38f5ca96-9e4b-4c0e-a8e4-02225ed07e02"),
application.resources.getString(R.string.libraries),
collections
)
)
}
private suspend fun loadDynamicItems(): List<Section> { private suspend fun loadDynamicItems(): List<Section> {
val resumeItems = repository.getResumeItems() val resumeItems = repository.getResumeItems()
val nextUpItems = repository.getNextUp() val nextUpItems = repository.getNextUp()

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="true" android:state_focused="true">
<shape android:shape="rectangle">
<solid android:color="@color/neutral_700"/>
<stroke android:width="2dp" android:color="@color/white" />
<corners android:radius="10dp" />
</shape>
</item>
<item android:state_enabled="true">
<shape android:shape="rectangle">
<solid android:color="@color/neutral_700"/>
<corners android:radius="10dp" />
</shape>
</item>
</selector>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="true" android:state_focused="true">
<shape android:shape="rectangle">
<solid android:color="?attr/colorPrimary"/>
<stroke android:width="2dp" android:color="@color/white" />
<corners android:radius="10dp" />
</shape>
</item>
<item android:state_enabled="true">
<shape android:shape="rectangle">
<solid android:color="?attr/colorPrimary"/>
<corners android:radius="10dp" />
</shape>
</item>
</selector>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="true" android:state_focused="true">
<shape android:shape="rectangle">
<solid android:color="?android:attr/windowBackground"/>
<padding android:bottom="4dp" android:left="4dp" android:right="4dp" android:top="4dp" />
<stroke android:width="2dp" android:color="@color/white" />
<corners android:bottomLeftRadius="10dp" android:bottomRightRadius="10dp" android:topLeftRadius="10dp" android:topRightRadius="10dp" />
</shape>
</item>
<item android:state_enabled="true">
<shape android:shape="rectangle">
<solid android:color="?android:attr/windowBackground"/>
<padding android:bottom="4dp" android:left="4dp" android:right="4dp" android:top="4dp" />
<corners android:bottomLeftRadius="10dp" android:bottomRightRadius="10dp" android:topLeftRadius="10dp" android:topRightRadius="10dp" />
</shape>
</item>
</selector>

View file

@ -2,8 +2,7 @@
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24" android:viewportHeight="24">
android:tint="?attr/colorControlNormal">
<path <path
android:pathData="M5,3l14,9l-14,9l0,-18z" android:pathData="M5,3l14,9l-14,9l0,-18z"
android:strokeLineJoin="round" android:strokeLineJoin="round"

View file

@ -1,13 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
>
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/tv_nav_host" android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment" android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -16,7 +14,6 @@
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="parent" app:layout_constraintTop_toBottomOf="parent"
app:navGraph="@navigation/tv_navigation" app:navGraph="@navigation/app_navigation" />
/>
</FrameLayout> </FrameLayout>

View file

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<import type="android.view.View" />
<variable
name="item"
type="org.jellyfin.sdk.model.api.BaseItemDto" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/item_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginBottom="24dp"
android:background="@drawable/focus_border"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:orientation="vertical">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/item_image"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="centerCrop"
app:itemImage="@{item}"
app:layout_constraintDimensionRatio="H,2:3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Findroid.Image"
app:strokeColor="@null" />
<TextView
android:id="@+id/item_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/item_image"
tools:text="Movie title" />
<TextView
android:id="@+id/item_count"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/circle_background"
android:gravity="center"
android:text="@{item.userData.unplayedItemCount.toString()}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="9" />
<ImageView
android:id="@+id/played_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/circle_background"
android:contentDescription="@string/episode_watched_indicator"
android:padding="4dp"
android:src="@drawable/ic_check"
android:visibility="@{item.userData.played == true ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="@id/item_image"
app:layout_constraintTop_toTopOf="@id/item_image" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -1,38 +1,34 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:ignore="MissingDefaultResource"
android:paddingTop="16dp" android:paddingTop="16dp"
> tools:ignore="MissingDefaultResource">
<ImageButton <ImageButton
android:id="@+id/settings" android:id="@+id/settings"
android:layout_width="wrap_content" android:layout_width="32dp"
android:layout_height="24dp" android:layout_height="32dp"
app:layout_constraintTop_toTopOf="parent" android:layout_marginEnd="24dp"
app:layout_constraintEnd_toStartOf="@+id/clock" android:background="@drawable/focus_border"
android:src="@drawable/ic_settings"
android:contentDescription="@string/title_settings" android:contentDescription="@string/title_settings"
android:background="@drawable/transparent_circle_background"
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:layout_marginEnd="24dp" android:src="@drawable/ic_settings"
/> app:layout_constraintEnd_toStartOf="@+id/clock"
app:layout_constraintTop_toTopOf="parent" />
<TextClock <TextClock
android:id="@+id/clock" android:id="@+id/clock"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="24dp" android:layout_height="32dp"
app:layout_constraintTop_toTopOf="parent" android:layout_marginEnd="24dp"
app:layout_constraintEnd_toEndOf="parent"
android:gravity="center_vertical" android:gravity="center_vertical"
android:textSize="18sp" android:textSize="18sp"
android:layout_marginEnd="24dp" app:layout_constraintEnd_toEndOf="parent"
tools:text="12:00" app:layout_constraintTop_toTopOf="parent"
/> tools:text="12:00" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="collection"
type="org.jellyfin.sdk.model.api.BaseItemDto" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="240dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginBottom="24dp"
android:background="@drawable/focus_border"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:orientation="vertical">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/collection_image"
android:layout_width="0dp"
android:layout_height="0dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
app:baseItemImage="@{collection}"
app:layout_constraintDimensionRatio="H,16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Findroid.Image"
app:strokeColor="@null" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@{collection.name}"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/collection_image"
tools:text="Movies" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -1,14 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context=".fragments.AddServerFragment" android:layout_width="match_parent"
tools:ignore="MissingDefaultResource"> android:layout_height="match_parent"
android:fillViewport="true"
tools:context=".fragments.AddServerFragment">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content">
<ImageView <ImageView
android:id="@+id/image_banner" android:id="@+id/image_banner"
@ -49,6 +50,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:autofillHints="" android:autofillHints=""
android:focusedByDefault="true"
android:hint="@string/edit_text_server_address_hint" android:hint="@string/edit_text_server_address_hint"
android:imeOptions="actionGo" android:imeOptions="actionGo"
android:inputType="textUri" /> android:inputType="textUri" />
@ -76,4 +78,4 @@
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</layout> </ScrollView>

View file

@ -0,0 +1,54 @@
<?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=".fragments.LibraryFragment">
<include
android:id="@+id/error_layout"
layout="@layout/error_panel" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Movies" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/items_recycler_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingHorizontal="12dp"
android:paddingTop="16dp"
android:scrollbars="none"
android:layout_marginTop="12dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
app:spanCount="@integer/library_columns"
tools:itemCount="14"
tools:listitem="@layout/base_item">
<requestFocus/>
</androidx.recyclerview.widget.RecyclerView>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/loading_indicator"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,14 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:ignore="MissingDefaultResource"> android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:context=".fragments.LoginFragment">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content">
tools:context=".fragments.LoginFragment">
<ImageView <ImageView
android:id="@+id/image_banner" android:id="@+id/image_banner"
@ -82,8 +83,9 @@
android:padding="8dp" android:padding="8dp"
android:visibility="invisible" /> android:visibility="invisible" />
</RelativeLayout> </RelativeLayout>
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</layout> </ScrollView>

View file

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="episode"
type="org.jellyfin.sdk.model.api.BaseItemDto" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="240dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:background="@drawable/focus_border"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:orientation="vertical">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/episode_image"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="centerCrop"
app:baseItemImage="@{episode}"
app:layout_constraintDimensionRatio="H,16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Findroid.Image"
app:strokeColor="@null" />
<TextView
android:id="@+id/primary_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/episode_image"
tools:text="Wonder Egg Priority" />
<TextView
android:id="@+id/secondary_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="@{String.format(@string/episode_name_extended, episode.parentIndexNumber, episode.indexNumber, episode.name)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/primary_name"
tools:text="The Girl Flautist" />
<FrameLayout
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="4dp"
android:layout_marginHorizontal="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/button_setup_background"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/episode_image"
app:layout_constraintStart_toStartOf="parent"
tools:layout_width="50dp"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -19,27 +19,14 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<ImageButton
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/transparent_circle_background"
android:contentDescription="@string/player_controls_exit"
android:focusable="true"
android:padding="16dp"
android:src="@drawable/ic_arrow_left"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
android:id="@+id/title" android:id="@+id/title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp" android:layout_marginHorizontal="@dimen/horizontal_margin"
android:layout_marginTop="8dp" android:layout_marginTop="12dp"
android:paddingBottom="16dp" android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toEndOf="@id/back_button"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="Alita: Battle Angel" /> tools:text="Alita: Battle Angel" />
@ -47,6 +34,7 @@
android:id="@+id/clock" android:id="@+id/clock"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp" android:layout_marginEnd="24dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:textSize="18sp" android:textSize="18sp"
@ -59,189 +47,209 @@
android:layout_width="320dp" android:layout_width="320dp"
android:layout_height="180dp" android:layout_height="180dp"
android:layout_marginStart="@dimen/horizontal_margin" android:layout_marginStart="@dimen/horizontal_margin"
android:layout_marginTop="12dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" />
<TextView
android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginStart="@dimen/horizontal_margin"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@id/poster"
app:layout_constraintTop_toBottomOf="@id/title" app:layout_constraintTop_toBottomOf="@id/title"
tools:text="Subtitle" /> app:shapeAppearance="@style/ShapeAppearanceOverlay.Findroid.Image"
app:strokeColor="@null" />
<TextView <LinearLayout
android:id="@+id/genres" android:id="@+id/main_info"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/horizontal_margin" android:layout_marginStart="@dimen/horizontal_margin"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:orientation="vertical"
app:layout_constraintStart_toEndOf="@id/poster" app:layout_constraintStart_toEndOf="@id/poster"
app:layout_constraintTop_toBottomOf="@id/subtitle" app:layout_constraintTop_toTopOf="@id/poster">
tools:text="Action, Science Fiction, Adventure" />
<TextView <LinearLayout
android:id="@+id/year" android:id="@+id/info"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/horizontal_margin" android:layout_marginBottom="12dp">
android:layout_marginEnd="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
app:layout_constraintStart_toEndOf="@id/poster"
app:layout_constraintTop_toBottomOf="@id/genres"
tools:text="2019" />
<TextView <TextView
android:id="@+id/playtime" android:id="@+id/year"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/horizontal_margin" android:layout_marginEnd="8dp"
android:layout_marginEnd="8dp" android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" tools:text="2019" />
app:layout_constraintStart_toEndOf="@id/year"
app:layout_constraintTop_toBottomOf="@id/genres"
tools:text="122 min" />
<TextView <TextView
android:id="@+id/official_rating" android:id="@+id/playtime"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/horizontal_margin" android:layout_marginEnd="8dp"
android:layout_marginEnd="8dp" android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" tools:text="122 min" />
app:layout_constraintStart_toEndOf="@id/playtime"
app:layout_constraintTop_toBottomOf="@id/genres"
tools:text="PG-13" />
<TextView <TextView
android:id="@+id/community_rating" android:id="@+id/official_rating"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/horizontal_margin" android:layout_marginEnd="8dp"
android:drawablePadding="4dp" android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" tools:text="PG-13" />
app:drawableStartCompat="@drawable/ic_star"
app:drawableTint="@color/yellow"
app:layout_constraintStart_toEndOf="@id/official_rating"
app:layout_constraintTop_toBottomOf="@id/genres"
tools:text="7.3" />
<TextView <TextView
android:id="@+id/description" android:id="@+id/community_rating"
android:layout_width="400dp" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="4dp"
android:gravity="bottom"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:drawableStartCompat="@drawable/ic_star"
app:drawableTint="@color/yellow"
tools:text="7.3" />
</LinearLayout>
<TextView
android:id="@+id/genres"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
tools:text="Action, Science Fiction, Adventure" />
<TextView
android:id="@+id/description"
android:layout_width="400dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="5"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
tools:text="An angel falls. A warrior rises. When Alita awakens with no memory of who she is in a future world she does not recognize, she is taken in by Ido, a compassionate doctor who realizes that somewhere in this abandoned cyborg shell is the heart and soul of a young woman with an extraordinary past." />
<LinearLayout
android:id="@+id/buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp">
<ImageButton
android:id="@+id/play_button"
android:layout_width="72dp"
android:layout_height="48dp"
android:background="@drawable/button_setup_background"
android:contentDescription="@string/play_button_description"
android:focusable="true"
android:focusableInTouchMode="true"
android:focusedByDefault="true"
android:nextFocusLeft="@id/play_button"
android:paddingHorizontal="24dp"
android:paddingVertical="12dp"
android:src="@drawable/ic_play">
<requestFocus />
</ImageButton>
<ProgressBar
android:id="@+id/progress_circular"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerHorizontal="true"
android:elevation="8dp"
android:indeterminateTint="@color/white"
android:padding="8dp"
android:visibility="invisible" />
</RelativeLayout>
<ImageButton
android:id="@+id/trailer_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:background="@drawable/button_accent_background"
android:contentDescription="@string/trailer_button_description"
android:focusable="true"
android:focusableInTouchMode="true"
android:padding="12dp"
android:src="@drawable/ic_film" />
<ImageButton
android:id="@+id/check_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:background="@drawable/button_accent_background"
android:contentDescription="@string/check_button_description"
android:focusable="true"
android:focusableInTouchMode="true"
android:padding="12dp"
android:src="@drawable/ic_check" />
<ImageButton
android:id="@+id/favorite_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/button_accent_background"
android:contentDescription="@string/favorite_button_description"
android:focusable="true"
android:focusableInTouchMode="true"
android:padding="12dp"
android:src="@drawable/ic_heart" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/seasons_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginStart="@dimen/horizontal_margin"
android:layout_marginBottom="24dp" android:layout_marginBottom="24dp"
android:ellipsize="end" android:orientation="vertical"
android:maxLines="5"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
app:layout_constraintStart_toEndOf="@id/poster"
app:layout_constraintTop_toBottomOf="@id/year"
tools:text="An angel falls. A warrior rises. When Alita awakens with no memory of who she is in a future world she does not recognize, she is taken in by Ido, a compassionate doctor who realizes that somewhere in this abandoned cyborg shell is the heart and soul of a young woman with an extraordinary past." />
<ProgressBar
android:id="@+id/progress_circular"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="@dimen/horizontal_margin"
android:elevation="8dp"
android:indeterminateTint="@color/white"
android:padding="8dp"
android:visibility="invisible"
app:layout_constraintStart_toEndOf="@id/poster"
app:layout_constraintTop_toBottomOf="@id/description" />
<ImageButton
android:id="@+id/play_button"
android:layout_width="72dp"
android:layout_height="48dp"
android:layout_marginStart="@dimen/horizontal_margin"
android:contentDescription="@string/play_button_description"
android:focusable="true"
android:paddingHorizontal="24dp"
android:paddingVertical="12dp"
android:src="@drawable/ic_play"
app:layout_constraintStart_toEndOf="@id/poster"
app:layout_constraintTop_toBottomOf="@id/description" />
<ImageButton
android:id="@+id/trailer_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:contentDescription="@string/trailer_button_description"
android:focusable="true"
android:padding="12dp"
android:src="@drawable/ic_film"
app:layout_constraintStart_toEndOf="@id/play_button"
app:layout_constraintTop_toBottomOf="@id/description" />
<ImageButton
android:id="@+id/check_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:contentDescription="@string/check_button_description"
android:focusable="true"
android:padding="12dp"
android:src="@drawable/ic_check"
app:layout_constraintStart_toEndOf="@id/trailer_button"
app:layout_constraintTop_toBottomOf="@id/description" />
<ImageButton
android:id="@+id/favorite_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/favorite_button_description"
android:focusable="true"
android:padding="12dp"
android:src="@drawable/ic_heart"
app:layout_constraintStart_toEndOf="@id/check_button"
app:layout_constraintTop_toBottomOf="@id/description" />
<TextView
android:id="@+id/season_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/horizontal_margin"
android:layout_marginTop="32dp"
android:text="@string/seasons"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline3"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/play_button" /> app:layout_constraintTop_toBottomOf="@id/main_info">
<androidx.leanback.widget.ListRowView <TextView
android:id="@+id/seasons_row" android:id="@+id/season_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/horizontal_margin"
android:layout_marginTop="32dp"
android:text="@string/seasons"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium" />
<androidx.leanback.widget.ListRowView
android:id="@+id/seasons_row"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:id="@+id/cast_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/season_title" /> app:layout_constraintTop_toBottomOf="@id/seasons_layout">
<TextView <TextView
android:id="@+id/cast_title" android:id="@+id/cast_title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/horizontal_margin" android:layout_marginStart="@dimen/horizontal_margin"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@string/cast_amp_crew" android:text="@string/cast_amp_crew"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline3" android:textAppearance="@style/TextAppearance.Material3.TitleMedium" />
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/seasons_row" />
<androidx.leanback.widget.ListRowView <androidx.leanback.widget.ListRowView
android:id="@+id/cast_row" android:id="@+id/cast_row"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cast_title" /> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="person"
type="org.jellyfin.sdk.model.api.BaseItemPerson" />
</data>
<LinearLayout
android:id="@+id/item_layout"
android:layout_width="110dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:background="@drawable/focus_border"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:orientation="vertical">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/person_image"
android:layout_width="match_parent"
android:layout_height="160dp"
android:layout_marginBottom="4dp"
android:scaleType="centerCrop"
app:personImage="@{person}"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Findroid.Image"
app:strokeColor="@null" />
<TextView
android:id="@+id/person_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="@{person.name}"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
tools:text="Rosa Salazar" />
<TextView
android:id="@+id/person_role"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="@{person.role}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
tools:text="Alita" />
</LinearLayout>
</layout>

View file

@ -0,0 +1 @@
../layout-television/activity_main.xml

View file

@ -0,0 +1,44 @@
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.navigationrail.NavigationRailView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/main_toolbar_layout"
app:menu="@menu/bottom_nav_menu" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/nav_view"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/main_toolbar_layout"
app:navGraph="@navigation/app_navigation" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/main_toolbar_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/nav_host_fragment_activity_main"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/main_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.navigationrail.NavigationRailView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/main_toolbar_layout"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:menu="@menu/bottom_nav_menu" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/nav_view"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/main_toolbar_layout"
app:navGraph="@navigation/app_navigation" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/main_toolbar_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/nav_host_fragment_activity_main"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/main_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,46 @@
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/bottom_nav_menu" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/main_toolbar_layout"
app:navGraph="@navigation/app_navigation" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/main_toolbar_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/nav_host_fragment_activity_main"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/main_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,49 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/bottom_nav_menu" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/main_toolbar_layout"
app:navGraph="@navigation/app_navigation" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/main_toolbar_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/nav_host_fragment_activity_main"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/main_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -21,7 +21,7 @@
<com.google.android.material.imageview.ShapeableImageView <com.google.android.material.imageview.ShapeableImageView
android:id="@+id/episode_image" android:id="@+id/episode_image"
android:layout_width="240dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:baseItemImage="@{episode}" app:baseItemImage="@{episode}"
@ -36,7 +36,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:singleLine="true" android:maxLines="1"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -47,7 +47,7 @@
android:id="@+id/secondary_name" android:id="@+id/secondary_name"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" android:maxLines="1"
android:text="@{String.format(@string/episode_name_extended, episode.parentIndexNumber, episode.indexNumber, episode.name)}" android:text="@{String.format(@string/episode_name_extended, episode.parentIndexNumber, episode.indexNumber, episode.name)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View file

@ -22,7 +22,7 @@
<com.google.android.material.imageview.ShapeableImageView <com.google.android.material.imageview.ShapeableImageView
android:id="@+id/person_image" android:id="@+id/person_image"
android:layout_width="110dp" android:layout_width="match_parent"
android:layout_height="160dp" android:layout_height="160dp"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
@ -34,7 +34,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="2" android:maxLines="1"
android:text="@{person.name}" android:text="@{person.name}"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
tools:text="Rosa Salazar" /> tools:text="Rosa Salazar" />
@ -44,7 +44,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="2" android:maxLines="1"
android:text="@{person.role}" android:text="@{person.role}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" android:textAppearance="@style/TextAppearance.Material3.BodySmall"
tools:text="Alita" /> tools:text="Alita" />

View file

@ -28,7 +28,7 @@
android:id="@+id/action_navigation_home_to_episodeBottomSheetFragment" android:id="@+id/action_navigation_home_to_episodeBottomSheetFragment"
app:destination="@id/episodeBottomSheetFragment" /> app:destination="@id/episodeBottomSheetFragment" />
<action <action
android:id="@+id/action_navigation_home_to_navigation_settings" android:id="@+id/action_homeFragment_to_settingsFragment"
app:destination="@id/twoPaneSettingsFragment" app:destination="@id/twoPaneSettingsFragment"
app:enterAnim="@anim/nav_default_enter_anim" app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim" app:exitAnim="@anim/nav_default_exit_anim"
@ -39,6 +39,37 @@
app:destination="@id/addServerFragment" app:destination="@id/addServerFragment"
app:popUpTo="@id/homeFragment" app:popUpTo="@id/homeFragment"
app:popUpToInclusive="true" /> app:popUpToInclusive="true" />
<action
android:id="@+id/action_homeFragment_to_mediaDetailFragment"
app:destination="@id/mediaDetailFragment" />
</fragment>
<fragment
android:id="@+id/homeFragmentTv"
android:name="dev.jdtech.jellyfin.tv.ui.HomeFragment"
android:label="@string/title_home" >
<action
android:id="@+id/action_homeFragment_to_mediaDetailFragment"
app:destination="@id/mediaDetailFragment" />
<action
android:id="@+id/action_homeFragment_to_settingsFragment"
app:destination="@id/twoPaneSettingsFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_homeFragment_to_addServerFragment"
app:destination="@id/addServerFragment"
app:popUpTo="@id/homeFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_homeFragment_to_libraryFragment"
app:destination="@id/libraryFragment" />
</fragment> </fragment>
<fragment <fragment
@ -93,6 +124,9 @@
app:exitAnim="@anim/nav_default_exit_anim" app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim" app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" /> app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_libraryFragment_to_mediaDetailFragment"
app:destination="@id/mediaDetailFragment" />
<argument <argument
android:name="libraryType" android:name="libraryType"
android:defaultValue="unknown" android:defaultValue="unknown"
@ -138,6 +172,29 @@
app:argType="boolean" app:argType="boolean"
android:defaultValue="false" /> android:defaultValue="false" />
</fragment> </fragment>
<fragment
android:id="@+id/mediaDetailFragment"
android:name="dev.jdtech.jellyfin.tv.ui.MediaDetailFragment"
android:label="{itemName}"
tools:layout="@layout/media_detail_fragment">
<argument
android:name="itemId"
app:argType="java.util.UUID" />
<argument
android:name="itemName"
android:defaultValue="Media Info"
app:argType="string"
app:nullable="true" />
<argument
android:name="itemType"
app:argType="org.jellyfin.sdk.model.api.BaseItemKind"
android:defaultValue="MOVIE" />
<action
android:id="@+id/action_mediaDetailFragment_to_playerActivity"
app:destination="@id/playerActivityTv" />
</fragment>
<fragment <fragment
android:id="@+id/seasonFragment" android:id="@+id/seasonFragment"
android:name="dev.jdtech.jellyfin.fragments.SeasonFragment" android:name="dev.jdtech.jellyfin.fragments.SeasonFragment"
@ -265,6 +322,11 @@
app:destination="@id/homeFragment" app:destination="@id/homeFragment"
app:popUpTo="@id/homeFragment" app:popUpTo="@id/homeFragment"
app:popUpToInclusive="true" /> app:popUpToInclusive="true" />
<action
android:id="@+id/action_serverSelectFragment_to_homeFragmentTv"
app:destination="@id/homeFragmentTv"
app:popUpTo="@id/homeFragmentTv"
app:popUpToInclusive="true" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/loginFragment" android:id="@+id/loginFragment"
@ -272,10 +334,15 @@
android:label="@string/login" android:label="@string/login"
tools:layout="@layout/fragment_login"> tools:layout="@layout/fragment_login">
<action <action
android:id="@+id/action_loginFragment_to_navigation_home" android:id="@+id/action_loginFragment_to_homeFragment"
app:destination="@id/homeFragment" app:destination="@id/homeFragment"
app:popUpTo="@id/homeFragment" app:popUpTo="@id/homeFragment"
app:popUpToInclusive="true" /> app:popUpToInclusive="true" />
<action
android:id="@+id/action_loginFragment_to_homeFragmentTv"
app:destination="@id/homeFragmentTv"
app:popUpTo="@id/homeFragmentTv"
app:popUpToInclusive="true" />
</fragment> </fragment>
<fragment <fragment
@ -303,6 +370,16 @@
app:argType="dev.jdtech.jellyfin.models.PlayerItem[]" /> app:argType="dev.jdtech.jellyfin.models.PlayerItem[]" />
</activity> </activity>
<activity
android:id="@+id/playerActivityTv"
android:name="dev.jdtech.jellyfin.tv.TvPlayerActivity"
android:label="activity_player_tv"
tools:layout="@layout/activity_player_tv">
<argument
android:name="items"
app:argType="dev.jdtech.jellyfin.models.PlayerItem[]" />
</activity>
<include app:graph="@navigation/aboutlibs_navigation" /> <include app:graph="@navigation/aboutlibs_navigation" />
<action <action
android:id="@+id/action_global_loginFragment" android:id="@+id/action_global_loginFragment"

View file

@ -1,94 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation 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/tv_navigation"
app:startDestination="@+id/homeFragment">
<fragment
android:id="@+id/homeFragment"
android:name="dev.jdtech.jellyfin.tv.ui.HomeFragment"
android:label="@string/title_home" >
<action
android:id="@+id/action_homeFragment_to_mediaDetailFragment"
app:destination="@id/mediaDetailFragment" />
<action
android:id="@+id/action_navigation_home_to_settings"
app:destination="@id/twoPaneSettingsFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_homeFragment_to_addServerFragment"
app:destination="@id/addServerTvFragment"
app:popUpTo="@id/homeFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/mediaDetailFragment"
android:name="dev.jdtech.jellyfin.tv.ui.MediaDetailFragment"
android:label="{itemName}"
tools:layout="@layout/media_detail_fragment">
<argument
android:name="itemId"
app:argType="java.util.UUID" />
<argument
android:name="itemName"
android:defaultValue="Media Info"
app:argType="string"
app:nullable="true" />
<argument
android:name="itemType"
app:argType="org.jellyfin.sdk.model.api.BaseItemKind"
android:defaultValue="MOVIE" />
<action
android:id="@+id/action_mediaDetailFragment_to_playerActivity"
app:destination="@id/playerActivityTv" />
</fragment>
<fragment
android:id="@+id/addServerTvFragment"
android:name="dev.jdtech.jellyfin.tv.ui.TvAddServerFragment"
android:label="@string/add_server"
tools:layout="@layout/tv_add_server_fragment">
<action
android:id="@+id/action_addServerFragment_to_loginFragment"
app:destination="@id/loginFragment" />
</fragment>
<fragment
android:id="@+id/loginFragment"
android:name="dev.jdtech.jellyfin.tv.ui.TvLoginFragment"
android:label="@string/login"
tools:layout="@layout/fragment_login">
<action
android:id="@+id/action_loginFragment_to_navigation_home"
app:destination="@id/homeFragment"
app:popUpTo="@id/homeFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/twoPaneSettingsFragment"
android:name="dev.jdtech.jellyfin.fragments.SettingsFragment"
android:label="@string/title_settings">
</fragment>
<activity
android:id="@+id/playerActivityTv"
android:name="dev.jdtech.jellyfin.tv.TvPlayerActivity"
android:label="activity_player_tv"
tools:layout="@layout/activity_player_tv">
<argument
android:name="items"
app:argType="dev.jdtech.jellyfin.models.PlayerItem[]" />
</activity>
</navigation>

View file

@ -11,4 +11,6 @@
<dimen name="track_selection_item_height">48dp</dimen> <dimen name="track_selection_item_height">48dp</dimen>
<dimen name="track_selection_item_text_size">16sp</dimen> <dimen name="track_selection_item_text_size">16sp</dimen>
<item name="library_columns" type="integer">6</item>
</resources> </resources>

View file

@ -1,6 +1,6 @@
<resources> <resources>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Theme.Jellyfin.Tv" parent="Theme.AppCompat.Leanback"> <style name="Theme.Findroid" parent="Theme.AppCompat.Leanback">
<item name="colorPrimary">@color/blue_600</item> <item name="colorPrimary">@color/blue_600</item>
<item name="colorPrimaryVariant">@color/blue_800</item> <item name="colorPrimaryVariant">@color/blue_800</item>
<item name="colorSecondary">@color/green_500</item> <item name="colorSecondary">@color/green_500</item>

View file

@ -46,6 +46,7 @@
<string name="next_up">Next Up</string> <string name="next_up">Next Up</string>
<string name="continue_watching">Continue Watching</string> <string name="continue_watching">Continue Watching</string>
<string name="latest_library">Latest %1$s</string> <string name="latest_library">Latest %1$s</string>
<string name="libraries">Libraries</string>
<string name="series_poster">Series poster</string> <string name="series_poster">Series poster</string>
<string name="no_favorites">You have no favorites</string> <string name="no_favorites">You have no favorites</string>
<string name="no_downloads">You have nothing downloaded</string> <string name="no_downloads">You have nothing downloaded</string>