Add basic tv support (#58)
* Add basic leanback support * Add TV home fragment Adds basic media browsing screen for TV. Shows Home screen media. * Fix double emit when loading user views * Fix bug when going back to this screen would duplicate menu items * Add basic media detail fragment * Add ability to navigate to detail fragment * Fix imports and null safe calls * Fix displaying of home item view type media files * Playback refactor * Add basic Tv player controls and split PlayerActivity * Update strings * Add progress bar to partially played items on TV home screen * Track selection dialog PoC * Update track selection WIP * Show track selection of focus change * Fix series display from home * Minor updates * Add back button to media detail * Zero effort add server and login * Fix colors * Fix back button from home going back to init fragment * Add settings button to home screen * Fix crash after goig back from media detail fragment * Show seasons and cast * Merge branch 'develop' into add_basic_tv_support # Conflicts: # app/src/main/java/dev/jdtech/jellyfin/MainActivity.kt # app/src/main/java/dev/jdtech/jellyfin/dialogs/VideoVersionDialogFragment.kt # app/src/main/res/navigation/app_navigation.xml * Fix cast title being shown with empty cast list * Remove useless method * Remove unused parameter * Fix crash due to colorOnPrimary not existing in Leanback styles * Remove unused theme * Fix home to addserver fragment navigation * Reuse home item layouts This creates some duplicate code which will probably be cleaned up later * Ignore more MissingDefaultResource * Add banner Co-authored-by: Jarne Demeulemeester <32322857+jarnedemeulemeester@users.noreply.github.com>
This commit is contained in:
parent
532e9adac1
commit
07a9e2a853
46 changed files with 1978 additions and 89 deletions
|
@ -54,6 +54,8 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation("androidx.leanback:leanback:1.2.0-alpha01")
|
||||||
|
|
||||||
implementation("androidx.core:core-ktx:1.6.0")
|
implementation("androidx.core:core-ktx:1.6.0")
|
||||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
||||||
implementation("androidx.appcompat:appcompat:1.3.1")
|
implementation("androidx.appcompat:appcompat:1.3.1")
|
||||||
|
|
|
@ -5,28 +5,56 @@
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="28" />
|
android:maxSdkVersion="28" />
|
||||||
|
|
||||||
|
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||||
|
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".BaseApplication"
|
android:name=".BaseApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:fullBackupOnly="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:banner="@mipmap/ic_banner"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Findroid"
|
android:theme="@style/Theme.Findroid"
|
||||||
android:usesCleartextTraffic="true">
|
android:usesCleartextTraffic="true">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".PlayerActivity"
|
android:name=".PlayerActivity"
|
||||||
android:screenOrientation="userLandscape" />
|
android:screenOrientation="userLandscape" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".tv.TvPlayerActivity"
|
||||||
|
android:screenOrientation="userLandscape" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.FindroidSplashScreen"
|
android:theme="@style/Theme.FindroidSplashScreen"
|
||||||
android:windowSoftInputMode="adjustPan">
|
android:windowSoftInputMode="adjustPan">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<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>
|
</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" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
69
app/src/main/java/dev/jdtech/jellyfin/BasePlayerActivity.kt
Normal file
69
app/src/main/java/dev/jdtech/jellyfin/BasePlayerActivity.kt
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
package dev.jdtech.jellyfin
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import com.google.android.exoplayer2.trackselection.MappingTrackSelector
|
||||||
|
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
|
||||||
|
|
||||||
|
abstract class BasePlayerActivity: AppCompatActivity() {
|
||||||
|
|
||||||
|
abstract val viewModel: PlayerActivityViewModel
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
viewModel.playWhenReady = viewModel.player.playWhenReady == true
|
||||||
|
viewModel.player.playWhenReady = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
viewModel.player.playWhenReady = viewModel.playWhenReady
|
||||||
|
hideSystemUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
protected fun hideSystemUI() {
|
||||||
|
// These methods are deprecated but we still use them because the new WindowInsetsControllerCompat has a bug which makes the action bar reappear
|
||||||
|
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
|
||||||
|
View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||||
|
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
|
||||||
|
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
window.attributes.layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun isRendererType(
|
||||||
|
mappedTrackInfo: MappingTrackSelector.MappedTrackInfo,
|
||||||
|
rendererIndex: Int,
|
||||||
|
type: Int
|
||||||
|
): Boolean {
|
||||||
|
val trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex)
|
||||||
|
if (trackGroupArray.length == 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val trackType = mappedTrackInfo.getRendererType(rendererIndex)
|
||||||
|
return type == trackType
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun configureInsets(playerControls: View) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
playerControls
|
||||||
|
.setOnApplyWindowInsetsListener { _, windowInsets ->
|
||||||
|
val cutout = windowInsets.displayCutout
|
||||||
|
playerControls.updatePadding(
|
||||||
|
left = cutout?.safeInsetLeft ?: 0,
|
||||||
|
top = cutout?.safeInsetTop ?: 0,
|
||||||
|
right = cutout?.safeInsetRight ?: 0,
|
||||||
|
bottom = cutout?.safeInsetBottom ?: 0,
|
||||||
|
)
|
||||||
|
return@setOnApplyWindowInsetsListener windowInsets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,29 +3,29 @@ package dev.jdtech.jellyfin
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import androidx.navigation.ui.AppBarConfiguration
|
import androidx.navigation.ui.AppBarConfiguration
|
||||||
import androidx.navigation.ui.setupActionBarWithNavController
|
import androidx.navigation.ui.setupActionBarWithNavController
|
||||||
import androidx.navigation.ui.setupWithNavController
|
import androidx.navigation.ui.setupWithNavController
|
||||||
|
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.databinding.ActivityMainBinding
|
import dev.jdtech.jellyfin.databinding.ActivityMainAppBinding
|
||||||
import dev.jdtech.jellyfin.fragments.HomeFragmentDirections
|
import dev.jdtech.jellyfin.fragments.HomeFragmentDirections
|
||||||
import dev.jdtech.jellyfin.viewmodels.MainViewModel
|
import dev.jdtech.jellyfin.viewmodels.MainViewModel
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainAppBinding
|
||||||
private val viewModel: MainViewModel by viewModels()
|
private val viewModel: MainViewModel by viewModels()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
binding = ActivityMainAppBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
|
34
app/src/main/java/dev/jdtech/jellyfin/MainActivityTv.kt
Normal file
34
app/src/main/java/dev/jdtech/jellyfin/MainActivityTv.kt
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package dev.jdtech.jellyfin
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import dev.jdtech.jellyfin.databinding.ActivityMainTvBinding
|
||||||
|
import dev.jdtech.jellyfin.tv.ui.HomeFragmentDirections
|
||||||
|
import dev.jdtech.jellyfin.viewmodels.MainViewModel
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
internal class MainActivityTv : FragmentActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityMainTvBinding
|
||||||
|
private val viewModel: MainViewModel by viewModels()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
viewModel.navigateToAddServer.observe(this, {
|
||||||
|
if (it) {
|
||||||
|
navController.navigate(HomeFragmentDirections.actionHomeFragmentToAddServerFragment())
|
||||||
|
viewModel.doneNavigateToAddServer()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,15 @@
|
||||||
package dev.jdtech.jellyfin
|
package dev.jdtech.jellyfin
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.navigation.navArgs
|
import androidx.navigation.navArgs
|
||||||
import com.google.android.exoplayer2.C
|
import com.google.android.exoplayer2.C
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector
|
|
||||||
import com.google.android.exoplayer2.ui.TrackSelectionDialogBuilder
|
import com.google.android.exoplayer2.ui.TrackSelectionDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.databinding.ActivityPlayerBinding
|
import dev.jdtech.jellyfin.databinding.ActivityPlayerBinding
|
||||||
|
@ -27,16 +24,17 @@ import timber.log.Timber
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class PlayerActivity : AppCompatActivity() {
|
class PlayerActivity : BasePlayerActivity() {
|
||||||
|
|
||||||
private lateinit var binding: ActivityPlayerBinding
|
private lateinit var binding: ActivityPlayerBinding
|
||||||
private val viewModel: PlayerActivityViewModel by viewModels()
|
override val viewModel: PlayerActivityViewModel by viewModels()
|
||||||
private val args: PlayerActivityArgs by navArgs()
|
private val args: PlayerActivityArgs by navArgs()
|
||||||
private val audioController by lazy { AudioController(this) }
|
private val audioController by lazy { AudioController(this) }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Timber.d("Creating player activity")
|
Timber.d("Creating player activity")
|
||||||
|
|
||||||
binding = ActivityPlayerBinding.inflate(layoutInflater)
|
binding = ActivityPlayerBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
@ -46,19 +44,7 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
val playerControls = binding.playerView.findViewById<View>(R.id.player_controls)
|
val playerControls = binding.playerView.findViewById<View>(R.id.player_controls)
|
||||||
setupVolumeControl()
|
setupVolumeControl()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
configureInsets(playerControls)
|
||||||
binding.playerView.findViewById<View>(R.id.player_controls)
|
|
||||||
.setOnApplyWindowInsetsListener { _, windowInsets ->
|
|
||||||
val cutout = windowInsets.displayCutout
|
|
||||||
playerControls.updatePadding(
|
|
||||||
left = cutout?.safeInsetLeft ?: 0,
|
|
||||||
top = cutout?.safeInsetTop ?: 0,
|
|
||||||
right = cutout?.safeInsetRight ?: 0,
|
|
||||||
bottom = cutout?.safeInsetBottom ?: 0,
|
|
||||||
)
|
|
||||||
return@setOnApplyWindowInsetsListener windowInsets
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.playerView.findViewById<View>(R.id.back_button).setOnClickListener {
|
binding.playerView.findViewById<View>(R.id.back_button).setOnClickListener {
|
||||||
onBackPressed()
|
onBackPressed()
|
||||||
|
@ -173,18 +159,6 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
hideSystemUI()
|
hideSystemUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
viewModel.playWhenReady = viewModel.player.playWhenReady == true
|
|
||||||
viewModel.player.playWhenReady = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
viewModel.player.playWhenReady = viewModel.playWhenReady
|
|
||||||
hideSystemUI()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupVolumeControl() {
|
private fun setupVolumeControl() {
|
||||||
val height = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
val height = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
windowManager.currentWindowMetrics.bounds.height()
|
windowManager.currentWindowMetrics.bounds.height()
|
||||||
|
@ -198,32 +172,5 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
threshold = max(height / 8, 100)
|
threshold = max(height / 8, 100)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun hideSystemUI() {
|
|
||||||
// These methods are deprecated but we still use them because the new WindowInsetsControllerCompat has a bug which makes the action bar reappear
|
|
||||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
|
|
||||||
View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
|
||||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
|
|
||||||
|
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
window.attributes.layoutInDisplayCutoutMode =
|
|
||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isRendererType(
|
|
||||||
mappedTrackInfo: MappingTrackSelector.MappedTrackInfo,
|
|
||||||
rendererIndex: Int,
|
|
||||||
type: Int
|
|
||||||
): Boolean {
|
|
||||||
val trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex)
|
|
||||||
if (trackGroupArray.length == 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val trackType = mappedTrackInfo.getRendererType(rendererIndex)
|
|
||||||
return type == trackType
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,10 @@ import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import java.lang.IllegalStateException
|
||||||
import dev.jdtech.jellyfin.R
|
import dev.jdtech.jellyfin.R
|
||||||
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
|
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
|
|
||||||
import java.lang.IllegalStateException
|
|
||||||
|
|
||||||
class VideoVersionDialogFragment(
|
class VideoVersionDialogFragment(
|
||||||
private val item: BaseItemDto,
|
private val item: BaseItemDto,
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
package dev.jdtech.jellyfin.fragments
|
package dev.jdtech.jellyfin.fragments
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
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 androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import dev.jdtech.jellyfin.R
|
||||||
import dev.jdtech.jellyfin.databinding.FragmentAddServerBinding
|
import dev.jdtech.jellyfin.databinding.FragmentAddServerBinding
|
||||||
import dev.jdtech.jellyfin.viewmodels.AddServerViewModel
|
import dev.jdtech.jellyfin.viewmodels.AddServerViewModel
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ class AddServerFragment : Fragment() {
|
||||||
viewModel.checkServer(serverAddress)
|
viewModel.checkServer(serverAddress)
|
||||||
binding.progressCircular.visibility = View.VISIBLE
|
binding.progressCircular.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
binding.editTextServerAddressLayout.error = "Empty server address"
|
binding.editTextServerAddressLayout.error = resources.getString(R.string.add_server_empty_error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
package dev.jdtech.jellyfin.fragments
|
package dev.jdtech.jellyfin.fragments
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.LayoutInflater
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
|
|
@ -26,8 +26,8 @@ import dev.jdtech.jellyfin.utils.requestDownload
|
||||||
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
|
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
|
||||||
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
|
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import timber.log.Timber
|
|
||||||
import org.jellyfin.sdk.model.serializer.toUUID
|
import org.jellyfin.sdk.model.serializer.toUUID
|
||||||
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
@ -263,7 +263,7 @@ class MediaInfoFragment : Fragment() {
|
||||||
) {
|
) {
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
MediaInfoFragmentDirections.actionMediaInfoFragmentToPlayerActivity(
|
MediaInfoFragmentDirections.actionMediaInfoFragmentToPlayerActivity(
|
||||||
playerItems,
|
playerItems
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
155
app/src/main/java/dev/jdtech/jellyfin/tv/TvPlayerActivity.kt
Normal file
155
app/src/main/java/dev/jdtech/jellyfin/tv/TvPlayerActivity.kt
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
package dev.jdtech.jellyfin.tv
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.PopupWindow
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.navigation.navArgs
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import dev.jdtech.jellyfin.BasePlayerActivity
|
||||||
|
import dev.jdtech.jellyfin.PlayerActivityArgs
|
||||||
|
import dev.jdtech.jellyfin.R
|
||||||
|
import dev.jdtech.jellyfin.databinding.ActivityPlayerTvBinding
|
||||||
|
import dev.jdtech.jellyfin.mpv.MPVPlayer
|
||||||
|
import dev.jdtech.jellyfin.mpv.TrackType.AUDIO
|
||||||
|
import dev.jdtech.jellyfin.mpv.TrackType.SUBTITLE
|
||||||
|
import dev.jdtech.jellyfin.tv.ui.TrackSelectorAdapter
|
||||||
|
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
internal class TvPlayerActivity : BasePlayerActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityPlayerTvBinding
|
||||||
|
override val viewModel: PlayerActivityViewModel by viewModels()
|
||||||
|
private val args: PlayerActivityArgs by navArgs()
|
||||||
|
private var displayedPopup: PopupWindow? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
Timber.d("Player activity created.")
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
binding = ActivityPlayerTvBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
|
||||||
|
binding.playerView.player = viewModel.player
|
||||||
|
val playerControls = binding.playerView.findViewById<View>(R.id.tv_player_controls)
|
||||||
|
configureInsets(playerControls)
|
||||||
|
|
||||||
|
bind()
|
||||||
|
viewModel.initializePlayer(args.items)
|
||||||
|
hideSystemUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
|
return if (!binding.playerView.isControllerVisible) {
|
||||||
|
binding.playerView.showController()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bind() = with(binding.playerView) {
|
||||||
|
val videoNameTextView = findViewById<TextView>(R.id.video_name)
|
||||||
|
viewModel.currentItemTitle.observe(this@TvPlayerActivity, { title ->
|
||||||
|
videoNameTextView.text = title
|
||||||
|
})
|
||||||
|
|
||||||
|
findViewById<ImageButton>(R.id.exo_play_pause).apply {
|
||||||
|
setOnClickListener {
|
||||||
|
when {
|
||||||
|
viewModel.player.isPlaying -> {
|
||||||
|
viewModel.player.pause()
|
||||||
|
setImageDrawable(resources.getDrawable(R.drawable.ic_play))
|
||||||
|
}
|
||||||
|
viewModel.player.isLoading -> Unit
|
||||||
|
else -> {
|
||||||
|
viewModel.player.play()
|
||||||
|
setImageDrawable(resources.getDrawable(R.drawable.ic_pause))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<View>(R.id.back_button).setOnClickListener {
|
||||||
|
onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
bindAudioControl()
|
||||||
|
bindSubtitleControl()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindAudioControl() {
|
||||||
|
val audioBtn = binding.playerView.findViewById<ImageButton>(R.id.btn_audio_track)
|
||||||
|
|
||||||
|
audioBtn.setOnFocusChangeListener { v, hasFocus ->
|
||||||
|
displayedPopup = if (hasFocus) {
|
||||||
|
val items = viewModel.currentSubtitleTracks.toUiTrack()
|
||||||
|
audioBtn.showPopupWindowAbove(items, AUDIO)
|
||||||
|
} else {
|
||||||
|
displayedPopup?.dismiss()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindSubtitleControl() {
|
||||||
|
val subtitleBtn = binding.playerView.findViewById<ImageButton>(R.id.btn_subtitle)
|
||||||
|
|
||||||
|
subtitleBtn.setOnFocusChangeListener { v, hasFocus ->
|
||||||
|
v.isFocusable = true
|
||||||
|
displayedPopup = if (hasFocus) {
|
||||||
|
val items = viewModel.currentSubtitleTracks.toUiTrack()
|
||||||
|
subtitleBtn.showPopupWindowAbove(items, SUBTITLE)
|
||||||
|
} else {
|
||||||
|
displayedPopup?.dismiss()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<MPVPlayer.Companion.Track>.toUiTrack() = map { track ->
|
||||||
|
TrackSelectorAdapter.Track(
|
||||||
|
title = track.title,
|
||||||
|
language = track.lang,
|
||||||
|
codec = track.codec,
|
||||||
|
selected = track.selected,
|
||||||
|
playerTrack = track
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun View.showPopupWindowAbove(
|
||||||
|
items: List<TrackSelectorAdapter.Track>,
|
||||||
|
type: String
|
||||||
|
): PopupWindow {
|
||||||
|
val popup = PopupWindow(this.context)
|
||||||
|
popup.contentView = LayoutInflater.from(context).inflate(R.layout.track_selector, null)
|
||||||
|
val recyclerView = popup.contentView.findViewById<RecyclerView>(R.id.track_selector)
|
||||||
|
|
||||||
|
recyclerView.adapter = TrackSelectorAdapter(items, viewModel, type) { popup.dismiss() }
|
||||||
|
|
||||||
|
val startViewCoords = IntArray(2)
|
||||||
|
getLocationInWindow(startViewCoords)
|
||||||
|
|
||||||
|
val itemHeight = resources.getDimension(R.dimen.track_selection_item_height).toInt()
|
||||||
|
val totalHeight = items.size * itemHeight
|
||||||
|
|
||||||
|
popup.showAsDropDown(
|
||||||
|
binding.root,
|
||||||
|
startViewCoords.first(),
|
||||||
|
startViewCoords.last() - totalHeight,
|
||||||
|
Gravity.TOP
|
||||||
|
)
|
||||||
|
|
||||||
|
return popup
|
||||||
|
}
|
||||||
|
}
|
102
app/src/main/java/dev/jdtech/jellyfin/tv/ui/HomeFragment.kt
Normal file
102
app/src/main/java/dev/jdtech/jellyfin/tv/ui/HomeFragment.kt
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
package dev.jdtech.jellyfin.tv.ui
|
||||||
|
|
||||||
|
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.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.navigation.fragment.findNavController
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import dev.jdtech.jellyfin.R
|
||||||
|
import dev.jdtech.jellyfin.adapters.HomeItem
|
||||||
|
import dev.jdtech.jellyfin.viewmodels.HomeViewModel
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
internal class HomeFragment : BrowseSupportFragment() {
|
||||||
|
|
||||||
|
private val viewModel: HomeViewModel by viewModels()
|
||||||
|
|
||||||
|
private lateinit var rowsAdapter: ArrayObjectAdapter
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
headersState = HEADERS_ENABLED
|
||||||
|
rowsAdapter = ArrayObjectAdapter(ListRowPresenter())
|
||||||
|
adapter = rowsAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
view.findViewById<ImageButton>(R.id.settings).apply {
|
||||||
|
setOnKeyListener { _, keyCode, _ ->
|
||||||
|
if (keyCode == KEYCODE_DPAD_DOWN || keyCode == KEYCODE_DPAD_DOWN_LEFT) {
|
||||||
|
headersSupportFragment.view?.requestFocus()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setOnClickListener { navigateToSettingsFragment() }
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.views.observe(viewLifecycleOwner) { homeItems ->
|
||||||
|
rowsAdapter.clear()
|
||||||
|
homeItems.map { section -> rowsAdapter.add(section.toListRow()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun HomeItem.toListRow(): ListRow {
|
||||||
|
return ListRow(
|
||||||
|
toHeader(),
|
||||||
|
toItems()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun HomeItem.toHeader(): HeaderItem {
|
||||||
|
return when (this) {
|
||||||
|
is HomeItem.Section -> HeaderItem(homeSection.name)
|
||||||
|
is HomeItem.ViewItem -> HeaderItem(
|
||||||
|
String.format(
|
||||||
|
resources.getString(R.string.latest_library),
|
||||||
|
view.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToMediaDetailFragment(item: BaseItemDto) {
|
||||||
|
findNavController().navigate(
|
||||||
|
HomeFragmentDirections.actionHomeFragmentToMediaDetailFragment(
|
||||||
|
item.id,
|
||||||
|
item.seriesName ?: item.name,
|
||||||
|
item.type ?: "Unknown"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToSettingsFragment() {
|
||||||
|
findNavController().navigate(
|
||||||
|
HomeFragmentDirections.actionNavigationHomeToSettings()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
package dev.jdtech.jellyfin.tv.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import dev.jdtech.jellyfin.R
|
||||||
|
import dev.jdtech.jellyfin.adapters.PersonListAdapter
|
||||||
|
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
||||||
|
import dev.jdtech.jellyfin.databinding.MediaDetailFragmentBinding
|
||||||
|
import dev.jdtech.jellyfin.dialogs.VideoVersionDialogFragment
|
||||||
|
import dev.jdtech.jellyfin.models.PlayerItem
|
||||||
|
import dev.jdtech.jellyfin.tv.ui.MediaDetailViewModel.State.Movie
|
||||||
|
import dev.jdtech.jellyfin.tv.ui.MediaDetailViewModel.State.TvShow
|
||||||
|
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
|
||||||
|
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
|
||||||
|
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel.PlayerItemError
|
||||||
|
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel.PlayerItems
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
internal class MediaDetailFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: MediaDetailFragmentBinding
|
||||||
|
|
||||||
|
private val viewModel: MediaInfoViewModel by viewModels()
|
||||||
|
private val detailViewModel: MediaDetailViewModel by viewModels()
|
||||||
|
private val playerViewModel: PlayerViewModel by viewModels()
|
||||||
|
|
||||||
|
private val args: MediaDetailFragmentArgs by navArgs()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
viewModel.loadData(args.itemId, args.itemType)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
binding = MediaDetailFragmentBinding.inflate(inflater)
|
||||||
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
binding.viewModel = viewModel
|
||||||
|
binding.item = detailViewModel.transformData(viewModel.item, resources) {
|
||||||
|
bindActions(it)
|
||||||
|
bindState(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val seasonsAdapter = ViewItemListAdapter(
|
||||||
|
fixedWidth = true,
|
||||||
|
onClickListener = ViewItemListAdapter.OnClickListener {})
|
||||||
|
|
||||||
|
viewModel.seasons.observe(viewLifecycleOwner) {
|
||||||
|
seasonsAdapter.submitList(it)
|
||||||
|
binding.seasonTitle.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.seasonsRow.gridView.adapter = seasonsAdapter
|
||||||
|
binding.seasonsRow.gridView.verticalSpacing = 25
|
||||||
|
|
||||||
|
val castAdapter = PersonListAdapter { person ->
|
||||||
|
Toast.makeText(requireContext(), "Not yet implemented", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.actors.observe(viewLifecycleOwner) { cast ->
|
||||||
|
castAdapter.submitList(cast)
|
||||||
|
binding.castTitle.isVisible = cast.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.castRow.gridView.adapter = castAdapter
|
||||||
|
binding.castRow.gridView.verticalSpacing = 25
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindState(state: MediaDetailViewModel.State) {
|
||||||
|
playerViewModel.onPlaybackRequested(lifecycleScope) { state ->
|
||||||
|
when (state) {
|
||||||
|
is PlayerItemError -> bindPlayerItemsError(state)
|
||||||
|
is PlayerItems -> bindPlayerItems(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (state.media) {
|
||||||
|
is Movie -> binding.title.text = state.media.title
|
||||||
|
is TvShow -> with(binding.subtitle) {
|
||||||
|
binding.title.text = state.media.episode
|
||||||
|
text = state.media.show
|
||||||
|
isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindPlayerItems(items: PlayerItems) {
|
||||||
|
navigateToPlayerActivity(items.items.toTypedArray())
|
||||||
|
binding.playButton.setImageDrawable(
|
||||||
|
ContextCompat.getDrawable(
|
||||||
|
requireActivity(),
|
||||||
|
R.drawable.ic_play
|
||||||
|
)
|
||||||
|
)
|
||||||
|
binding.progressCircular.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindPlayerItemsError(error: PlayerItemError) {
|
||||||
|
Timber.e(error.message)
|
||||||
|
|
||||||
|
binding.errorLayout.errorPanel.isVisible = true
|
||||||
|
binding.playButton.setImageDrawable(
|
||||||
|
ContextCompat.getDrawable(
|
||||||
|
requireActivity(),
|
||||||
|
R.drawable.ic_play
|
||||||
|
)
|
||||||
|
)
|
||||||
|
binding.progressCircular.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindActions(state: MediaDetailViewModel.State) {
|
||||||
|
binding.playButton.setOnClickListener {
|
||||||
|
binding.progressCircular.isVisible = true
|
||||||
|
viewModel.item.value?.let { item ->
|
||||||
|
playerViewModel.loadPlayerItems(item) {
|
||||||
|
VideoVersionDialogFragment(item, playerViewModel).show(
|
||||||
|
parentFragmentManager,
|
||||||
|
"videoversiondialog"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.trailerUrl != null) {
|
||||||
|
with(binding.trailerButton) {
|
||||||
|
isVisible = true
|
||||||
|
setOnClickListener { playTrailer(state.trailerUrl) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.trailerButton.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.isPlayed) {
|
||||||
|
with(binding.checkButton) {
|
||||||
|
setImageDrawable(resources.getDrawable(R.drawable.ic_check_filled))
|
||||||
|
setOnClickListener { viewModel.markAsUnplayed(args.itemId) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
with(binding.checkButton) {
|
||||||
|
setImageDrawable(resources.getDrawable(R.drawable.ic_check))
|
||||||
|
setOnClickListener { viewModel.markAsPlayed(args.itemId) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.isFavorite) {
|
||||||
|
with(binding.favoriteButton) {
|
||||||
|
setImageDrawable(resources.getDrawable(R.drawable.ic_heart_filled))
|
||||||
|
setOnClickListener { viewModel.unmarkAsFavorite(args.itemId) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
with(binding.favoriteButton) {
|
||||||
|
setImageDrawable(resources.getDrawable(R.drawable.ic_heart))
|
||||||
|
setOnClickListener { viewModel.markAsFavorite(args.itemId) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.backButton.setOnClickListener { activity?.onBackPressed() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playTrailer(url: String) {
|
||||||
|
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToPlayerActivity(
|
||||||
|
playerItems: Array<PlayerItem>,
|
||||||
|
) {
|
||||||
|
findNavController().navigate(
|
||||||
|
MediaDetailFragmentDirections.actionMediaDetailFragmentToPlayerActivity(
|
||||||
|
playerItems
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package dev.jdtech.jellyfin.tv.ui
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Transformations
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dev.jdtech.jellyfin.R
|
||||||
|
import dev.jdtech.jellyfin.models.ContentType.MOVIE
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
internal class MediaDetailViewModel @Inject internal constructor() : ViewModel() {
|
||||||
|
|
||||||
|
fun transformData(
|
||||||
|
data: LiveData<BaseItemDto>,
|
||||||
|
resources: Resources,
|
||||||
|
transformed: (State) -> Unit
|
||||||
|
): LiveData<State> {
|
||||||
|
return Transformations.map(data) { baseItemDto ->
|
||||||
|
State(
|
||||||
|
dto = baseItemDto,
|
||||||
|
description = baseItemDto.overview.orEmpty(),
|
||||||
|
year = baseItemDto.productionYear.toString(),
|
||||||
|
officialRating = baseItemDto.officialRating.orEmpty(),
|
||||||
|
communityRating = baseItemDto.communityRating.toString(),
|
||||||
|
runtimeMinutes = String.format(
|
||||||
|
resources.getString(R.string.runtime_minutes),
|
||||||
|
baseItemDto.runTimeTicks?.div(600_000_000)
|
||||||
|
),
|
||||||
|
genres = baseItemDto.genres?.joinToString(" / ").orEmpty(),
|
||||||
|
trailerUrl = baseItemDto.remoteTrailers?.firstOrNull()?.url,
|
||||||
|
isPlayed = baseItemDto.userData?.played == true,
|
||||||
|
isFavorite = baseItemDto.userData?.isFavorite == true,
|
||||||
|
media = if (baseItemDto.type == MOVIE.type) {
|
||||||
|
State.Movie(
|
||||||
|
title = baseItemDto.name.orEmpty()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
State.TvShow(
|
||||||
|
episode = baseItemDto.episodeTitle ?: baseItemDto.name.orEmpty(),
|
||||||
|
show = baseItemDto.seriesName.orEmpty()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
).also(transformed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class State(
|
||||||
|
val dto: BaseItemDto,
|
||||||
|
val description: String,
|
||||||
|
val year: String,
|
||||||
|
val officialRating: String,
|
||||||
|
val communityRating: String,
|
||||||
|
val runtimeMinutes: String,
|
||||||
|
val genres: String,
|
||||||
|
val trailerUrl: String?,
|
||||||
|
val isPlayed: Boolean,
|
||||||
|
val isFavorite: Boolean,
|
||||||
|
val media: Media
|
||||||
|
) {
|
||||||
|
|
||||||
|
sealed class Media
|
||||||
|
|
||||||
|
data class Movie(val title: String): Media()
|
||||||
|
data class TvShow(val episode: String, val show: String): Media()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
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
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
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.HomeEpisodeItemBinding
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
|
||||||
|
if (item is BaseItemDto) {
|
||||||
|
DataBindingUtil.getBinding<BaseItemBinding>(viewHolder.view)?.apply {
|
||||||
|
this.item = item
|
||||||
|
this.itemName.text = if (item.type == "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 =
|
||||||
|
this.itemLayout.resources.getDimension(R.dimen.overview_media_width).toInt()
|
||||||
|
(this.itemLayout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = 0
|
||||||
|
viewHolder.view.setOnClickListener { onClick(item) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnbindViewHolder(viewHolder: ViewHolder) = Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
|
||||||
|
if (item is BaseItemDto) {
|
||||||
|
DataBindingUtil.getBinding<HomeEpisodeItemBinding>(viewHolder.view)?.apply {
|
||||||
|
episode = item
|
||||||
|
item.userData?.playedPercentage?.toInt()?.let {
|
||||||
|
progressBar.layoutParams.width = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
(it.times(2.24)).toFloat(), progressBar.context.resources.displayMetrics).toInt()
|
||||||
|
progressBar.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type == "Movie") {
|
||||||
|
primaryName.text = item.name
|
||||||
|
secondaryName.visibility = View.GONE
|
||||||
|
} else if (item.type == "Episode") {
|
||||||
|
primaryName.text = item.seriesName
|
||||||
|
}
|
||||||
|
|
||||||
|
viewHolder.view.setOnClickListener { onClick(item) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnbindViewHolder(viewHolder: ViewHolder) = Unit
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package dev.jdtech.jellyfin.tv.ui
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import dev.jdtech.jellyfin.R
|
||||||
|
import dev.jdtech.jellyfin.mpv.MPVPlayer
|
||||||
|
import dev.jdtech.jellyfin.mpv.TrackType
|
||||||
|
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
|
||||||
|
|
||||||
|
class TrackSelectorAdapter(
|
||||||
|
private val items: List<Track>,
|
||||||
|
private val viewModel: PlayerActivityViewModel,
|
||||||
|
private val trackType: String,
|
||||||
|
private val dismissWindow: () -> Unit
|
||||||
|
) : RecyclerView.Adapter<TrackSelectorAdapter.TrackSelectorViewHolder>() {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackSelectorViewHolder {
|
||||||
|
return TrackSelectorViewHolder(
|
||||||
|
LayoutInflater.from(parent.context).inflate(R.layout.track_item, parent, false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: TrackSelectorViewHolder, position: Int) {
|
||||||
|
holder.bind(items[position], viewModel, trackType, dismissWindow)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
|
class TrackSelectorViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
|
fun bind(
|
||||||
|
item: Track,
|
||||||
|
viewModel: PlayerActivityViewModel,
|
||||||
|
trackType: String,
|
||||||
|
dismissWindow: () -> Unit
|
||||||
|
) {
|
||||||
|
view.findViewById<Button>(R.id.track_name).apply {
|
||||||
|
text = String.format(
|
||||||
|
view.resources.getString(R.string.track_selection),
|
||||||
|
item.language,
|
||||||
|
item.title,
|
||||||
|
item.codec
|
||||||
|
)
|
||||||
|
setOnClickListener {
|
||||||
|
when (trackType) {
|
||||||
|
TrackType.AUDIO -> viewModel.switchToTrack(
|
||||||
|
TrackType.AUDIO,
|
||||||
|
item.playerTrack
|
||||||
|
)
|
||||||
|
TrackType.SUBTITLE -> viewModel.switchToTrack(
|
||||||
|
TrackType.SUBTITLE,
|
||||||
|
item.playerTrack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
dismissWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Track(
|
||||||
|
val title: String,
|
||||||
|
val language: String,
|
||||||
|
val codec: String,
|
||||||
|
val selected: Boolean,
|
||||||
|
val playerTrack: MPVPlayer.Companion.Track
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package dev.jdtech.jellyfin.tv.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import dev.jdtech.jellyfin.R
|
||||||
|
import dev.jdtech.jellyfin.databinding.TvAddServerFragmentBinding
|
||||||
|
import dev.jdtech.jellyfin.viewmodels.AddServerViewModel
|
||||||
|
|
||||||
|
@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.lifecycleOwner = viewLifecycleOwner
|
||||||
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
|
binding.buttonConnect.setOnClickListener {
|
||||||
|
val serverAddress = binding.serverAddress.text.toString()
|
||||||
|
if (serverAddress.isNotBlank()) {
|
||||||
|
viewModel.checkServer(serverAddress)
|
||||||
|
binding.progressCircular.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.serverAddress.error = resources.getString(R.string.add_server_empty_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.navigateToLogin.observe(viewLifecycleOwner, {
|
||||||
|
if (it) {
|
||||||
|
navigateToLoginFragment()
|
||||||
|
}
|
||||||
|
binding.progressCircular.visibility = View.GONE
|
||||||
|
})
|
||||||
|
|
||||||
|
viewModel.error.observe(viewLifecycleOwner, {
|
||||||
|
binding.serverAddress.error = it
|
||||||
|
})
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToLoginFragment() {
|
||||||
|
findNavController().navigate(TvAddServerFragmentDirections.actionAddServerFragmentToLoginFragment())
|
||||||
|
viewModel.onNavigateToLoginDone()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package dev.jdtech.jellyfin.tv.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import dev.jdtech.jellyfin.databinding.TvLoginFragmentBinding
|
||||||
|
import dev.jdtech.jellyfin.viewmodels.LoginViewModel
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class TvLoginFragment : Fragment() {
|
||||||
|
|
||||||
|
private val viewModel: LoginViewModel by viewModels()
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
val binding = TvLoginFragmentBinding.inflate(inflater)
|
||||||
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
|
binding.buttonLogin.setOnClickListener {
|
||||||
|
val username = binding.username.text.toString()
|
||||||
|
val password = binding.password.text.toString()
|
||||||
|
|
||||||
|
binding.progressCircular.visibility = View.VISIBLE
|
||||||
|
viewModel.login(username, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.error.observe(viewLifecycleOwner, {
|
||||||
|
binding.progressCircular.visibility = View.GONE
|
||||||
|
binding.username.error = it
|
||||||
|
})
|
||||||
|
|
||||||
|
viewModel.navigateToMain.observe(viewLifecycleOwner, {
|
||||||
|
if (it) {
|
||||||
|
navigateToMainActivity()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToMainActivity() {
|
||||||
|
findNavController().navigate(TvLoginFragmentDirections.actionLoginFragmentToNavigationHome())
|
||||||
|
viewModel.doneNavigatingToMain()
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import android.widget.Toast
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import dev.jdtech.jellyfin.MainNavigationDirections
|
import dev.jdtech.jellyfin.AppNavigationDirections
|
||||||
import dev.jdtech.jellyfin.models.ContentType
|
import dev.jdtech.jellyfin.models.ContentType
|
||||||
import dev.jdtech.jellyfin.models.View
|
import dev.jdtech.jellyfin.models.View
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
@ -28,7 +28,7 @@ fun BaseItemDto.contentType() = when (type) {
|
||||||
fun Fragment.checkIfLoginRequired(error: String) {
|
fun Fragment.checkIfLoginRequired(error: String) {
|
||||||
if (error.contains("401")) {
|
if (error.contains("401")) {
|
||||||
Timber.d("Login required!")
|
Timber.d("Login required!")
|
||||||
findNavController().navigate(MainNavigationDirections.actionGlobalLoginFragment())
|
findNavController().navigate(AppNavigationDirections.actionGlobalLoginFragment())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,9 @@ 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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.discovery.RecommendedServerInfo
|
import org.jellyfin.sdk.discovery.RecommendedServerInfo
|
||||||
|
|
|
@ -53,8 +53,6 @@ constructor(
|
||||||
_finishedLoading.value = false
|
_finishedLoading.value = false
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
|
||||||
jellyfinRepository.postCapabilities()
|
jellyfinRepository.postCapabilities()
|
||||||
|
|
||||||
val items = mutableListOf<HomeItem>()
|
val items = mutableListOf<HomeItem>()
|
||||||
|
@ -77,8 +75,6 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_views.value = items
|
|
||||||
|
|
||||||
val views: MutableList<View> = mutableListOf()
|
val views: MutableList<View> = mutableListOf()
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
|
|
|
@ -86,7 +86,7 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
||||||
_dateString.value = getDateString(_item.value!!)
|
_dateString.value = getDateString(_item.value!!)
|
||||||
_played.value = _item.value?.userData?.played
|
_played.value = _item.value?.userData?.played
|
||||||
_favorite.value = _item.value?.userData?.isFavorite
|
_favorite.value = _item.value?.userData?.isFavorite
|
||||||
if (itemType == "Series") {
|
if (itemType == "Series" || itemType == "Episode") {
|
||||||
_nextUp.value = getNextUp(itemId)
|
_nextUp.value = getNextUp(itemId)
|
||||||
_seasons.value = jellyfinRepository.getSeasons(itemId)
|
_seasons.value = jellyfinRepository.getSeasons(itemId)
|
||||||
}
|
}
|
||||||
|
|
60
app/src/main/res/drawable/ic_banner_foreground.xml
Normal file
60
app/src/main/res/drawable/ic_banner_foreground.xml
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="320dp"
|
||||||
|
android:height="180dp"
|
||||||
|
android:viewportWidth="320"
|
||||||
|
android:viewportHeight="180">
|
||||||
|
<group android:scaleX="0.6666667"
|
||||||
|
android:scaleY="0.6666667"
|
||||||
|
android:translateX="53.333332"
|
||||||
|
android:translateY="30">
|
||||||
|
<group android:scaleX="1.6666666"
|
||||||
|
android:scaleY="1.6666666"
|
||||||
|
android:translateX="-16.4">
|
||||||
|
<group android:scaleX="0.056557618"
|
||||||
|
android:scaleY="0.056557618"
|
||||||
|
android:translateX="25.0425"
|
||||||
|
android:translateY="25.0425">
|
||||||
|
<path
|
||||||
|
android:pathData="m586.76,542.3 l25.39,-43.97c3.53,-6.1 -5.62,-11.39 -9.15,-5.29l-25.71,44.52c-19.66,-8.97 -41.74,-13.97 -65.29,-13.97 -23.56,0 -45.63,5 -65.29,13.97l-25.7,-44.52c-3.52,-6.1 -12.67,-0.82 -9.15,5.28l25.39,43.98c-43.59,23.71 -73.41,67.84 -77.77,119.98L664.53,662.28C660.16,610.14 630.35,566.01 586.76,542.3m-74.71,-405.8c-99.41,0 -419.26,580 -370.53,677.95 48.74,97.96 692.8,96.83 741.05,0 48.25,-96.83 -271.12,-677.95 -370.53,-677.95zM754.92,729.57c-31.63,63.42 -453.64,64.23 -485.59,0C237.38,665.34 447.01,285.29 512.04,285.29c65.04,0 274.51,380.69 242.88,444.28z"
|
||||||
|
android:strokeWidth="0.23938">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startY="479.77658"
|
||||||
|
android:startX="363.41766"
|
||||||
|
android:endY="702.5666"
|
||||||
|
android:endX="749.3077"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0" android:color="#FF3DDC84"/>
|
||||||
|
<item android:offset="1" android:color="#FF00A4DC"/>
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group android:scaleX="0.29483145"
|
||||||
|
android:scaleY="0.29483145"
|
||||||
|
android:translateX="128"
|
||||||
|
android:translateY="63.46516">
|
||||||
|
<group android:translateY="144.00006">
|
||||||
|
<path android:pathData="M17.28125,0.5Q15.984375,0.5,15.1875,-0.359375Q14.40625,-1.21875,14.40625,-2.375L14.40625,-97.125Q14.40625,-98.265625,15.265625,-99.125Q16.125,-100,17.28125,-100L71.5625,-100Q72.71875,-100,73.578125,-99.125Q74.453125,-98.265625,74.453125,-97.125Q74.453125,-95.828125,73.578125,-95.03125Q72.71875,-94.25,71.5625,-94.25L19.734375,-94.25L20.15625,-94.828125L20.15625,-53.59375L19.578125,-54.734375L65.09375,-54.734375Q66.234375,-54.734375,67.09375,-53.875Q67.96875,-53.015625,67.96875,-51.875Q67.96875,-50.578125,67.09375,-49.78125Q66.234375,-49,65.09375,-49L19.296875,-49L20.15625,-50.140625L20.15625,-2.375Q20.15625,-1.21875,19.359375,-0.359375Q18.578125,0.5,17.28125,0.5Z"
|
||||||
|
android:fillColor="#FFFFFF"/>
|
||||||
|
<path android:pathData="M99.71875,-2.875Q99.71875,-1.71875,98.84375,-0.859375Q97.984375,0,96.828125,0Q95.53125,0,94.734375,-0.859375Q93.953125,-1.71875,93.953125,-2.875L93.953125,-71.125Q93.953125,-72.28125,94.8125,-73.140625Q95.6875,-74,96.828125,-74Q98.125,-74,98.921875,-73.140625Q99.71875,-72.28125,99.71875,-71.125L99.71875,-2.875ZM96.828125,-83.65625Q94.53125,-83.65625,93.15625,-84.9375Q91.796875,-86.234375,91.796875,-88.25L91.796875,-89.390625Q91.796875,-91.40625,93.234375,-92.703125Q94.671875,-94,96.96875,-94Q98.984375,-94,100.359375,-92.703125Q101.734375,-91.40625,101.734375,-89.390625L101.734375,-88.25Q101.734375,-86.234375,100.359375,-84.9375Q98.984375,-83.65625,96.828125,-83.65625Z"
|
||||||
|
android:fillColor="#FFFFFF"/>
|
||||||
|
<path android:pathData="M156.64062,-74Q165.71875,-74,171.40625,-70.40625Q177.09375,-66.8125,179.75,-60.640625Q182.42188,-54.484375,182.42188,-46.859375L182.42188,-2.921875Q182.42188,-1.765625,181.54688,-0.90625Q180.6875,-0.046875,179.53125,-0.046875Q178.23438,-0.046875,177.4375,-0.90625Q176.65625,-1.765625,176.65625,-2.921875L176.65625,-46.28125Q176.65625,-52.46875,174.5625,-57.484375Q172.48438,-62.515625,167.9375,-65.53125Q163.40625,-68.546875,156.0625,-68.546875Q149.73438,-68.546875,143.67188,-65.53125Q137.625,-62.515625,133.67188,-57.484375Q129.71875,-52.46875,129.71875,-46.28125L129.71875,-2.921875Q129.71875,-1.765625,128.84375,-0.90625Q127.984375,-0.046875,126.828125,-0.046875Q125.53125,-0.046875,124.734375,-0.90625Q123.953125,-1.765625,123.953125,-2.921875L123.953125,-68.828125Q123.953125,-69.984375,124.8125,-70.84375Q125.6875,-71.703125,126.828125,-71.703125Q128.125,-71.703125,128.92188,-70.84375Q129.71875,-69.984375,129.71875,-68.828125L129.71875,-54.765625L126.109375,-47.875Q126.109375,-53.0625,128.84375,-57.796875Q131.57812,-62.53125,136.04688,-66.1875Q140.51562,-69.84375,145.90625,-71.921875Q151.3125,-74,156.64062,-74Z"
|
||||||
|
android:fillColor="#FFFFFF"/>
|
||||||
|
<path android:pathData="M264.84375,-106Q266.14062,-106,266.9375,-105.140625Q267.73438,-104.28125,267.73438,-103.15625L267.73438,-2.84375Q267.73438,-1.6875,266.85938,-0.828125Q266,0.03125,264.84375,0.03125Q263.54688,0.03125,262.75,-0.828125Q261.96875,-1.6875,261.96875,-2.84375L261.96875,-23.265625L264.26562,-26.421875Q264.26562,-21.53125,262.03125,-16.5625Q259.8125,-11.609375,255.70312,-7.515625Q251.59375,-3.421875,246.125,-0.96875Q240.65625,1.46875,234.3125,1.46875Q224.8125,1.46875,217.25,-3.484375Q209.70312,-8.453125,205.29688,-17Q200.90625,-25.5625,200.90625,-36.34375Q200.90625,-47.109375,205.29688,-55.671875Q209.70312,-64.234375,217.25,-69.109375Q224.8125,-74,234.3125,-74Q240.21875,-74,245.625,-71.765625Q251.03125,-69.546875,255.20312,-65.515625Q259.375,-61.5,261.8125,-56.09375Q264.26562,-50.703125,264.26562,-44.390625L261.96875,-47.984375L261.96875,-103.15625Q261.96875,-104.296875,262.75,-105.140625Q263.54688,-106,264.84375,-106ZM234.60938,-4Q242.8125,-4,249.07812,-8.15625Q255.34375,-12.328125,258.9375,-19.65625Q262.54688,-27,262.54688,-36.34375Q262.54688,-45.6875,258.9375,-52.9375Q255.34375,-60.203125,249,-64.375Q242.67188,-68.546875,234.60938,-68.546875Q226.6875,-68.546875,220.34375,-64.375Q214.01562,-60.203125,210.34375,-52.9375Q206.67188,-45.6875,206.67188,-36.34375Q206.67188,-27.140625,210.34375,-19.796875Q214.01562,-12.46875,220.34375,-8.234375Q226.6875,-4,234.60938,-4Z"
|
||||||
|
android:fillColor="#FFFFFF"/>
|
||||||
|
<path android:pathData="M294.82812,0.03125Q293.53125,0.03125,292.73438,-0.828125Q291.95312,-1.6875,291.95312,-2.84375L291.95312,-68.828125Q291.95312,-69.984375,292.8125,-70.84375Q293.6875,-71.703125,294.82812,-71.703125Q296.125,-71.703125,296.92188,-70.84375Q297.71875,-69.984375,297.71875,-68.828125L297.71875,-44.546875L295.26562,-40.796875Q295.26562,-46.390625,297.20312,-52.140625Q299.15625,-57.90625,302.89062,-62.859375Q306.64062,-67.828125,312.03125,-70.90625Q317.4375,-74,324.5,-74Q326.51562,-74,328.8125,-73.421875Q331.125,-72.859375,331.125,-70.703125Q331.125,-69.40625,330.40625,-68.546875Q329.6875,-67.6875,328.53125,-67.6875Q327.65625,-67.6875,326.4375,-68.328125Q325.21875,-68.96875,322.90625,-68.96875Q318.29688,-68.96875,313.82812,-66.234375Q309.375,-63.515625,305.70312,-58.984375Q302.03125,-54.46875,299.875,-49.140625Q297.71875,-43.8125,297.71875,-38.78125L297.71875,-2.84375Q297.71875,-1.6875,296.84375,-0.828125Q295.98438,0.03125,294.82812,0.03125Z"
|
||||||
|
android:fillColor="#FFFFFF"/>
|
||||||
|
<path android:pathData="M410.90625,-36.203125Q410.90625,-25.5625,406.29688,-17Q401.6875,-8.453125,393.6875,-3.484375Q385.70312,1.46875,375.48438,1.46875Q365.40625,1.46875,357.32812,-3.484375Q349.26562,-8.453125,344.57812,-17Q339.90625,-25.5625,339.90625,-36.203125Q339.90625,-46.96875,344.57812,-55.53125Q349.26562,-64.09375,357.32812,-69.046875Q365.40625,-74,375.48438,-74Q385.70312,-74,393.6875,-69.046875Q401.6875,-64.09375,406.29688,-55.53125Q410.90625,-46.96875,410.90625,-36.203125ZM405.14062,-36.203125Q405.14062,-45.53125,401.32812,-52.796875Q397.51562,-60.0625,390.8125,-64.296875Q384.125,-68.546875,375.48438,-68.546875Q366.98438,-68.546875,360.21875,-64.296875Q353.45312,-60.0625,349.5625,-52.796875Q345.67188,-45.53125,345.67188,-36.203125Q345.67188,-27,349.5625,-19.734375Q353.45312,-12.46875,360.21875,-8.234375Q366.98438,-4,375.48438,-4Q384.125,-4,390.8125,-8.234375Q397.51562,-12.46875,401.32812,-19.734375Q405.14062,-27,405.14062,-36.203125Z"
|
||||||
|
android:fillColor="#FFFFFF"/>
|
||||||
|
<path android:pathData="M435.71875,-2.875Q435.71875,-1.71875,434.84375,-0.859375Q433.98438,0,432.82812,0Q431.53125,0,430.73438,-0.859375Q429.95312,-1.71875,429.95312,-2.875L429.95312,-71.125Q429.95312,-72.28125,430.8125,-73.140625Q431.6875,-74,432.82812,-74Q434.125,-74,434.92188,-73.140625Q435.71875,-72.28125,435.71875,-71.125L435.71875,-2.875ZM432.82812,-83.65625Q430.53125,-83.65625,429.15625,-84.9375Q427.79688,-86.234375,427.79688,-88.25L427.79688,-89.390625Q427.79688,-91.40625,429.23438,-92.703125Q430.67188,-94,432.96875,-94Q434.98438,-94,436.35938,-92.703125Q437.73438,-91.40625,437.73438,-89.390625L437.73438,-88.25Q437.73438,-86.234375,436.35938,-84.9375Q434.98438,-83.65625,432.82812,-83.65625Z"
|
||||||
|
android:fillColor="#FFFFFF"/>
|
||||||
|
<path android:pathData="M518.84375,-106Q520.1406,-106,520.9375,-105.140625Q521.7344,-104.28125,521.7344,-103.15625L521.7344,-2.84375Q521.7344,-1.6875,520.8594,-0.828125Q520,0.03125,518.84375,0.03125Q517.5469,0.03125,516.75,-0.828125Q515.96875,-1.6875,515.96875,-2.84375L515.96875,-23.265625L518.2656,-26.421875Q518.2656,-21.53125,516.03125,-16.5625Q513.8125,-11.609375,509.70312,-7.515625Q505.59375,-3.421875,500.125,-0.96875Q494.65625,1.46875,488.3125,1.46875Q478.8125,1.46875,471.25,-3.484375Q463.70312,-8.453125,459.29688,-17Q454.90625,-25.5625,454.90625,-36.34375Q454.90625,-47.109375,459.29688,-55.671875Q463.70312,-64.234375,471.25,-69.109375Q478.8125,-74,488.3125,-74Q494.21875,-74,499.625,-71.765625Q505.03125,-69.546875,509.20312,-65.515625Q513.375,-61.5,515.8125,-56.09375Q518.2656,-50.703125,518.2656,-44.390625L515.96875,-47.984375L515.96875,-103.15625Q515.96875,-104.296875,516.75,-105.140625Q517.5469,-106,518.84375,-106ZM488.60938,-4Q496.8125,-4,503.07812,-8.15625Q509.34375,-12.328125,512.9375,-19.65625Q516.5469,-27,516.5469,-36.34375Q516.5469,-45.6875,512.9375,-52.9375Q509.34375,-60.203125,503,-64.375Q496.67188,-68.546875,488.60938,-68.546875Q480.6875,-68.546875,474.34375,-64.375Q468.01562,-60.203125,464.34375,-52.9375Q460.67188,-45.6875,460.67188,-36.34375Q460.67188,-27.140625,464.34375,-19.796875Q468.01562,-12.46875,474.34375,-8.234375Q480.6875,-4,488.60938,-4Z"
|
||||||
|
android:fillColor="#FFFFFF"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
tools:ignore="MissingDefaultResource"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/settings"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/clock"
|
||||||
|
android:src="@drawable/ic_settings"
|
||||||
|
android:contentDescription="@string/title_settings"
|
||||||
|
android:background="@drawable/transparent_circle_background"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextClock
|
||||||
|
android:id="@+id/clock"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
tools:text="12:00"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
25
app/src/main/res/layout-television/icon_header_item.xml
Normal file
25
app/src/main/res/layout-television/icon_header_item.xml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:focusable="true"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
tools:ignore="MissingDefaultResource"
|
||||||
|
>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/header_icon"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/header_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
297
app/src/main/res/layout-television/media_detail_fragment.xml
Normal file
297
app/src/main/res/layout-television/media_detail_fragment.xml
Normal file
|
@ -0,0 +1,297 @@
|
||||||
|
<?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"
|
||||||
|
tools:ignore="MissingDefaultResource"
|
||||||
|
>
|
||||||
|
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<import type="android.view.View" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="item"
|
||||||
|
type="androidx.lifecycle.LiveData<dev.jdtech.jellyfin.tv.ui.MediaDetailViewModel.State>"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="viewModel"
|
||||||
|
type="dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel"
|
||||||
|
/>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
>
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/error_layout"
|
||||||
|
layout="@layout/error_panel"
|
||||||
|
tools:visibility="gone"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
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:padding="16dp"
|
||||||
|
android:src="@drawable/ic_arrow_left"
|
||||||
|
android:focusable="true"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/back_button"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Alita: Battle Angel"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextClock
|
||||||
|
android:id="@+id/clock"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
tools:text="12:00"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/poster"
|
||||||
|
android:layout_width="320dp"
|
||||||
|
android:layout_height="180dp"
|
||||||
|
android:layout_marginStart="@dimen/horizontal_margin"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:baseItemImage="@{item.dto}"
|
||||||
|
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"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/poster"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/title"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:text="Subtitle"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/genres"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/horizontal_margin"
|
||||||
|
android:text="@{item.genres}"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/poster"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/subtitle"
|
||||||
|
tools:text="Action, Science Fiction, Adventure"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/year"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/horizontal_margin"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/genres"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/poster"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="@{item.year}"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
tools:text="2019" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/playtime"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/horizontal_margin"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/genres"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/year"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="@{item.runtimeMinutes}"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
tools:text="122 min" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/official_rating"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/horizontal_margin"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/genres"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/playtime"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="@{item.officialRating}"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
tools:text="PG-13" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/community_rating"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/horizontal_margin"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/genres"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/official_rating"
|
||||||
|
android:drawablePadding="4dp"
|
||||||
|
android:text="@{item.communityRating}"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
android:drawableStart="@drawable/ic_star"
|
||||||
|
android:drawableTint="@color/yellow"
|
||||||
|
tools:text="7.3" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/description"
|
||||||
|
android:layout_width="400dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:text="@{item.description}"
|
||||||
|
android:maxLines="5"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:layout_marginStart="@dimen/horizontal_margin"
|
||||||
|
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"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/poster"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/description"
|
||||||
|
android:elevation="8dp"
|
||||||
|
android:indeterminateTint="@color/white"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:visibility="invisible" />
|
||||||
|
|
||||||
|
<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:paddingHorizontal="24dp"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:src="@drawable/ic_play"
|
||||||
|
android:focusable="true"
|
||||||
|
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:padding="12dp"
|
||||||
|
android:src="@drawable/ic_film"
|
||||||
|
android:focusable="true"
|
||||||
|
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:padding="12dp"
|
||||||
|
android:focusable="true"
|
||||||
|
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:padding="12dp"
|
||||||
|
android:focusable="true"
|
||||||
|
android:src="@drawable/ic_heart"
|
||||||
|
android:contentDescription="@string/favorite_button_description"
|
||||||
|
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_marginTop="32dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/play_button"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
android:text="@string/seasons"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_marginStart="@dimen/horizontal_margin"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<androidx.leanback.widget.ListRowView
|
||||||
|
android:id="@+id/seasons_row"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/season_title"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/cast_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/seasons_row"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
android:text="@string/cast_amp_crew"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_marginStart="@dimen/horizontal_margin"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<androidx.leanback.widget.ListRowView
|
||||||
|
android:id="@+id/cast_row"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/cast_title"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</layout>
|
26
app/src/main/res/layout-television/track_item.xml
Normal file
26
app/src/main/res/layout-television/track_item.xml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="@dimen/track_selection_item_height"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
tools:ignore="MissingDefaultResource"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
|
>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/track_name"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
android:textSize="@dimen/track_selection_item_text_size"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
tools:text="subtitle track"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
25
app/src/main/res/layout-television/track_selector.xml
Normal file
25
app/src/main/res/layout-television/track_selector.xml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.leanback.widget.BrowseFrameLayout
|
||||||
|
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/selector_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:descendantFocusability="afterDescendants"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:ignore="MissingDefaultResource"
|
||||||
|
>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/track_selector"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:descendantFocusability="afterDescendants"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:ignore="MissingDefaultResource"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</androidx.leanback.widget.BrowseFrameLayout>
|
|
@ -0,0 +1,97 @@
|
||||||
|
<?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"
|
||||||
|
tools:ignore="MissingDefaultResource">
|
||||||
|
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="viewModel"
|
||||||
|
type="dev.jdtech.jellyfin.viewmodels.AddServerViewModel"
|
||||||
|
/>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".fragments.AddServerFragment"
|
||||||
|
>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_banner"
|
||||||
|
android:layout_width="268dp"
|
||||||
|
android:layout_height="75dp"
|
||||||
|
android:layout_marginTop="64dp"
|
||||||
|
android:contentDescription="@string/jellyfin_banner"
|
||||||
|
android:src="@drawable/ic_banner"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linearLayout"
|
||||||
|
android:layout_width="@dimen/setup_container_width"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/image_banner"
|
||||||
|
app:layout_constraintVertical_bias="0.36"
|
||||||
|
>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_add_server"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="32dp"
|
||||||
|
android:text="@string/add_server"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
||||||
|
android:textColor="?android:textColorPrimary"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/server_address"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:hint="@string/edit_text_server_address_hint"
|
||||||
|
app:errorEnabled="true"
|
||||||
|
app:startIconDrawable="@drawable/ic_server"
|
||||||
|
android:inputType="textUri"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_connect"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:drawableStart="@drawable/ic_launcher_foreground"
|
||||||
|
android:text="@string/button_connect"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_circular"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:elevation="8dp"
|
||||||
|
android:indeterminateTint="@color/white"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:visibility="invisible"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout></layout>
|
110
app/src/main/res/layout-television/tv_login_fragment.xml
Normal file
110
app/src/main/res/layout-television/tv_login_fragment.xml
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
<?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"
|
||||||
|
tools:ignore="MissingDefaultResource">
|
||||||
|
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="viewModel"
|
||||||
|
type="dev.jdtech.jellyfin.viewmodels.LoginViewModel"
|
||||||
|
/>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".fragments.LoginFragment"
|
||||||
|
>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_banner"
|
||||||
|
android:layout_width="268dp"
|
||||||
|
android:layout_height="75dp"
|
||||||
|
android:layout_marginTop="64dp"
|
||||||
|
android:contentDescription="@string/jellyfin_banner"
|
||||||
|
android:src="@drawable/ic_banner"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/linearLayout"
|
||||||
|
android:layout_width="@dimen/setup_container_width"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/image_banner"
|
||||||
|
app:layout_constraintVertical_bias="0.36"
|
||||||
|
>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_login"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="32dp"
|
||||||
|
android:text="@string/login"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
||||||
|
android:textColor="?android:textColorPrimary"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/username"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:autofillHints="username"
|
||||||
|
android:hint="@string/edit_text_username_hint"
|
||||||
|
android:inputType="text"
|
||||||
|
app:startIconDrawable="@drawable/ic_user"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/password"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:autofillHints="password"
|
||||||
|
android:hint="@string/edit_text_password_hint"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
app:passwordToggleEnabled="true"
|
||||||
|
app:startIconDrawable="@drawable/ic_lock"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_login"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:text="@string/button_login"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_circular"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:elevation="8dp"
|
||||||
|
android:indeterminateTint="@color/white"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:visibility="invisible"
|
||||||
|
/>
|
||||||
|
</RelativeLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</layout>
|
136
app/src/main/res/layout-television/tv_player_controls.xml
Normal file
136
app/src/main/res/layout-television/tv_player_controls.xml
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
<?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:id="@+id/tv_player_controls"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/player_background"
|
||||||
|
tools:ignore="MissingDefaultResource"
|
||||||
|
>
|
||||||
|
|
||||||
|
<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:padding="@dimen/tv_controls_padding"
|
||||||
|
android:src="@drawable/ic_arrow_left"
|
||||||
|
android:focusable="true"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/video_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:padding="@dimen/tv_controls_padding"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Title"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/back_button"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="The Dawn of Despair"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_audio_track"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/transparent_circle_background"
|
||||||
|
android:contentDescription="@string/select_audio_track"
|
||||||
|
android:padding="@dimen/tv_controls_padding"
|
||||||
|
android:src="@drawable/ic_speaker"
|
||||||
|
android:focusable="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/exo_progress"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_subtitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/tv_controls_margin_start"
|
||||||
|
android:background="@drawable/transparent_circle_background"
|
||||||
|
android:contentDescription="@string/select_subtile_track"
|
||||||
|
android:padding="@dimen/tv_controls_padding"
|
||||||
|
android:src="@drawable/ic_closed_caption"
|
||||||
|
android:focusable="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/exo_progress"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/btn_audio_track"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<com.google.android.exoplayer2.ui.DefaultTimeBar
|
||||||
|
android:id="@+id/exo_progress"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/exo_play_pause"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
android:focusable="true"
|
||||||
|
app:played_color="?colorPrimary"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/exo_rew"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/transparent_circle_background"
|
||||||
|
android:contentDescription="@string/player_controls_rewind"
|
||||||
|
android:padding="@dimen/tv_controls_padding"
|
||||||
|
android:src="@drawable/ic_rewind"
|
||||||
|
android:focusable="true"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/exo_play_pause"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/tv_controls_margin_start"
|
||||||
|
android:background="@drawable/transparent_circle_background"
|
||||||
|
android:contentDescription="@string/play_button_description"
|
||||||
|
android:padding="@dimen/tv_controls_padding"
|
||||||
|
android:src="@drawable/ic_pause"
|
||||||
|
android:focusable="true"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/exo_rew"
|
||||||
|
android:focusedByDefault="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/exo_ffwd"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/tv_controls_margin_start"
|
||||||
|
android:background="@drawable/transparent_circle_background"
|
||||||
|
android:contentDescription="@string/player_controls_fast_forward"
|
||||||
|
android:padding="@dimen/tv_controls_padding"
|
||||||
|
android:src="@drawable/ic_fast_forward"
|
||||||
|
android:focusable="true"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/exo_play_pause"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/exo_next"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/tv_controls_margin_start"
|
||||||
|
android:background="@drawable/transparent_circle_background"
|
||||||
|
android:contentDescription="@string/player_controls_skip"
|
||||||
|
android:padding="@dimen/tv_controls_padding"
|
||||||
|
android:src="@drawable/ic_skip_forward"
|
||||||
|
android:focusable="true"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/exo_ffwd"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -27,7 +27,7 @@
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/main_toolbar_layout"
|
app:layout_constraintTop_toBottomOf="@id/main_toolbar_layout"
|
||||||
app:navGraph="@navigation/main_navigation" />
|
app:navGraph="@navigation/app_navigation" />
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:id="@+id/main_toolbar_layout"
|
android:id="@+id/main_toolbar_layout"
|
22
app/src/main/res/layout/activity_main_tv.xml
Normal file
22
app/src/main/res/layout/activity_main_tv.xml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
|
||||||
|
<androidx.fragment.app.FragmentContainerView
|
||||||
|
android:id="@+id/tv_nav_host"
|
||||||
|
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:defaultNavHost="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="parent"
|
||||||
|
app:navGraph="@navigation/tv_navigation"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
20
app/src/main/res/layout/activity_player_tv.xml
Normal file
20
app/src/main/res/layout/activity_player_tv.xml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
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=".PlayerActivity"
|
||||||
|
>
|
||||||
|
|
||||||
|
<com.google.android.exoplayer2.ui.PlayerView
|
||||||
|
android:id="@+id/player_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black"
|
||||||
|
app:controller_layout_id="@layout/tv_player_controls"
|
||||||
|
app:show_buffering="always"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
|
@ -57,7 +57,7 @@
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@{item.userData.unplayedItemCount.toString()}"
|
android:text="@{item.userData.unplayedItemCount.toString()}"
|
||||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||||
android:textColor="?attr/colorOnPrimary"
|
android:textColor="@color/white"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="9" />
|
tools:text="9" />
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/transparent_circle_background"
|
android:background="@drawable/transparent_circle_background"
|
||||||
|
android:contentDescription="@string/player_controls_exit"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
android:src="@drawable/ic_arrow_left" />
|
android:src="@drawable/ic_arrow_left" />
|
||||||
|
|
||||||
|
@ -75,6 +76,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="end"
|
android:layout_gravity="end"
|
||||||
android:background="@drawable/transparent_circle_background"
|
android:background="@drawable/transparent_circle_background"
|
||||||
|
android:contentDescription="@string/select_audio_track"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
android:src="@drawable/ic_speaker"
|
android:src="@drawable/ic_speaker"
|
||||||
app:tint="@color/white" />
|
app:tint="@color/white" />
|
||||||
|
@ -89,6 +91,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="end"
|
android:layout_gravity="end"
|
||||||
android:background="@drawable/transparent_circle_background"
|
android:background="@drawable/transparent_circle_background"
|
||||||
|
android:contentDescription="@string/select_subtile_track"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
android:src="@drawable/ic_closed_caption"
|
android:src="@drawable/ic_closed_caption"
|
||||||
app:tint="@color/white" />
|
app:tint="@color/white" />
|
||||||
|
|
|
@ -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="0dp"
|
android:layout_width="240dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
app:baseItemImage="@{episode}"
|
app:baseItemImage="@{episode}"
|
||||||
|
|
5
app/src/main/res/mipmap-anydpi-v26/ic_banner.xml
Normal file
5
app/src/main/res/mipmap-anydpi-v26/ic_banner.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_banner_background"/>
|
||||||
|
<foreground android:drawable="@drawable/ic_banner_foreground"/>
|
||||||
|
</adaptive-icon>
|
BIN
app/src/main/res/mipmap-xhdpi/ic_banner.png
Normal file
BIN
app/src/main/res/mipmap-xhdpi/ic_banner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6 KiB |
|
@ -2,7 +2,7 @@
|
||||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
<navigation 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"
|
||||||
android:id="@+id/main_navigation"
|
android:id="@+id/app_navigation"
|
||||||
app:startDestination="@+id/homeFragment">
|
app:startDestination="@+id/homeFragment">
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
|
@ -178,15 +178,6 @@
|
||||||
app:argType="boolean"
|
app:argType="boolean"
|
||||||
android:defaultValue="false" />
|
android:defaultValue="false" />
|
||||||
</dialog>
|
</dialog>
|
||||||
<activity
|
|
||||||
android:id="@+id/playerActivity"
|
|
||||||
android:name="dev.jdtech.jellyfin.PlayerActivity"
|
|
||||||
android:label="activity_player"
|
|
||||||
tools:layout="@layout/activity_player">
|
|
||||||
<argument
|
|
||||||
android:name="items"
|
|
||||||
app:argType="dev.jdtech.jellyfin.models.PlayerItem[]" />
|
|
||||||
</activity>
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/favoriteFragment"
|
android:id="@+id/favoriteFragment"
|
||||||
android:name="dev.jdtech.jellyfin.fragments.FavoriteFragment"
|
android:name="dev.jdtech.jellyfin.fragments.FavoriteFragment"
|
||||||
|
@ -276,6 +267,16 @@
|
||||||
app:destination="@id/mediaInfoFragment" />
|
app:destination="@id/mediaInfoFragment" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:id="@+id/playerActivity"
|
||||||
|
android:name="dev.jdtech.jellyfin.PlayerActivity"
|
||||||
|
android:label="activity_player"
|
||||||
|
tools:layout="@layout/activity_player">
|
||||||
|
<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"
|
93
app/src/main/res/navigation/tv_navigation.xml
Normal file
93
app/src/main/res/navigation/tv_navigation.xml
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
<?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/settingsFragment"
|
||||||
|
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="string" />
|
||||||
|
|
||||||
|
<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/fragment_add_server">
|
||||||
|
<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/settingsFragment"
|
||||||
|
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>
|
14
app/src/main/res/values-television/dimens.xml
Normal file
14
app/src/main/res/values-television/dimens.xml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:ignore="MissingDefaultResource">
|
||||||
|
|
||||||
|
<dimen name="horizontal_margin">48dp</dimen>
|
||||||
|
<dimen name="vertical_margin">48dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="tv_controls_padding">8dp</dimen>
|
||||||
|
<dimen name="tv_controls_margin_start">32dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="track_selection_item_height">48dp</dimen>
|
||||||
|
<dimen name="track_selection_item_text_size">16sp</dimen>
|
||||||
|
|
||||||
|
</resources>
|
9
app/src/main/res/values-television/styles.xml
Normal file
9
app/src/main/res/values-television/styles.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="roundedCardView">
|
||||||
|
<item name="cornerFamily">rounded</item>
|
||||||
|
<item name="cornerSize">40dp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
29
app/src/main/res/values-television/themes.xml
Normal file
29
app/src/main/res/values-television/themes.xml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<resources>
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.Jellyfin.Tv" parent="Theme.AppCompat.Leanback">
|
||||||
|
<item name="colorPrimary">@color/blue_600</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/blue_800</item>
|
||||||
|
<item name="colorSecondary">@color/green_500</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/green_800</item>
|
||||||
|
<!-- Status bar color. -->
|
||||||
|
<item name="android:statusBarColor">@color/neutral_900</item>
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="android:windowBackground">@color/neutral_900</item>
|
||||||
|
<item name="android:colorBackgroundFloating">@color/neutral_900</item>
|
||||||
|
|
||||||
|
<item name="defaultBrandColor">@color/neutral_900</item>
|
||||||
|
<item name="browseTitleViewLayout">@layout/browse_support_fragment_title_view</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<string-array name="themes">
|
||||||
|
<item>Follow system</item>
|
||||||
|
<item>Light</item>
|
||||||
|
<item>Dark</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="themes_value">
|
||||||
|
<item>system</item>
|
||||||
|
<item>light</item>
|
||||||
|
<item>dark</item>
|
||||||
|
</string-array>
|
||||||
|
</resources>
|
4
app/src/main/res/values/ic_banner_background.xml
Normal file
4
app/src/main/res/values/ic_banner_background.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_banner_background">#000000</color>
|
||||||
|
</resources>
|
|
@ -90,4 +90,13 @@
|
||||||
<item>Ascending</item>
|
<item>Ascending</item>
|
||||||
<item>Descending</item>
|
<item>Descending</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string name="runtime_minutes">%1$d mins</string>
|
||||||
|
<string name="select_video_version_title">Select version</string>
|
||||||
|
<string name="player_controls_exit">Exit player</string>
|
||||||
|
<string name="player_controls_rewind">Rewind</string>
|
||||||
|
<string name="player_controls_fast_forward">Fast forward</string>
|
||||||
|
<string name="player_controls_pause">Pause</string>
|
||||||
|
<string name="player_controls_skip">Skip</string>
|
||||||
|
<string name="track_selection">[%1$s] %2$s (%3$s)</string>
|
||||||
|
<string name="add_server_empty_error">Empty server address</string>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in a new issue