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 {
|
||||
implementation("androidx.leanback:leanback:1.2.0-alpha01")
|
||||
|
||||
implementation("androidx.core:core-ktx:1.6.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
||||
implementation("androidx.appcompat:appcompat:1.3.1")
|
||||
|
|
|
@ -5,28 +5,56 @@
|
|||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".BaseApplication"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupOnly="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Findroid"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<activity
|
||||
android:name=".PlayerActivity"
|
||||
android:screenOrientation="userLandscape" />
|
||||
|
||||
<activity
|
||||
android:name=".tv.TvPlayerActivity"
|
||||
android:screenOrientation="userLandscape" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.FindroidSplashScreen"
|
||||
android:windowSoftInputMode="adjustPan">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivityTv"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.Jellyfin.Tv"
|
||||
android:windowSoftInputMode="adjustPan">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
</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.view.View
|
||||
import androidx.activity.viewModels
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.setupActionBarWithNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
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.viewmodels.MainViewModel
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var binding: ActivityMainAppBinding
|
||||
private val viewModel: MainViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
binding = ActivityMainAppBinding.inflate(layoutInflater)
|
||||
|
||||
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
|
||||
|
||||
import android.os.Build
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.navigation.navArgs
|
||||
import com.google.android.exoplayer2.C
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector
|
||||
import com.google.android.exoplayer2.ui.TrackSelectionDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.jdtech.jellyfin.databinding.ActivityPlayerBinding
|
||||
|
@ -27,16 +24,17 @@ import timber.log.Timber
|
|||
import kotlin.math.max
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PlayerActivity : AppCompatActivity() {
|
||||
class PlayerActivity : BasePlayerActivity() {
|
||||
|
||||
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 audioController by lazy { AudioController(this) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Timber.d("Creating player activity")
|
||||
|
||||
binding = ActivityPlayerBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
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)
|
||||
setupVolumeControl()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
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
|
||||
}
|
||||
}
|
||||
configureInsets(playerControls)
|
||||
|
||||
binding.playerView.findViewById<View>(R.id.back_button).setOnClickListener {
|
||||
onBackPressed()
|
||||
|
@ -173,18 +159,6 @@ class PlayerActivity : AppCompatActivity() {
|
|||
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() {
|
||||
val height = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
windowManager.currentWindowMetrics.bounds.height()
|
||||
|
@ -198,32 +172,5 @@ class PlayerActivity : AppCompatActivity() {
|
|||
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 androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import java.lang.IllegalStateException
|
||||
import dev.jdtech.jellyfin.R
|
||||
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
class VideoVersionDialogFragment(
|
||||
private val item: BaseItemDto,
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
package dev.jdtech.jellyfin.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
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.FragmentAddServerBinding
|
||||
import dev.jdtech.jellyfin.viewmodels.AddServerViewModel
|
||||
|
||||
|
@ -32,7 +33,7 @@ class AddServerFragment : Fragment() {
|
|||
viewModel.checkServer(serverAddress)
|
||||
binding.progressCircular.visibility = View.VISIBLE
|
||||
} 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
|
||||
|
||||
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.viewModels
|
||||
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.PlayerViewModel
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import timber.log.Timber
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
@ -263,7 +263,7 @@ class MediaInfoFragment : Fragment() {
|
|||
) {
|
||||
findNavController().navigate(
|
||||
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.fragment.app.Fragment
|
||||
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.View
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
|
@ -28,7 +28,7 @@ fun BaseItemDto.contentType() = when (type) {
|
|||
fun Fragment.checkIfLoginRequired(error: String) {
|
||||
if (error.contains("401")) {
|
||||
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.ServerDatabaseDao
|
||||
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.withContext
|
||||
import org.jellyfin.sdk.discovery.RecommendedServerInfo
|
||||
|
|
|
@ -53,8 +53,6 @@ constructor(
|
|||
_finishedLoading.value = false
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
|
||||
|
||||
jellyfinRepository.postCapabilities()
|
||||
|
||||
val items = mutableListOf<HomeItem>()
|
||||
|
@ -77,8 +75,6 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
_views.value = items
|
||||
|
||||
val views: MutableList<View> = mutableListOf()
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
|
|
|
@ -86,7 +86,7 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
|||
_dateString.value = getDateString(_item.value!!)
|
||||
_played.value = _item.value?.userData?.played
|
||||
_favorite.value = _item.value?.userData?.isFavorite
|
||||
if (itemType == "Series") {
|
||||
if (itemType == "Series" || itemType == "Episode") {
|
||||
_nextUp.value = getNextUp(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_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/main_toolbar_layout"
|
||||
app:navGraph="@navigation/main_navigation" />
|
||||
app:navGraph="@navigation/app_navigation" />
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
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:text="@{item.userData.unplayedItemCount.toString()}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:textColor="?attr/colorOnPrimary"
|
||||
android:textColor="@color/white"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="9" />
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
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" />
|
||||
|
||||
|
@ -75,6 +76,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:background="@drawable/transparent_circle_background"
|
||||
android:contentDescription="@string/select_audio_track"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_speaker"
|
||||
app:tint="@color/white" />
|
||||
|
@ -89,6 +91,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:background="@drawable/transparent_circle_background"
|
||||
android:contentDescription="@string/select_subtile_track"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_closed_caption"
|
||||
app:tint="@color/white" />
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/episode_image"
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="240dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="centerCrop"
|
||||
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"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main_navigation"
|
||||
android:id="@+id/app_navigation"
|
||||
app:startDestination="@+id/homeFragment">
|
||||
|
||||
<fragment
|
||||
|
@ -178,15 +178,6 @@
|
|||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
</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
|
||||
android:id="@+id/favoriteFragment"
|
||||
android:name="dev.jdtech.jellyfin.fragments.FavoriteFragment"
|
||||
|
@ -276,6 +267,16 @@
|
|||
app:destination="@id/mediaInfoFragment" />
|
||||
</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" />
|
||||
<action
|
||||
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>Descending</item>
|
||||
</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>
|
Loading…
Reference in a new issue