diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index aa8206fb..0c463e68 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,19 +36,6 @@ - - - - - - - - - diff --git a/app/src/main/java/dev/jdtech/jellyfin/MainActivity.kt b/app/src/main/java/dev/jdtech/jellyfin/MainActivity.kt index d143ab49..ea934785 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/MainActivity.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/MainActivity.kt @@ -1,69 +1,86 @@ package dev.jdtech.jellyfin +import android.app.UiModeManager +import android.content.res.Configuration import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.NavigationUI +import androidx.navigation.ui.NavigationUiSaveStateControl import androidx.navigation.ui.setupActionBarWithNavController import com.google.android.material.navigation.NavigationBarView import dagger.hilt.android.AndroidEntryPoint 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 javax.inject.Inject @AndroidEntryPoint class MainActivity : AppCompatActivity() { - private lateinit var binding: ActivityMainAppBinding + private lateinit var binding: ActivityMainBinding + private lateinit var uiModeManager: UiModeManager + @Inject lateinit var database: ServerDatabaseDao + @OptIn(NavigationUiSaveStateControl::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityMainAppBinding.inflate(layoutInflater) + binding = ActivityMainBinding.inflate(layoutInflater) + uiModeManager = getSystemService(UI_MODE_SERVICE) as UiModeManager setContentView(binding.root) - val navView: NavigationBarView = binding.navView as NavigationBarView - val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment_activity_main) as NavHostFragment - - setSupportActionBar(binding.mainToolbar) - 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() if (nServers < 1) { - val inflater = navController.navInflater - val graph = inflater.inflate(R.navigation.app_navigation) graph.setStartDestination(R.id.addServerFragment) - navController.setGraph(graph, intent.extras) } - // 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 + navController.setGraph(graph, intent.extras) + + if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_TELEVISION) { + val navView: NavigationBarView = binding.navView as NavigationBarView + + 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) - // navView.setupWithNavController(navController) - // Don't save the state of other main navigation items, only this experimental function allows turning off this behavior - NavigationUI.setupWithNavController(navView, navController, false) + setupActionBarWithNavController(navController, appBarConfiguration) + // navView.setupWithNavController(navController) + // Don't save the state of other main navigation items, only this experimental function allows turning off this behavior + NavigationUI.setupWithNavController(navView, navController, false) - navController.addOnDestinationChangedListener { _, destination, _ -> - 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 - else -> View.VISIBLE + navController.addOnDestinationChangedListener { _, destination, _ -> + 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 + 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) diff --git a/app/src/main/java/dev/jdtech/jellyfin/MainActivityTv.kt b/app/src/main/java/dev/jdtech/jellyfin/MainActivityTv.kt deleted file mode 100644 index f7c7a1fb..00000000 --- a/app/src/main/java/dev/jdtech/jellyfin/MainActivityTv.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/adapters/ViewListAdapter.kt b/app/src/main/java/dev/jdtech/jellyfin/adapters/ViewListAdapter.kt index 1d4448bf..6cef5147 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/adapters/ViewListAdapter.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/adapters/ViewListAdapter.kt @@ -94,6 +94,7 @@ class ViewListAdapter( override fun getItemViewType(position: Int): Int { return when (getItem(position)) { + is HomeItem.Libraries -> -1 is HomeItem.Section -> ITEM_VIEW_TYPE_NEXT_UP is HomeItem.ViewItem -> ITEM_VIEW_TYPE_VIEW } @@ -105,6 +106,10 @@ class ViewListAdapter( } sealed class HomeItem { + data class Libraries(val section: HomeSection) : HomeItem() { + override val id = section.id + } + data class Section(val homeSection: HomeSection) : HomeItem() { override val id = homeSection.id } diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/AddServerFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/AddServerFragment.kt index b09da829..5adf1656 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/AddServerFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/AddServerFragment.kt @@ -1,10 +1,14 @@ package dev.jdtech.jellyfin.fragments +import android.app.UiModeManager +import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.AppCompatEditText import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels @@ -22,6 +26,7 @@ import timber.log.Timber class AddServerFragment : Fragment() { private lateinit var binding: FragmentAddServerBinding + private lateinit var uiModeManager: UiModeManager private val viewModel: AddServerViewModel by viewModels() override fun onCreateView( @@ -29,8 +34,10 @@ class AddServerFragment : Fragment() { savedInstanceState: Bundle? ): View { 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) { EditorInfo.IME_ACTION_GO -> { connectToServer() @@ -56,6 +63,7 @@ class AddServerFragment : Fragment() { } } } + viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.navigateToLogin.collect { @@ -77,17 +85,25 @@ class AddServerFragment : Fragment() { private fun bindUiStateError(uiState: AddServerViewModel.UiState.Error) { binding.buttonConnect.isEnabled = true 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() { binding.buttonConnect.isEnabled = false 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() { - val serverAddress = binding.editTextServerAddress.text.toString() + val serverAddress = (binding.editTextServerAddress as AppCompatEditText).text.toString() viewModel.checkServer(serverAddress.removeSuffix("/")) } diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/HomeFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/HomeFragment.kt index d0a9fc01..ad653229 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/HomeFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/HomeFragment.kt @@ -77,12 +77,12 @@ class HomeFragment : Fragment() { override fun onResume() { super.onResume() - viewModel.refreshData() + viewModel.loadData() } private fun setupView() { binding.refreshLayout.setOnRefreshListener { - viewModel.refreshData() + viewModel.loadData() } binding.viewsRecyclerView.adapter = ViewListAdapter( @@ -100,7 +100,7 @@ class HomeFragment : Fragment() { }) binding.errorLayout.errorRetryButton.setOnClickListener { - viewModel.refreshData() + viewModel.loadData() } binding.errorLayout.errorDetailsButton.setOnClickListener { @@ -187,7 +187,7 @@ class HomeFragment : Fragment() { private fun navigateToSettingsFragment() { findNavController().navigate( - HomeFragmentDirections.actionNavigationHomeToNavigationSettings() + HomeFragmentDirections.actionHomeFragmentToSettingsFragment() ) } } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt index 771afffb..cb635204 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/LibraryFragment.kt @@ -1,8 +1,11 @@ package dev.jdtech.jellyfin.fragments +import android.app.UiModeManager import android.content.SharedPreferences +import android.content.res.Configuration import android.os.Bundle import android.view.* +import androidx.appcompat.app.AppCompatActivity import androidx.core.view.MenuHost import androidx.core.view.MenuProvider import androidx.core.view.isVisible @@ -14,6 +17,7 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.paging.LoadState +import androidx.recyclerview.widget.LinearSnapHelper import dagger.hilt.android.AndroidEntryPoint import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.viewmodels.LibraryViewModel @@ -33,6 +37,7 @@ import javax.inject.Inject class LibraryFragment : Fragment() { private lateinit var binding: FragmentLibraryBinding + private lateinit var uiModeManager: UiModeManager private val viewModel: LibraryViewModel by viewModels() private val args: LibraryFragmentArgs by navArgs() @@ -46,6 +51,8 @@ class LibraryFragment : Fragment() { savedInstanceState: Bundle? ): View { binding = FragmentLibraryBinding.inflate(inflater, container, false) + uiModeManager = + requireContext().getSystemService(AppCompatActivity.UI_MODE_SERVICE) as UiModeManager return binding.root } @@ -91,6 +98,8 @@ class LibraryFragment : Fragment() { }, viewLifecycleOwner, Lifecycle.State.RESUMED ) + binding.title?.text = args.libraryName + binding.errorLayout.errorRetryButton.setOnClickListener { 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 = ViewItemPagingAdapter(ViewItemPagingAdapter.OnClickListener { item -> navigateToMediaInfoFragment(item) @@ -182,12 +196,22 @@ class LibraryFragment : Fragment() { } private fun navigateToMediaInfoFragment(item: BaseItemDto) { - findNavController().navigate( - LibraryFragmentDirections.actionLibraryFragmentToMediaInfoFragment( - item.id, - item.name, - item.type + if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) { + findNavController().navigate( + LibraryFragmentDirections.actionLibraryFragmentToMediaDetailFragment( + item.id, + item.name, + item.type + ) ) - ) + } else { + findNavController().navigate( + LibraryFragmentDirections.actionLibraryFragmentToMediaInfoFragment( + item.id, + item.name, + item.type + ) + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/LoginFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/LoginFragment.kt index 285fc87f..af712d24 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/LoginFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/LoginFragment.kt @@ -1,10 +1,14 @@ package dev.jdtech.jellyfin.fragments +import android.app.UiModeManager +import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.AppCompatEditText import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels @@ -22,6 +26,7 @@ import timber.log.Timber class LoginFragment : Fragment() { private lateinit var binding: FragmentLoginBinding + private lateinit var uiModeManager: UiModeManager private val viewModel: LoginViewModel by viewModels() override fun onCreateView( @@ -29,8 +34,10 @@ class LoginFragment : Fragment() { savedInstanceState: Bundle? ): View { 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) { EditorInfo.IME_ACTION_GO -> { login() @@ -61,7 +68,7 @@ class LoginFragment : Fragment() { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.navigateToMain.collect { if (it) { - navigateToMainActivity() + navigateToHomeFragment() } } } @@ -78,22 +85,34 @@ class LoginFragment : Fragment() { private fun bindUiStateError(uiState: LoginViewModel.UiState.Error) { binding.buttonLogin.isEnabled = true 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() { binding.buttonLogin.isEnabled = false 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() { - val username = binding.editTextUsername.text.toString() - val password = binding.editTextPassword.text.toString() + val username = (binding.editTextUsername as AppCompatEditText).text.toString() + val password = (binding.editTextPassword as AppCompatEditText).text.toString() viewModel.login(username, password) } - private fun navigateToMainActivity() { - findNavController().navigate(LoginFragmentDirections.actionLoginFragmentToNavigationHome()) + private fun navigateToHomeFragment() { + if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) { + findNavController().navigate(LoginFragmentDirections.actionLoginFragmentToHomeFragmentTv()) + } else { + findNavController().navigate(LoginFragmentDirections.actionLoginFragmentToHomeFragment()) + } } } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/tv/ui/HomeFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/tv/ui/HomeFragment.kt index a956dc81..73a70b2f 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/tv/ui/HomeFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/tv/ui/HomeFragment.kt @@ -1,16 +1,16 @@ package dev.jdtech.jellyfin.tv.ui +import android.app.UiModeManager +import android.content.res.Configuration import android.os.Bundle import android.view.KeyEvent.KEYCODE_DPAD_DOWN import android.view.KeyEvent.KEYCODE_DPAD_DOWN_LEFT import android.view.View import android.widget.ImageButton +import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.viewModels import androidx.leanback.app.BrowseSupportFragment -import androidx.leanback.widget.ArrayObjectAdapter -import androidx.leanback.widget.HeaderItem -import androidx.leanback.widget.ListRow -import androidx.leanback.widget.ListRowPresenter +import androidx.leanback.widget.* import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -18,6 +18,7 @@ import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.adapters.HomeItem +import dev.jdtech.jellyfin.fragments.HomeFragmentDirections import dev.jdtech.jellyfin.viewmodels.HomeViewModel import kotlinx.coroutines.launch import org.jellyfin.sdk.model.api.BaseItemDto @@ -29,12 +30,21 @@ internal class HomeFragment : BrowseSupportFragment() { private val viewModel: HomeViewModel by viewModels() private lateinit var rowsAdapter: ArrayObjectAdapter + private lateinit var uiModeManager: UiModeManager + + private val adapterMap = mutableMapOf() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + uiModeManager = + requireContext().getSystemService(AppCompatActivity.UI_MODE_SERVICE) as UiModeManager + + val rowPresenter = ListRowPresenter() + rowPresenter.selectEffectEnabled = false + headersState = HEADERS_ENABLED - rowsAdapter = ArrayObjectAdapter(ListRowPresenter()) + rowsAdapter = ArrayObjectAdapter(rowPresenter) adapter = rowsAdapter } @@ -59,21 +69,42 @@ internal class HomeFragment : BrowseSupportFragment() { Timber.d("$uiState") when (uiState) { is HomeViewModel.UiState.Normal -> bindUiStateNormal(uiState) - is HomeViewModel.UiState.Loading -> Unit + is HomeViewModel.UiState.Loading -> bindUiStateLoading() is HomeViewModel.UiState.Error -> Unit } } } } + + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.loadData(includeLibraries = true) + } + } + } + + private val diffCallbackListRow = object : DiffCallback() { + 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) { + progressBarManager.hide() uiState.apply { - rowsAdapter.clear() - homeItems.map { section -> rowsAdapter.add(section.toListRow()) } + rowsAdapter.setItems(homeItems.map { homeItem -> homeItem.toListRow() }, diffCallbackListRow) } } + private fun bindUiStateLoading() { + progressBarManager.show() + } + private fun HomeItem.toListRow(): ListRow { return ListRow( toHeader(), @@ -83,6 +114,7 @@ internal class HomeFragment : BrowseSupportFragment() { private fun HomeItem.toHeader(): HeaderItem { return when (this) { + is HomeItem.Libraries -> HeaderItem(section.name) is HomeItem.Section -> HeaderItem(homeSection.name) is HomeItem.ViewItem -> HeaderItem( String.format( @@ -93,14 +125,59 @@ internal class HomeFragment : BrowseSupportFragment() { } } + val diffCallback = object : DiffCallback() { + 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 { - return when (this) { - is HomeItem.Section -> ArrayObjectAdapter(DynamicMediaItemPresenter { item -> - navigateToMediaDetailFragment(item) - }).apply { addAll(0, homeSection.items) } - is HomeItem.ViewItem -> ArrayObjectAdapter(MediaItemPresenter { item -> - navigateToMediaDetailFragment(item) - }).apply { addAll(0, view.items) } + val name = this.toHeader().name + val items = when (this) { + is HomeItem.Libraries -> section.items + is HomeItem.Section -> homeSection.items + is HomeItem.ViewItem -> 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() { findNavController().navigate( - HomeFragmentDirections.actionNavigationHomeToSettings() + HomeFragmentDirections.actionHomeFragmentToSettingsFragment() ) } } diff --git a/app/src/main/java/dev/jdtech/jellyfin/tv/ui/MediaDetailFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/tv/ui/MediaDetailFragment.kt index c8b543e6..1fd8b9e6 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/tv/ui/MediaDetailFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/tv/ui/MediaDetailFragment.kt @@ -121,11 +121,9 @@ internal class MediaDetailFragment : Fragment() { when (viewModel.played) { true -> { viewModel.markAsUnplayed(args.itemId) - val typedValue = TypedValue() - requireActivity().theme.resolveAttribute(R.attr.colorOnSecondaryContainer, typedValue, true) binding.checkButton.imageTintList = ColorStateList.valueOf( resources.getColor( - typedValue.resourceId, + R.color.white, requireActivity().theme ) ) @@ -147,11 +145,9 @@ internal class MediaDetailFragment : Fragment() { true -> { viewModel.unmarkAsFavorite(args.itemId) binding.favoriteButton.setImageResource(R.drawable.ic_heart) - val typedValue = TypedValue() - requireActivity().theme.resolveAttribute(R.attr.colorOnSecondaryContainer, typedValue, true) binding.favoriteButton.imageTintList = ColorStateList.valueOf( resources.getColor( - typedValue.resourceId, + R.color.white, requireActivity().theme ) ) @@ -168,16 +164,14 @@ internal class MediaDetailFragment : Fragment() { } } } - - binding.backButton.setOnClickListener { activity?.onBackPressed() } } private fun bindUiStateNormal(uiState: MediaInfoViewModel.UiState.Normal) { uiState.apply { - binding.seasonTitle.isVisible = seasons.isNotEmpty() + binding.seasonsLayout.isVisible = seasons.isNotEmpty() val seasonsAdapter = binding.seasonsRow.gridView.adapter as ViewItemListAdapter seasonsAdapter.submitList(seasons) - binding.castTitle.isVisible = actors.isNotEmpty() + binding.castLayout.isVisible = actors.isNotEmpty() val actorsAdapter = binding.castRow.gridView.adapter as PersonListAdapter actorsAdapter.submitList(actors) @@ -217,11 +211,6 @@ internal class MediaDetailFragment : Fragment() { ) 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.year.text = dateString binding.playtime.text = runTime diff --git a/app/src/main/java/dev/jdtech/jellyfin/tv/ui/MediaSectionPresenter.kt b/app/src/main/java/dev/jdtech/jellyfin/tv/ui/MediaSectionPresenter.kt index f7707a25..9f72ffdf 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/tv/ui/MediaSectionPresenter.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/tv/ui/MediaSectionPresenter.kt @@ -1,6 +1,5 @@ package dev.jdtech.jellyfin.tv.ui -import android.content.Context.LAYOUT_INFLATER_SERVICE import android.util.TypedValue import android.view.LayoutInflater import android.view.View @@ -10,25 +9,52 @@ import androidx.databinding.DataBindingUtil import androidx.leanback.widget.Presenter import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.databinding.BaseItemBinding +import dev.jdtech.jellyfin.databinding.CollectionItemBinding import dev.jdtech.jellyfin.databinding.HomeEpisodeItemBinding import org.jellyfin.sdk.model.api.BaseItemDto 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(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() { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { - val mediaView = - BaseItemBinding - .inflate(parent.context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater) - .root - return ViewHolder(mediaView) + return ViewHolder( + BaseItemBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ).root + ) } override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) { if (item is BaseItemDto) { DataBindingUtil.getBinding(viewHolder.view)?.apply { 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 = if (item.userData?.unplayedItemCount != null && item.userData?.unplayedItemCount!! > 0) View.VISIBLE else View.GONE this.itemLayout.layoutParams.width = @@ -45,11 +71,13 @@ class MediaItemPresenter(private val onClick: (BaseItemDto) -> Unit) : Presenter class DynamicMediaItemPresenter(private val onClick: (BaseItemDto) -> Unit) : Presenter() { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { - val mediaView = - HomeEpisodeItemBinding - .inflate(parent.context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater) - .root - return ViewHolder(mediaView) + return ViewHolder( + HomeEpisodeItemBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ).root + ) } override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) { @@ -59,7 +87,8 @@ class DynamicMediaItemPresenter(private val onClick: (BaseItemDto) -> Unit) : Pr item.userData?.playedPercentage?.toInt()?.let { progressBar.layoutParams.width = TypedValue.applyDimension( 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 } diff --git a/app/src/main/java/dev/jdtech/jellyfin/tv/ui/TvAddServerFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/tv/ui/TvAddServerFragment.kt deleted file mode 100644 index e5aff469..00000000 --- a/app/src/main/java/dev/jdtech/jellyfin/tv/ui/TvAddServerFragment.kt +++ /dev/null @@ -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()) - } -} \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/tv/ui/TvLoginFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/tv/ui/TvLoginFragment.kt deleted file mode 100644 index f83e81f1..00000000 --- a/app/src/main/java/dev/jdtech/jellyfin/tv/ui/TvLoginFragment.kt +++ /dev/null @@ -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()) - } -} \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/AddServerViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/AddServerViewModel.kt index 99f195d5..0008ba7d 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/AddServerViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/AddServerViewModel.kt @@ -10,11 +10,8 @@ import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.database.Server import dev.jdtech.jellyfin.database.ServerDatabaseDao -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.jellyfin.sdk.discovery.RecommendedServerInfo import org.jellyfin.sdk.discovery.RecommendedServerInfoScore import org.jellyfin.sdk.discovery.RecommendedServerIssue @@ -68,7 +65,6 @@ constructor( RecommendedServerInfoScore.OK ) - val greatServers = mutableListOf() val goodServers = mutableListOf() val okServers = mutableListOf() @@ -76,9 +72,6 @@ constructor( .onCompletion { if (serverFound) return@onCompletion when { - greatServers.isNotEmpty() -> { - connectToServer(greatServers.first()) - } goodServers.isNotEmpty() -> { val issuesString = createIssuesString(goodServers.first()) Toast.makeText( @@ -109,6 +102,8 @@ constructor( RecommendedServerInfoScore.BAD -> Unit } } + } catch (e: CancellationException) { + } catch (e: Exception) { _uiState.emit( UiState.Error( diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt index 991ca534..14e17af4 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/HomeViewModel.kt @@ -38,18 +38,25 @@ class HomeViewModel @Inject internal constructor( } init { - loadData(updateCapabilities = true) + viewModelScope.launch { + try { + repository.postCapabilities() + } catch (e: Exception) { + } + } } - fun refreshData() = loadData(updateCapabilities = false) - - private fun loadData(updateCapabilities: Boolean) { + fun loadData(includeLibraries: Boolean = false) { viewModelScope.launch { _uiState.emit(UiState.Loading) try { - if (updateCapabilities) repository.postCapabilities() + val items = mutableListOf() - val updated = loadDynamicItems() + loadViews() + if (includeLibraries) { + items.add(loadLibraries()) + } + + val updated = items + loadDynamicItems() + loadViews() withContext(Dispatchers.Default) { 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
{ val resumeItems = repository.getResumeItems() val nextUpItems = repository.getNextUp() diff --git a/app/src/main/res/drawable-television/button_accent_background.xml b/app/src/main/res/drawable-television/button_accent_background.xml new file mode 100644 index 00000000..7cb3b3f7 --- /dev/null +++ b/app/src/main/res/drawable-television/button_accent_background.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-television/button_setup_background.xml b/app/src/main/res/drawable-television/button_setup_background.xml new file mode 100644 index 00000000..5079712d --- /dev/null +++ b/app/src/main/res/drawable-television/button_setup_background.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/focus_border.xml b/app/src/main/res/drawable/focus_border.xml new file mode 100644 index 00000000..b2dc9917 --- /dev/null +++ b/app/src/main/res/drawable/focus_border.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_play.xml b/app/src/main/res/drawable/ic_play.xml index 0bf84f11..17da1f55 100644 --- a/app/src/main/res/drawable/ic_play.xml +++ b/app/src/main/res/drawable/ic_play.xml @@ -2,8 +2,7 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> + android:viewportHeight="24"> - + android:layout_height="match_parent"> + app:navGraph="@navigation/app_navigation" /> \ No newline at end of file diff --git a/app/src/main/res/layout-television/base_item.xml b/app/src/main/res/layout-television/base_item.xml new file mode 100644 index 00000000..21d15548 --- /dev/null +++ b/app/src/main/res/layout-television/base_item.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-television/browse_support_fragment_title_view.xml b/app/src/main/res/layout-television/browse_support_fragment_title_view.xml index e7abcde5..8cd301fc 100644 --- a/app/src/main/res/layout-television/browse_support_fragment_title_view.xml +++ b/app/src/main/res/layout-television/browse_support_fragment_title_view.xml @@ -1,38 +1,34 @@ - + tools:ignore="MissingDefaultResource"> + android:src="@drawable/ic_settings" + app:layout_constraintEnd_toStartOf="@+id/clock" + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="12:00" /> \ No newline at end of file diff --git a/app/src/main/res/layout-television/collection_item.xml b/app/src/main/res/layout-television/collection_item.xml new file mode 100644 index 00000000..df6a573b --- /dev/null +++ b/app/src/main/res/layout-television/collection_item.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-television/tv_add_server_fragment.xml b/app/src/main/res/layout-television/fragment_add_server.xml similarity index 90% rename from app/src/main/res/layout-television/tv_add_server_fragment.xml rename to app/src/main/res/layout-television/fragment_add_server.xml index b4863eeb..81cb1302 100644 --- a/app/src/main/res/layout-television/tv_add_server_fragment.xml +++ b/app/src/main/res/layout-television/fragment_add_server.xml @@ -1,14 +1,15 @@ - - + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fillViewport="true" + tools:context=".fragments.AddServerFragment"> + android:layout_height="wrap_content"> @@ -76,4 +78,4 @@ - + diff --git a/app/src/main/res/layout-television/fragment_library.xml b/app/src/main/res/layout-television/fragment_library.xml new file mode 100644 index 00000000..89133813 --- /dev/null +++ b/app/src/main/res/layout-television/fragment_library.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-television/tv_login_fragment.xml b/app/src/main/res/layout-television/fragment_login.xml similarity index 92% rename from app/src/main/res/layout-television/tv_login_fragment.xml rename to app/src/main/res/layout-television/fragment_login.xml index f8474f31..e3c115c7 100644 --- a/app/src/main/res/layout-television/tv_login_fragment.xml +++ b/app/src/main/res/layout-television/fragment_login.xml @@ -1,14 +1,15 @@ - - + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fillViewport="true" + tools:context=".fragments.LoginFragment"> + android:layout_height="wrap_content"> + - + diff --git a/app/src/main/res/layout-television/home_episode_item.xml b/app/src/main/res/layout-television/home_episode_item.xml new file mode 100644 index 00000000..e67188e3 --- /dev/null +++ b/app/src/main/res/layout-television/home_episode_item.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-television/media_detail_fragment.xml b/app/src/main/res/layout-television/media_detail_fragment.xml index 57ec35e0..bc100652 100644 --- a/app/src/main/res/layout-television/media_detail_fragment.xml +++ b/app/src/main/res/layout-television/media_detail_fragment.xml @@ -19,27 +19,14 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - - @@ -47,6 +34,7 @@ android:id="@+id/clock" android:layout_width="wrap_content" android:layout_height="24dp" + android:layout_marginTop="12dp" android:layout_marginEnd="24dp" android:gravity="center_vertical" android:textSize="18sp" @@ -59,189 +47,209 @@ android:layout_width="320dp" android:layout_height="180dp" android:layout_marginStart="@dimen/horizontal_margin" + android:layout_marginTop="12dp" android:scaleType="centerCrop" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/title" /> - - + app:shapeAppearance="@style/ShapeAppearanceOverlay.Findroid.Image" + app:strokeColor="@null" /> - + app:layout_constraintTop_toTopOf="@id/poster"> - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + app:layout_constraintTop_toBottomOf="@id/main_info"> - + + + + + + + app:layout_constraintTop_toBottomOf="@id/seasons_layout"> - + - + + + diff --git a/app/src/main/res/layout-television/person_item.xml b/app/src/main/res/layout-television/person_item.xml new file mode 100644 index 00000000..c5491990 --- /dev/null +++ b/app/src/main/res/layout-television/person_item.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-w600dp-television/activity_main.xml b/app/src/main/res/layout-w600dp-television/activity_main.xml new file mode 120000 index 00000000..48cb2e0b --- /dev/null +++ b/app/src/main/res/layout-w600dp-television/activity_main.xml @@ -0,0 +1 @@ +../layout-television/activity_main.xml \ No newline at end of file diff --git a/app/src/main/res/layout-w600dp/activity_main.xml b/app/src/main/res/layout-w600dp/activity_main.xml new file mode 100644 index 00000000..2847a342 --- /dev/null +++ b/app/src/main/res/layout-w600dp/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-w600dp/activity_main_app.xml b/app/src/main/res/layout-w600dp/activity_main_app.xml deleted file mode 100644 index 82714c02..00000000 --- a/app/src/main/res/layout-w600dp/activity_main_app.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..9911d987 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main_app.xml b/app/src/main/res/layout/activity_main_app.xml deleted file mode 100644 index ab84762f..00000000 --- a/app/src/main/res/layout/activity_main_app.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/home_episode_item.xml b/app/src/main/res/layout/home_episode_item.xml index 90c936c7..adb58f43 100644 --- a/app/src/main/res/layout/home_episode_item.xml +++ b/app/src/main/res/layout/home_episode_item.xml @@ -21,7 +21,7 @@ @@ -44,7 +44,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:ellipsize="end" - android:maxLines="2" + android:maxLines="1" android:text="@{person.role}" android:textAppearance="@style/TextAppearance.Material3.BodySmall" tools:text="Alita" /> diff --git a/app/src/main/res/navigation/app_navigation.xml b/app/src/main/res/navigation/app_navigation.xml index 4c390690..2323af3b 100644 --- a/app/src/main/res/navigation/app_navigation.xml +++ b/app/src/main/res/navigation/app_navigation.xml @@ -28,7 +28,7 @@ android:id="@+id/action_navigation_home_to_episodeBottomSheetFragment" app:destination="@id/episodeBottomSheetFragment" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-television/dimens.xml b/app/src/main/res/values-television/dimens.xml index e0bc502a..5492eb53 100644 --- a/app/src/main/res/values-television/dimens.xml +++ b/app/src/main/res/values-television/dimens.xml @@ -11,4 +11,6 @@ 48dp 16sp + 6 + \ No newline at end of file diff --git a/app/src/main/res/values-television/themes.xml b/app/src/main/res/values-television/themes.xml index 862c7243..8b60622e 100644 --- a/app/src/main/res/values-television/themes.xml +++ b/app/src/main/res/values-television/themes.xml @@ -1,6 +1,6 @@ -