Merge branch 'main' into Skip-credit
This commit is contained in:
commit
3c6e03db89
59 changed files with 1573 additions and 134 deletions
8
.github/workflows/build.yaml
vendored
8
.github/workflows/build.yaml
vendored
|
@ -12,14 +12,14 @@ jobs:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v2
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/gradle-build-action@v2
|
uses: gradle/actions/setup-gradle@v3
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew lintDebug ktlintCheck
|
run: ./gradlew lintDebug ktlintCheck
|
||||||
assemble:
|
assemble:
|
||||||
|
@ -29,14 +29,14 @@ jobs:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v2
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/gradle-build-action@v2
|
uses: gradle/actions/setup-gradle@v3
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew assembleDebug
|
run: ./gradlew assembleDebug
|
||||||
# Upload all build artifacts in separate steps. This can be shortened once https://github.com/actions/upload-artifact/pull/354 is merged.
|
# Upload all build artifacts in separate steps. This can be shortened once https://github.com/actions/upload-artifact/pull/354 is merged.
|
||||||
|
|
|
@ -40,6 +40,9 @@ I am developing this application in my spare time.
|
||||||
- Subtitle codecs: SRT, VTT, SSA/ASS, DVDSUB
|
- Subtitle codecs: SRT, VTT, SSA/ASS, DVDSUB
|
||||||
- Optionally force software decoding when hardware decoding has issues.
|
- Optionally force software decoding when hardware decoding has issues.
|
||||||
- Picture-in-picture mode
|
- Picture-in-picture mode
|
||||||
|
- Media chapters
|
||||||
|
- Timeline markers
|
||||||
|
- Chapter navigation gestures
|
||||||
|
|
||||||
## Planned features
|
## Planned features
|
||||||
- Android TV
|
- Android TV
|
||||||
|
|
|
@ -7,11 +7,13 @@ import android.content.Intent
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Color
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
|
import android.provider.Settings
|
||||||
import android.util.Rational
|
import android.util.Rational
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
@ -27,15 +29,14 @@ import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.ui.AspectRatioFrameLayout
|
|
||||||
import androidx.media3.ui.DefaultTimeBar
|
import androidx.media3.ui.DefaultTimeBar
|
||||||
|
import androidx.media3.ui.PlayerControlView
|
||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import androidx.navigation.navArgs
|
import androidx.navigation.navArgs
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.databinding.ActivityPlayerBinding
|
import dev.jdtech.jellyfin.databinding.ActivityPlayerBinding
|
||||||
import dev.jdtech.jellyfin.dialogs.SpeedSelectionDialogFragment
|
import dev.jdtech.jellyfin.dialogs.SpeedSelectionDialogFragment
|
||||||
import dev.jdtech.jellyfin.dialogs.TrackSelectionDialogFragment
|
import dev.jdtech.jellyfin.dialogs.TrackSelectionDialogFragment
|
||||||
import dev.jdtech.jellyfin.mpv.MPVPlayer
|
|
||||||
import dev.jdtech.jellyfin.utils.PlayerGestureHelper
|
import dev.jdtech.jellyfin.utils.PlayerGestureHelper
|
||||||
import dev.jdtech.jellyfin.utils.PreviewScrubListener
|
import dev.jdtech.jellyfin.utils.PreviewScrubListener
|
||||||
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
|
import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
|
||||||
|
@ -57,6 +58,7 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
private var playerGestureHelper: PlayerGestureHelper? = null
|
private var playerGestureHelper: PlayerGestureHelper? = null
|
||||||
override val viewModel: PlayerActivityViewModel by viewModels()
|
override val viewModel: PlayerActivityViewModel by viewModels()
|
||||||
private var previewScrubListener: PreviewScrubListener? = null
|
private var previewScrubListener: PreviewScrubListener? = null
|
||||||
|
private var wasZoom: Boolean = false
|
||||||
|
|
||||||
private val isPipSupported by lazy {
|
private val isPipSupported by lazy {
|
||||||
// Check if device has PiP feature
|
// Check if device has PiP feature
|
||||||
|
@ -113,10 +115,6 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.playerView.findViewById<View>(R.id.back_button_alt).setOnClickListener {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
val videoNameTextView = binding.playerView.findViewById<TextView>(R.id.video_name)
|
val videoNameTextView = binding.playerView.findViewById<TextView>(R.id.video_name)
|
||||||
|
|
||||||
val audioButton = binding.playerView.findViewById<ImageButton>(R.id.btn_audio_track)
|
val audioButton = binding.playerView.findViewById<ImageButton>(R.id.btn_audio_track)
|
||||||
|
@ -166,6 +164,18 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
it.currentTrickPlay = currentTrickPlay
|
it.currentTrickPlay = currentTrickPlay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chapters
|
||||||
|
if (appPreferences.showChapterMarkers && currentChapters != null) {
|
||||||
|
currentChapters?.let { chapters ->
|
||||||
|
val playerControlView = findViewById<PlayerControlView>(R.id.exo_controller)
|
||||||
|
val numOfChapters = chapters.size
|
||||||
|
playerControlView.setExtraAdGroupMarkers(
|
||||||
|
LongArray(numOfChapters) { index -> chapters[index].startPosition },
|
||||||
|
BooleanArray(numOfChapters) { false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// File Loaded
|
// File Loaded
|
||||||
if (fileLoaded) {
|
if (fileLoaded) {
|
||||||
audioButton.isEnabled = true
|
audioButton.isEnabled = true
|
||||||
|
@ -187,6 +197,11 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
viewModel.eventsChannelFlow.collect { event ->
|
viewModel.eventsChannelFlow.collect { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
is PlayerEvents.NavigateBack -> finish()
|
is PlayerEvents.NavigateBack -> finish()
|
||||||
|
is PlayerEvents.IsPlayingChanged -> {
|
||||||
|
if (appPreferences.playerPipGesture) {
|
||||||
|
setPictureInPictureParams(pipParams(event.isPlaying))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -256,9 +271,12 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
pictureInPicture()
|
pictureInPicture()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set marker color
|
||||||
|
val timeBar = binding.playerView.findViewById<DefaultTimeBar>(R.id.exo_progress)
|
||||||
|
timeBar.setAdMarkerColor(Color.WHITE)
|
||||||
|
|
||||||
if (appPreferences.playerTrickPlay) {
|
if (appPreferences.playerTrickPlay) {
|
||||||
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
|
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
|
||||||
val timeBar = binding.playerView.findViewById<DefaultTimeBar>(R.id.exo_progress)
|
|
||||||
previewScrubListener = PreviewScrubListener(
|
previewScrubListener = PreviewScrubListener(
|
||||||
imagePreview,
|
imagePreview,
|
||||||
timeBar,
|
timeBar,
|
||||||
|
@ -281,12 +299,16 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUserLeaveHint() {
|
override fun onUserLeaveHint() {
|
||||||
if (appPreferences.playerPipGesture && viewModel.player.isPlaying && !isControlsLocked) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S &&
|
||||||
|
appPreferences.playerPipGesture &&
|
||||||
|
viewModel.player.isPlaying &&
|
||||||
|
!isControlsLocked
|
||||||
|
) {
|
||||||
pictureInPicture()
|
pictureInPicture()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pipParams(): PictureInPictureParams {
|
private fun pipParams(enableAutoEnter: Boolean = viewModel.player.isPlaying): PictureInPictureParams {
|
||||||
val displayAspectRatio = Rational(binding.playerView.width, binding.playerView.height)
|
val displayAspectRatio = Rational(binding.playerView.width, binding.playerView.height)
|
||||||
|
|
||||||
val aspectRatio = binding.playerView.player?.videoSize?.let {
|
val aspectRatio = binding.playerView.player?.videoSize?.let {
|
||||||
|
@ -314,24 +336,21 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return PictureInPictureParams.Builder()
|
val builder = PictureInPictureParams.Builder()
|
||||||
.setAspectRatio(aspectRatio)
|
.setAspectRatio(aspectRatio)
|
||||||
.setSourceRectHint(sourceRectHint)
|
.setSourceRectHint(sourceRectHint)
|
||||||
.build()
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
builder.setAutoEnterEnabled(enableAutoEnter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pictureInPicture() {
|
private fun pictureInPicture() {
|
||||||
if (!isPipSupported) {
|
if (!isPipSupported) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
binding.playerView.useController = false
|
|
||||||
binding.playerView.findViewById<Button>(R.id.btn_skip_intro).isVisible = false
|
|
||||||
|
|
||||||
if (binding.playerView.player is MPVPlayer) {
|
|
||||||
(binding.playerView.player as MPVPlayer).updateZoomMode(false)
|
|
||||||
} else {
|
|
||||||
binding.playerView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
enterPictureInPictureMode(pipParams())
|
enterPictureInPictureMode(pipParams())
|
||||||
|
@ -343,8 +362,35 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
newConfig: Configuration,
|
newConfig: Configuration,
|
||||||
) {
|
) {
|
||||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||||
if (!isInPictureInPictureMode) {
|
when (isInPictureInPictureMode) {
|
||||||
binding.playerView.useController = true
|
true -> {
|
||||||
|
binding.playerView.useController = false
|
||||||
|
binding.playerView.findViewById<Button>(R.id.btn_skip_intro).isVisible = false
|
||||||
|
|
||||||
|
wasZoom = playerGestureHelper?.isZoomEnabled ?: false
|
||||||
|
playerGestureHelper?.updateZoomMode(false)
|
||||||
|
|
||||||
|
// Brightness mode Auto
|
||||||
|
window.attributes = window.attributes.apply {
|
||||||
|
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false -> {
|
||||||
|
binding.playerView.useController = true
|
||||||
|
playerGestureHelper?.updateZoomMode(wasZoom)
|
||||||
|
|
||||||
|
// Override auto brightness
|
||||||
|
window.attributes = window.attributes.apply {
|
||||||
|
screenBrightness = if (appPreferences.playerBrightnessRemember) {
|
||||||
|
appPreferences.playerBrightness
|
||||||
|
} else {
|
||||||
|
Settings.System.getInt(
|
||||||
|
contentResolver,
|
||||||
|
Settings.System.SCREEN_BRIGHTNESS,
|
||||||
|
).toFloat() / 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import dev.jdtech.jellyfin.databinding.FragmentLibraryBinding
|
||||||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||||
import dev.jdtech.jellyfin.dialogs.SortDialogFragment
|
import dev.jdtech.jellyfin.dialogs.SortDialogFragment
|
||||||
import dev.jdtech.jellyfin.models.FindroidBoxSet
|
import dev.jdtech.jellyfin.models.FindroidBoxSet
|
||||||
|
import dev.jdtech.jellyfin.models.FindroidFolder
|
||||||
import dev.jdtech.jellyfin.models.FindroidItem
|
import dev.jdtech.jellyfin.models.FindroidItem
|
||||||
import dev.jdtech.jellyfin.models.FindroidMovie
|
import dev.jdtech.jellyfin.models.FindroidMovie
|
||||||
import dev.jdtech.jellyfin.models.FindroidShow
|
import dev.jdtech.jellyfin.models.FindroidShow
|
||||||
|
@ -222,6 +223,15 @@ class LibraryFragment : Fragment() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is FindroidFolder -> {
|
||||||
|
findNavController().navigate(
|
||||||
|
LibraryFragmentDirections.actionLibraryFragmentSelf(
|
||||||
|
item.id,
|
||||||
|
item.name,
|
||||||
|
args.libraryType,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import dev.jdtech.jellyfin.AppPreferences
|
||||||
import dev.jdtech.jellyfin.Constants
|
import dev.jdtech.jellyfin.Constants
|
||||||
import dev.jdtech.jellyfin.PlayerActivity
|
import dev.jdtech.jellyfin.PlayerActivity
|
||||||
import dev.jdtech.jellyfin.isControlsLocked
|
import dev.jdtech.jellyfin.isControlsLocked
|
||||||
|
import dev.jdtech.jellyfin.models.PlayerChapter
|
||||||
import dev.jdtech.jellyfin.mpv.MPVPlayer
|
import dev.jdtech.jellyfin.mpv.MPVPlayer
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
@ -37,7 +38,7 @@ class PlayerGestureHelper(
|
||||||
* Tracks whether video content should fill the screen, cutting off unwanted content on the sides.
|
* Tracks whether video content should fill the screen, cutting off unwanted content on the sides.
|
||||||
* Useful on wide-screen phones to remove black bars from some movies.
|
* Useful on wide-screen phones to remove black bars from some movies.
|
||||||
*/
|
*/
|
||||||
private var isZoomEnabled = false
|
var isZoomEnabled = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks a value during a swipe gesture (between multiple onScroll calls).
|
* Tracks a value during a swipe gesture (between multiple onScroll calls).
|
||||||
|
@ -55,9 +56,14 @@ class PlayerGestureHelper(
|
||||||
|
|
||||||
private var lastScaleEvent: Long = 0
|
private var lastScaleEvent: Long = 0
|
||||||
|
|
||||||
|
private var playbackSpeedIncrease: Float = 2f
|
||||||
|
private var lastPlaybackSpeed: Float = 0f
|
||||||
|
|
||||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||||
private val screenHeight = Resources.getSystem().displayMetrics.heightPixels
|
private val screenHeight = Resources.getSystem().displayMetrics.heightPixels
|
||||||
|
|
||||||
|
private var currentNumberOfPointers: Int = 0
|
||||||
|
|
||||||
private val tapGestureDetector = GestureDetector(
|
private val tapGestureDetector = GestureDetector(
|
||||||
playerView.context,
|
playerView.context,
|
||||||
object : GestureDetector.SimpleOnGestureListener() {
|
object : GestureDetector.SimpleOnGestureListener() {
|
||||||
|
@ -69,6 +75,22 @@ class PlayerGestureHelper(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onLongPress(e: MotionEvent) {
|
||||||
|
// Disables long press gesture if view is locked
|
||||||
|
if (isControlsLocked) return
|
||||||
|
|
||||||
|
// Stop long press gesture when more than 1 pointer
|
||||||
|
if (currentNumberOfPointers > 1) return
|
||||||
|
|
||||||
|
// This is a temporary solution for chapter skipping.
|
||||||
|
// TODO: Remove this after implementing #636
|
||||||
|
if (appPreferences.playerGesturesChapterSkip) {
|
||||||
|
handleChapterSkip(e)
|
||||||
|
} else {
|
||||||
|
enableSpeedIncrease()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||||
// Disables double tap gestures if view is locked
|
// Disables double tap gestures if view is locked
|
||||||
if (isControlsLocked) return false
|
if (isControlsLocked) return false
|
||||||
|
@ -100,6 +122,55 @@ class PlayerGestureHelper(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
private fun enableSpeedIncrease() {
|
||||||
|
playerView.player?.let {
|
||||||
|
if (it.isPlaying) {
|
||||||
|
lastPlaybackSpeed = it.playbackParameters.speed
|
||||||
|
it.setPlaybackSpeed(playbackSpeedIncrease)
|
||||||
|
activity.binding.gestureSpeedText.text = playbackSpeedIncrease.toString() + "x"
|
||||||
|
activity.binding.gestureSpeedLayout.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleChapterSkip(e: MotionEvent) {
|
||||||
|
if (isControlsLocked) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val viewWidth = playerView.measuredWidth
|
||||||
|
val areaWidth = viewWidth / 5 // Divide the view into 5 parts: 2:1:2
|
||||||
|
|
||||||
|
// Define the areas and their boundaries
|
||||||
|
val leftmostAreaStart = 0
|
||||||
|
val middleAreaStart = areaWidth * 2
|
||||||
|
val rightmostAreaStart = middleAreaStart + areaWidth
|
||||||
|
|
||||||
|
when (e.x.toInt()) {
|
||||||
|
in leftmostAreaStart until middleAreaStart -> {
|
||||||
|
activity.viewModel.seekToPreviousChapter()?.let { chapter ->
|
||||||
|
displayChapter(chapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
in rightmostAreaStart until viewWidth -> {
|
||||||
|
if (activity.viewModel.isLastChapter() == true) {
|
||||||
|
playerView.player?.seekToNextMediaItem()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
activity.viewModel.seekToNextChapter()?.let { chapter ->
|
||||||
|
displayChapter(chapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayChapter(chapter: PlayerChapter) {
|
||||||
|
activity.binding.progressScrubberLayout.visibility = View.VISIBLE
|
||||||
|
activity.binding.progressScrubberText.text = chapter.name ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
private fun fastForward() {
|
private fun fastForward() {
|
||||||
val currentPosition = playerView.player?.currentPosition ?: 0
|
val currentPosition = playerView.player?.currentPosition ?: 0
|
||||||
val fastForwardPosition = currentPosition + appPreferences.playerSeekForwardIncrement
|
val fastForwardPosition = currentPosition + appPreferences.playerSeekForwardIncrement
|
||||||
|
@ -315,8 +386,8 @@ class PlayerGestureHelper(
|
||||||
lastScaleEvent = SystemClock.elapsedRealtime()
|
lastScaleEvent = SystemClock.elapsedRealtime()
|
||||||
val scaleFactor = detector.scaleFactor
|
val scaleFactor = detector.scaleFactor
|
||||||
if (abs(scaleFactor - Constants.ZOOM_SCALE_BASE) > Constants.ZOOM_SCALE_THRESHOLD) {
|
if (abs(scaleFactor - Constants.ZOOM_SCALE_BASE) > Constants.ZOOM_SCALE_THRESHOLD) {
|
||||||
isZoomEnabled = scaleFactor > 1
|
val enableZoom = scaleFactor > 1
|
||||||
updateZoomMode(isZoomEnabled)
|
updateZoomMode(enableZoom)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -325,16 +396,17 @@ class PlayerGestureHelper(
|
||||||
},
|
},
|
||||||
).apply { isQuickScaleEnabled = false }
|
).apply { isQuickScaleEnabled = false }
|
||||||
|
|
||||||
private fun updateZoomMode(enabled: Boolean) {
|
fun updateZoomMode(enabled: Boolean) {
|
||||||
if (playerView.player is MPVPlayer) {
|
if (playerView.player is MPVPlayer) {
|
||||||
(playerView.player as MPVPlayer).updateZoomMode(enabled)
|
(playerView.player as MPVPlayer).updateZoomMode(enabled)
|
||||||
} else {
|
} else {
|
||||||
playerView.resizeMode = if (enabled) AspectRatioFrameLayout.RESIZE_MODE_ZOOM else AspectRatioFrameLayout.RESIZE_MODE_FIT
|
playerView.resizeMode = if (enabled) AspectRatioFrameLayout.RESIZE_MODE_ZOOM else AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||||
}
|
}
|
||||||
|
isZoomEnabled = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun releaseAction(event: MotionEvent) {
|
private fun releaseAction(event: MotionEvent) {
|
||||||
if (event.action == MotionEvent.ACTION_UP) {
|
if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
|
||||||
activity.binding.gestureVolumeLayout.apply {
|
activity.binding.gestureVolumeLayout.apply {
|
||||||
if (visibility == View.VISIBLE) {
|
if (visibility == View.VISIBLE) {
|
||||||
removeCallbacks(hideGestureVolumeIndicatorOverlayAction)
|
removeCallbacks(hideGestureVolumeIndicatorOverlayAction)
|
||||||
|
@ -361,6 +433,12 @@ class PlayerGestureHelper(
|
||||||
swipeGestureValueTrackerProgress = -1L
|
swipeGestureValueTrackerProgress = -1L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
currentNumberOfPointers = 0
|
||||||
|
}
|
||||||
|
if (lastPlaybackSpeed > 0 && (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL)) {
|
||||||
|
playerView.player?.setPlaybackSpeed(lastPlaybackSpeed)
|
||||||
|
lastPlaybackSpeed = 0f
|
||||||
|
activity.binding.gestureSpeedLayout.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -398,9 +476,12 @@ class PlayerGestureHelper(
|
||||||
activity.window.attributes.screenBrightness = appPreferences.playerBrightness
|
activity.window.attributes.screenBrightness = appPreferences.playerBrightness
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateZoomMode(appPreferences.playerStartMaximized)
|
||||||
|
|
||||||
@Suppress("ClickableViewAccessibility")
|
@Suppress("ClickableViewAccessibility")
|
||||||
playerView.setOnTouchListener { _, event ->
|
playerView.setOnTouchListener { _, event ->
|
||||||
if (playerView.useController) {
|
if (playerView.useController) {
|
||||||
|
currentNumberOfPointers = event.pointerCount
|
||||||
when (event.pointerCount) {
|
when (event.pointerCount) {
|
||||||
1 -> {
|
1 -> {
|
||||||
tapGestureDetector.onTouchEvent(event)
|
tapGestureDetector.onTouchEvent(event)
|
||||||
|
|
|
@ -113,6 +113,37 @@
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/gesture_speed_layout"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:layout_gravity="center_horizontal|top"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:background="@drawable/overlay_background"
|
||||||
|
android:clickable="false"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/gesture_speed_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textColor="@android:color/white" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/gesture_speed_image"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:src="@drawable/ic_speed_forward"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/image_ffwd_animation_ripple"
|
android:id="@+id/image_ffwd_animation_ripple"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
|
|
|
@ -8,55 +8,16 @@
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<ImageButton
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/btn_unlock"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:background="@drawable/rounded_corner"
|
||||||
|
android:contentDescription="@string/select_playback_speed"
|
||||||
|
android:padding="16dp"
|
||||||
android:layout_margin="8dp"
|
android:layout_margin="8dp"
|
||||||
android:orientation="horizontal">
|
android:src="@drawable/ic_unlock"
|
||||||
|
app:tint="@android:color/white" />
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/extra_buttons_alt"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/back_button_alt"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@drawable/rounded_corner"
|
|
||||||
android:contentDescription="@string/player_controls_exit"
|
|
||||||
android:padding="16dp"
|
|
||||||
android:src="@drawable/ic_arrow_left" />
|
|
||||||
|
|
||||||
<Space
|
|
||||||
android:layout_width="16dp"
|
|
||||||
android:layout_height="0dp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/extra_buttons_alt"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/btn_unlock"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="end"
|
|
||||||
android:background="@drawable/rounded_corner"
|
|
||||||
android:contentDescription="@string/select_playback_speed"
|
|
||||||
android:padding="16dp"
|
|
||||||
android:src="@drawable/ic_unlock"
|
|
||||||
app:tint="@android:color/white" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
|
@ -1,7 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/exo_controller">
|
||||||
|
|
||||||
<androidx.media3.ui.AspectRatioFrameLayout
|
<androidx.media3.ui.AspectRatioFrameLayout
|
||||||
android:id="@id/exo_content_frame"
|
android:id="@id/exo_content_frame"
|
||||||
|
@ -81,9 +82,10 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
<View
|
<androidx.media3.ui.PlayerControlView
|
||||||
android:id="@id/exo_controller_placeholder"
|
android:id="@id/exo_controller"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent"
|
||||||
|
app:animation_enabled="false"/>
|
||||||
|
|
||||||
</merge>
|
</merge>
|
|
@ -123,6 +123,9 @@
|
||||||
<argument
|
<argument
|
||||||
android:name="libraryType"
|
android:name="libraryType"
|
||||||
app:argType="dev.jdtech.jellyfin.models.CollectionType" />
|
app:argType="dev.jdtech.jellyfin.models.CollectionType" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_libraryFragment_self"
|
||||||
|
app:destination="@id/libraryFragment" />
|
||||||
</fragment>
|
</fragment>
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/showFragment"
|
android:id="@+id/showFragment"
|
||||||
|
|
|
@ -82,12 +82,15 @@ ktlint {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
val composeBom = platform(libs.androidx.compose.bom)
|
||||||
|
|
||||||
implementation(projects.core)
|
implementation(projects.core)
|
||||||
implementation(projects.data)
|
implementation(projects.data)
|
||||||
implementation(projects.preferences)
|
implementation(projects.preferences)
|
||||||
implementation(projects.player.core)
|
implementation(projects.player.core)
|
||||||
implementation(projects.player.video)
|
implementation(projects.player.video)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
implementation(composeBom)
|
||||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
|
|
|
@ -23,9 +23,11 @@ import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import dev.jdtech.jellyfin.destinations.LibraryScreenDestination
|
||||||
import dev.jdtech.jellyfin.destinations.MovieScreenDestination
|
import dev.jdtech.jellyfin.destinations.MovieScreenDestination
|
||||||
import dev.jdtech.jellyfin.destinations.ShowScreenDestination
|
import dev.jdtech.jellyfin.destinations.ShowScreenDestination
|
||||||
import dev.jdtech.jellyfin.models.CollectionType
|
import dev.jdtech.jellyfin.models.CollectionType
|
||||||
|
import dev.jdtech.jellyfin.models.FindroidFolder
|
||||||
import dev.jdtech.jellyfin.models.FindroidItem
|
import dev.jdtech.jellyfin.models.FindroidItem
|
||||||
import dev.jdtech.jellyfin.models.FindroidMovie
|
import dev.jdtech.jellyfin.models.FindroidMovie
|
||||||
import dev.jdtech.jellyfin.models.FindroidShow
|
import dev.jdtech.jellyfin.models.FindroidShow
|
||||||
|
@ -65,6 +67,9 @@ fun LibraryScreen(
|
||||||
is FindroidShow -> {
|
is FindroidShow -> {
|
||||||
navigator.navigate(ShowScreenDestination(item.id))
|
navigator.navigate(ShowScreenDestination(item.id))
|
||||||
}
|
}
|
||||||
|
is FindroidFolder -> {
|
||||||
|
navigator.navigate(LibraryScreenDestination(item.id, item.name, libraryType))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -55,6 +55,7 @@ val dummyEpisode = FindroidEpisode(
|
||||||
seasonId = UUID.randomUUID(),
|
seasonId = UUID.randomUUID(),
|
||||||
communityRating = 9.2f,
|
communityRating = 9.2f,
|
||||||
images = FindroidImages(),
|
images = FindroidImages(),
|
||||||
|
chapters = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
val dummyEpisodes = listOf(
|
val dummyEpisodes = listOf(
|
||||||
|
|
|
@ -55,6 +55,7 @@ val dummyMovie = FindroidMovie(
|
||||||
endDate = null,
|
endDate = null,
|
||||||
trailer = "https://www.youtube.com/watch?v=puKWa8hrvA8",
|
trailer = "https://www.youtube.com/watch?v=puKWa8hrvA8",
|
||||||
images = FindroidImages(),
|
images = FindroidImages(),
|
||||||
|
chapters = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
val dummyMovies = listOf(
|
val dummyMovies = listOf(
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import org.gradle.api.JavaVersion
|
import org.gradle.api.JavaVersion
|
||||||
|
|
||||||
object Versions {
|
object Versions {
|
||||||
const val appCode = 22
|
const val appCode = 23
|
||||||
const val appName = "0.13.1"
|
const val appName = "2024.02"
|
||||||
|
|
||||||
const val compileSdk = 34
|
const val compileSdk = 34
|
||||||
const val buildTools = "34.0.0"
|
const val buildTools = "34.0.0"
|
||||||
|
@ -11,6 +11,6 @@ object Versions {
|
||||||
|
|
||||||
val java = JavaVersion.VERSION_17
|
val java = JavaVersion.VERSION_17
|
||||||
|
|
||||||
const val composeCompiler = "1.5.8"
|
const val composeCompiler = "1.5.10"
|
||||||
const val ktlint = "0.50.0"
|
const val ktlint = "0.50.0"
|
||||||
}
|
}
|
|
@ -53,11 +53,14 @@ ktlint {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
val composeBom = platform(libs.androidx.compose.bom)
|
||||||
|
|
||||||
implementation(projects.data)
|
implementation(projects.data)
|
||||||
implementation(projects.preferences)
|
implementation(projects.preferences)
|
||||||
implementation(projects.player.core)
|
implementation(projects.player.core)
|
||||||
implementation(libs.androidx.activity)
|
implementation(libs.androidx.activity)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
|
implementation(composeBom)
|
||||||
implementation(libs.androidx.compose.ui)
|
implementation(libs.androidx.compose.ui)
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.androidx.hilt.work)
|
implementation(libs.androidx.hilt.work)
|
||||||
|
|
|
@ -93,7 +93,7 @@ class HomeViewModel @Inject internal constructor(
|
||||||
|
|
||||||
private suspend fun loadViews() = repository
|
private suspend fun loadViews() = repository
|
||||||
.getUserViews()
|
.getUserViews()
|
||||||
.filter { view -> CollectionType.supported.any { it.type == view.collectionType } }
|
.filter { view -> CollectionType.fromString(view.collectionType) in CollectionType.supported }
|
||||||
.map { view -> view to repository.getLatestMedia(view.id) }
|
.map { view -> view to repository.getLatestMedia(view.id) }
|
||||||
.filter { (_, latest) -> latest.isNotEmpty() }
|
.filter { (_, latest) -> latest.isNotEmpty() }
|
||||||
.map { (view, latest) -> view.toView().apply { items = latest } }
|
.map { (view, latest) -> view.toView().apply { items = latest } }
|
||||||
|
|
|
@ -48,15 +48,19 @@ constructor(
|
||||||
CollectionType.Movies -> listOf(BaseItemKind.MOVIE)
|
CollectionType.Movies -> listOf(BaseItemKind.MOVIE)
|
||||||
CollectionType.TvShows -> listOf(BaseItemKind.SERIES)
|
CollectionType.TvShows -> listOf(BaseItemKind.SERIES)
|
||||||
CollectionType.BoxSets -> listOf(BaseItemKind.BOX_SET)
|
CollectionType.BoxSets -> listOf(BaseItemKind.BOX_SET)
|
||||||
|
CollectionType.Mixed -> listOf(BaseItemKind.FOLDER, BaseItemKind.MOVIE, BaseItemKind.SERIES)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val recursive = itemType == null || !itemType.contains(BaseItemKind.FOLDER)
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.emit(UiState.Loading)
|
_uiState.emit(UiState.Loading)
|
||||||
try {
|
try {
|
||||||
val items = jellyfinRepository.getItemsPaging(
|
val items = jellyfinRepository.getItemsPaging(
|
||||||
parentId = parentId,
|
parentId = parentId,
|
||||||
includeTypes = itemType,
|
includeTypes = itemType,
|
||||||
recursive = true,
|
recursive = recursive,
|
||||||
sortBy = sortBy,
|
sortBy = sortBy,
|
||||||
sortOrder = sortOrder,
|
sortOrder = sortOrder,
|
||||||
).cachedIn(viewModelScope)
|
).cachedIn(viewModelScope)
|
||||||
|
|
|
@ -3,7 +3,6 @@ package dev.jdtech.jellyfin.viewmodels
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dev.jdtech.jellyfin.models.CollectionType
|
|
||||||
import dev.jdtech.jellyfin.models.FindroidCollection
|
import dev.jdtech.jellyfin.models.FindroidCollection
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
@ -35,9 +34,7 @@ constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.emit(UiState.Loading)
|
_uiState.emit(UiState.Loading)
|
||||||
try {
|
try {
|
||||||
val items = jellyfinRepository.getLibraries()
|
val collections = jellyfinRepository.getLibraries()
|
||||||
val collections =
|
|
||||||
items.filter { collection -> collection.type in CollectionType.supported }
|
|
||||||
_uiState.emit(UiState.Normal(collections))
|
_uiState.emit(UiState.Normal(collections))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
_uiState.emit(
|
_uiState.emit(
|
||||||
|
|
|
@ -97,7 +97,7 @@ constructor(
|
||||||
nameStringResource = R.string.pref_player_mpv_vo,
|
nameStringResource = R.string.pref_player_mpv_vo,
|
||||||
dependencies = listOf(Constants.PREF_PLAYER_MPV),
|
dependencies = listOf(Constants.PREF_PLAYER_MPV),
|
||||||
backendName = Constants.PREF_PLAYER_MPV_VO,
|
backendName = Constants.PREF_PLAYER_MPV_VO,
|
||||||
backendDefaultValue = "gpu",
|
backendDefaultValue = "gpu-next",
|
||||||
options = R.array.mpv_vos,
|
options = R.array.mpv_vos,
|
||||||
optionValues = R.array.mpv_vos,
|
optionValues = R.array.mpv_vos,
|
||||||
),
|
),
|
||||||
|
|
27
core/src/main/res/drawable/ic_speed_forward.xml
Normal file
27
core/src/main/res/drawable/ic_speed_forward.xml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="36dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="36"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M14,19l9,-7l-9,-7l0,14z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="@android:color/white"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M2,19l9,-7l-9,-7l0,14z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="@android:color/white"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M26,19l9,-7l-9,-7l0,14z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="@android:color/white"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
4
core/src/main/res/values-da/strings.xml
Normal file
4
core/src/main/res/values-da/strings.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="external">Ekstern</string>
|
||||||
|
</resources>
|
|
@ -185,4 +185,6 @@
|
||||||
<string name="live_tv">Élő TV</string>
|
<string name="live_tv">Élő TV</string>
|
||||||
<string name="play">Lejátszás</string>
|
<string name="play">Lejátszás</string>
|
||||||
<string name="watch_trailer">Előzetes megtekintése</string>
|
<string name="watch_trailer">Előzetes megtekintése</string>
|
||||||
|
<string name="player_start_maximized_summary">Videó megnyitása alapértelmezés szerint maximalizált módban</string>
|
||||||
|
<string name="player_start_maximized">Indítás maximalizálva</string>
|
||||||
</resources>
|
</resources>
|
|
@ -188,4 +188,10 @@
|
||||||
<string name="skip_intro_button">Salta intro</string>
|
<string name="skip_intro_button">Salta intro</string>
|
||||||
<string name="skip_credit_button">Prossimo episodio</string>
|
<string name="skip_credit_button">Prossimo episodio</string>
|
||||||
<string name="skip_credit_button_last">Chiudi player</string>
|
<string name="skip_credit_button_last">Chiudi player</string>
|
||||||
|
<string name="player_gestures_chapter_skip">Gesto per le scene</string>
|
||||||
|
<string name="pref_player_chapter_markers">Marcatori delle scene</string>
|
||||||
|
<string name="pref_player_chapter_markers_summary">Mostra i marcatori delle scene sulla timebar</string>
|
||||||
|
<string name="player_gestures_chapter_skip_summary">Premere a lungo sul lato sinistro/destro per saltare le scene (disattiva il gesto di velocità 2x)</string>
|
||||||
|
<string name="player_start_maximized">Adatta video al display</string>
|
||||||
|
<string name="player_start_maximized_summary">Avvia di default il video adattato al display</string>
|
||||||
</resources>
|
</resources>
|
|
@ -185,4 +185,10 @@
|
||||||
<string name="live_tv">TV ao vivo</string>
|
<string name="live_tv">TV ao vivo</string>
|
||||||
<string name="play">Reproduzir</string>
|
<string name="play">Reproduzir</string>
|
||||||
<string name="watch_trailer">Assista o trailer</string>
|
<string name="watch_trailer">Assista o trailer</string>
|
||||||
|
<string name="player_gestures_chapter_skip">Gesto do capítulo</string>
|
||||||
|
<string name="player_gestures_chapter_skip_summary">Pressione longamente no lado esquerdo/direito para pular capítulos (substitui o gesto de velocidade 2x)</string>
|
||||||
|
<string name="player_start_maximized">Iniciar maximizado</string>
|
||||||
|
<string name="player_start_maximized_summary">Abra o vídeo no modo maximizado por padrão</string>
|
||||||
|
<string name="pref_player_chapter_markers">Marcadores de capítulo</string>
|
||||||
|
<string name="pref_player_chapter_markers_summary">Exibir marcadores de capítulo na barra de tempo</string>
|
||||||
</resources>
|
</resources>
|
126
core/src/main/res/values-tr/strings.xml
Normal file
126
core/src/main/res/values-tr/strings.xml
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="add_server">Sunucu ekle</string>
|
||||||
|
<string name="login">Giriş yap</string>
|
||||||
|
<string name="edit_text_server_address_hint">Sunucu adresi</string>
|
||||||
|
<string name="edit_text_password_hint">Şifre</string>
|
||||||
|
<string name="button_connect">Bağlan</string>
|
||||||
|
<string name="button_login">Giriş yap</string>
|
||||||
|
<string name="remove_server">Sunucuyu kaldır</string>
|
||||||
|
<string name="remove_user">Kullanıcıyı kaldır</string>
|
||||||
|
<string name="title_download">İndirilenler</string>
|
||||||
|
<string name="view_all">Hepsini göster</string>
|
||||||
|
<string name="title_media">Medyam</string>
|
||||||
|
<string name="title_home">Ana Sayfa</string>
|
||||||
|
<string name="remove">Kaldır</string>
|
||||||
|
<string name="cancel">İptal</string>
|
||||||
|
<string name="director">Yönetmen</string>
|
||||||
|
<string name="writers">Yazarlar</string>
|
||||||
|
<string name="trailer_button_description">Fragmanı izle</string>
|
||||||
|
<string name="genres">Türler</string>
|
||||||
|
<string name="retry">Yeniden dene</string>
|
||||||
|
<string name="play_button_description">Medyayı oynat</string>
|
||||||
|
<string name="next_up">Sıradaki</string>
|
||||||
|
<string name="continue_watching">İzlemeye Devam Et</string>
|
||||||
|
<string name="settings_category_language">Dil</string>
|
||||||
|
<string name="app_language">Uygulama dili</string>
|
||||||
|
<string name="settings_preferred_audio_language">Tercih edilen ses dili</string>
|
||||||
|
<string name="settings_category_download">İndirilenler</string>
|
||||||
|
<string name="episodes_label">Bölümler</string>
|
||||||
|
<string name="download_button_description">İndir</string>
|
||||||
|
<string name="person_detail_title">Detaylar</string>
|
||||||
|
<string name="close">Kapat</string>
|
||||||
|
<string name="sort_by_options_0">Başlık</string>
|
||||||
|
<string name="add_user">Kullanıcı ekle</string>
|
||||||
|
<string name="subtitle">Altyazılar</string>
|
||||||
|
<string name="add_server_error_not_found">Sunucu bulunamadı</string>
|
||||||
|
<string name="title_settings">Ayarlar</string>
|
||||||
|
<string name="select_server">Sunucu seç</string>
|
||||||
|
<string name="theme">Tema</string>
|
||||||
|
<string name="share">Paylaş</string>
|
||||||
|
<string name="edit_text_username_hint">Kullanıcı adı</string>
|
||||||
|
<string name="seasons">Sezonlar</string>
|
||||||
|
<string name="libraries">Kütüphaneler</string>
|
||||||
|
<string name="settings_category_device">Cihaz</string>
|
||||||
|
<string name="title_favorite">Favoriler</string>
|
||||||
|
<string name="search">Ara</string>
|
||||||
|
<string name="about">Hakkında</string>
|
||||||
|
<string name="player_brightness_remember">Parlaklık seviyesini hatırla</string>
|
||||||
|
<string name="login_error_wrong_username_password">Hatalı kullanıcı adı veya şifre</string>
|
||||||
|
<string name="sort_by_options_5">Çıkış Tarihi</string>
|
||||||
|
<string name="settings_category_servers">Sunucular</string>
|
||||||
|
<string name="movies_label">Filmler</string>
|
||||||
|
<string name="subtitles">Altyazılar</string>
|
||||||
|
<string name="add">Ekle</string>
|
||||||
|
<string name="users">Kullanıcılar</string>
|
||||||
|
<string name="settings_preferred_subtitle_language">Tercih edilen altyazı dili</string>
|
||||||
|
<string name="add_server_error_version">Desteklenmeyen sunucu sürümü: %1$s. Lütfen sunucunuzu güncelleyin</string>
|
||||||
|
<string name="latest_library">En son %1$s</string>
|
||||||
|
<string name="settings_category_player">Oynatıcı</string>
|
||||||
|
<string name="settings_category_appearance">Görünüş</string>
|
||||||
|
<string name="device_name">Cihaz adı</string>
|
||||||
|
<string name="view_details">Detayları göster</string>
|
||||||
|
<string name="unknown_error">Bilinmeyen hata</string>
|
||||||
|
<string name="app_info">Uygulama bilgisi</string>
|
||||||
|
<string name="hide">Gizle</string>
|
||||||
|
<string name="sort_by_options_1">IMDB Puanı</string>
|
||||||
|
<string name="amoled_theme">AMOLED karanlık tema</string>
|
||||||
|
<string name="theme_dark">Karanlık</string>
|
||||||
|
<string name="theme_light">Aydınlık</string>
|
||||||
|
<string name="audio">Ses</string>
|
||||||
|
<string name="add_address">Adres ekle</string>
|
||||||
|
<string name="add_server_address">Sunucu adresi ekle</string>
|
||||||
|
<string name="addresses">Adresler</string>
|
||||||
|
<string name="cancel_download">İndirmeyi iptal et</string>
|
||||||
|
<string name="stop_download">İndirmeyi duraklat</string>
|
||||||
|
<string name="no_users_found">Kullanıcı bulunamadı</string>
|
||||||
|
<string name="remove_from_favorites">Favorilerden çıkar</string>
|
||||||
|
<string name="add_to_favorites">Favorilere ekle</string>
|
||||||
|
<string name="select_user">Kullanıcı seç</string>
|
||||||
|
<string name="add_server_error_empty_address">Boş sunucu adresi</string>
|
||||||
|
<string name="theme_system">Sistemi takip et</string>
|
||||||
|
<string name="app_description">Üçüncü parti yerel Jellyfin uygulaması</string>
|
||||||
|
<string name="jellyfin_banner">Jellyfin afişi</string>
|
||||||
|
<string name="add_server_error_outdated">Sunucu sürümü güncel değil: %1$s. Lütfen sunucunuzu güncelleyin</string>
|
||||||
|
<string name="add_server_error_not_jellyfin">Jellyfin sunucusu değil: %1$s</string>
|
||||||
|
<string name="add_server_error_slow">Sunucu yanıt vermekte çok yavaş: %1$s</string>
|
||||||
|
<string name="remove_server_dialog_text">%1$s sunucusunu kaldırmak istediğinizden emin misiniz</string>
|
||||||
|
<string name="remove_user_dialog_text">%1$s kullanıcısını kaldırmak istediğinizden emin misiniz</string>
|
||||||
|
<string name="remove_server_address">Sunucu adresini kaldır</string>
|
||||||
|
<string name="remove_server_address_dialog_text">%1$s sunucu adresini kaldırmak istediğinizden emin misiniz</string>
|
||||||
|
<string name="error_loading_data">Veriler yüklenirken hata oluştu</string>
|
||||||
|
<string name="cast_amp_crew">Oyuncular ve Ekip</string>
|
||||||
|
<string name="episode_name">%1$d. %2$s</string>
|
||||||
|
<string name="episode_name_extended">S%1$d:B%2$d - %3$s</string>
|
||||||
|
<string name="episode_name_extended_with_end">S%1$d:B%2$d-%3$d - %4$s</string>
|
||||||
|
<string name="episode_name_with_end">%1$d-%2$d. %3$s</string>
|
||||||
|
<string name="series_poster">Dizi posteri</string>
|
||||||
|
<string name="no_favorites">Hiç favoriniz yok</string>
|
||||||
|
<string name="no_search_results">Arama sonucu bulunamadı</string>
|
||||||
|
<string name="settings_category_cache">Önbellek</string>
|
||||||
|
<string name="settings_use_cache_title">Resimleri önbelleğe al</string>
|
||||||
|
<string name="view_details_underlined"><u>Detayları göster</u></string>
|
||||||
|
<string name="privacy_policy">Gizlilik politikası</string>
|
||||||
|
<string name="download_mobile_data">Mobil veri kullanarak indir</string>
|
||||||
|
<string name="shows_label">Diziler</string>
|
||||||
|
<string name="sort_by">Şuna göre sırala</string>
|
||||||
|
<string name="sort_order">Sıralama düzeni</string>
|
||||||
|
<string name="gestures">Hareketler</string>
|
||||||
|
<string name="player_gestures_zoom">Yakınlaştırma hareketi</string>
|
||||||
|
<string name="sort_by_options_2">Ebeveyn Derecelendirmesi</string>
|
||||||
|
<string name="sort_by_options_3">Eklenme Tarihi</string>
|
||||||
|
<string name="sort_by_options_4">Oynatma Tarihi</string>
|
||||||
|
<string name="ascending">Artan</string>
|
||||||
|
<string name="runtime_minutes">%1$d dakika</string>
|
||||||
|
<string name="select_video_version_title">Sürüm seçiniz</string>
|
||||||
|
<string name="dynamic_colors">Dinamik renkler</string>
|
||||||
|
<string name="subtitles_summary">Altyazıların görünümünü özelleştir</string>
|
||||||
|
<string name="settings_category_network">Ağ</string>
|
||||||
|
<string name="settings_cache_size">Önbellek boyutu (MB)</string>
|
||||||
|
<string name="player_gestures">Oynatıcı hareketleri</string>
|
||||||
|
<string name="player_gestures_vb">Ses ve parlaklık hareketleri</string>
|
||||||
|
<string name="descending">Azalan</string>
|
||||||
|
<string name="offline_mode_go_online">Çevrimiçi ol</string>
|
||||||
|
<string name="search_hint">Filmleri, dizileri, bölümleri arayın…</string>
|
||||||
|
<string name="track_selection">[%1$s] %2$s (%3$s)</string>
|
||||||
|
</resources>
|
|
@ -175,4 +175,16 @@
|
||||||
<string name="picture_in_picture">画中画</string>
|
<string name="picture_in_picture">画中画</string>
|
||||||
<string name="picture_in_picture_gesture_summary">在视频播放时使用主页按钮或手势进入画中画模式</string>
|
<string name="picture_in_picture_gesture_summary">在视频播放时使用主页按钮或手势进入画中画模式</string>
|
||||||
<string name="picture_in_picture_gesture">画中画返回手势</string>
|
<string name="picture_in_picture_gesture">画中画返回手势</string>
|
||||||
|
<string name="select_user">选择用户</string>
|
||||||
|
<string name="live_tv">电视直播</string>
|
||||||
|
<string name="no_servers_found">没有找到服务器</string>
|
||||||
|
<string name="no_users_found">找不到用户</string>
|
||||||
|
<string name="unmark_as_played">取消标记已播放</string>
|
||||||
|
<string name="add_to_favorites">加入收藏</string>
|
||||||
|
<string name="remove_from_favorites">从收藏中删除</string>
|
||||||
|
<string name="mark_as_played">标记为已播放</string>
|
||||||
|
<string name="player_start_maximized">最大化启动</string>
|
||||||
|
<string name="player_start_maximized_summary">默认以最大化模式打开视频</string>
|
||||||
|
<string name="play">播放</string>
|
||||||
|
<string name="watch_trailer">观看预告</string>
|
||||||
</resources>
|
</resources>
|
|
@ -103,10 +103,14 @@
|
||||||
<string name="player_gestures_vb">Volume and brightness gestures</string>
|
<string name="player_gestures_vb">Volume and brightness gestures</string>
|
||||||
<string name="player_gestures_zoom">Zoom gesture</string>
|
<string name="player_gestures_zoom">Zoom gesture</string>
|
||||||
<string name="player_gestures_seek">Seek gesture</string>
|
<string name="player_gestures_seek">Seek gesture</string>
|
||||||
|
<string name="player_gestures_chapter_skip">Chapter gesture</string>
|
||||||
<string name="player_gestures_vb_summary">Swipe up and down on the right side of the screen to change the volume and on the left side to change the brightness</string>
|
<string name="player_gestures_vb_summary">Swipe up and down on the right side of the screen to change the volume and on the left side to change the brightness</string>
|
||||||
<string name="player_gestures_zoom_summary">Pinch to fill the screen with the video</string>
|
<string name="player_gestures_zoom_summary">Pinch to fill the screen with the video</string>
|
||||||
|
<string name="player_gestures_chapter_skip_summary">Long press on Left / Right side to skip chapters (overrides 2x speed gesture)</string>
|
||||||
<string name="player_gestures_seek_summary">Swipe horizontally to seek forwards or backwards</string>
|
<string name="player_gestures_seek_summary">Swipe horizontally to seek forwards or backwards</string>
|
||||||
<string name="player_brightness_remember">Remember brightness level</string>
|
<string name="player_brightness_remember">Remember brightness level</string>
|
||||||
|
<string name="player_start_maximized">Start maximized</string>
|
||||||
|
<string name="player_start_maximized_summary">Open video in maximized mode by default</string>
|
||||||
<string name="sort_by_options_0">Title</string>
|
<string name="sort_by_options_0">Title</string>
|
||||||
<string name="sort_by_options_1">IMDB Rating</string>
|
<string name="sort_by_options_1">IMDB Rating</string>
|
||||||
<string name="sort_by_options_2">Parental Rating</string>
|
<string name="sort_by_options_2">Parental Rating</string>
|
||||||
|
@ -143,6 +147,9 @@
|
||||||
<string name="pref_player_intro_skipper_summary">Requires <i>ConfusedPolarBear\'s</i> <b>Intro Skipper</b> plugin to be installed on the server.\nInstall <i>jumoog\'s</i> <b>Intro Skipper v0.1.8.0 or higher</b> to skip end credits.</string>
|
<string name="pref_player_intro_skipper_summary">Requires <i>ConfusedPolarBear\'s</i> <b>Intro Skipper</b> plugin to be installed on the server.\nInstall <i>jumoog\'s</i> <b>Intro Skipper v0.1.8.0 or higher</b> to skip end credits.</string>
|
||||||
<string name="pref_player_trick_play">Trick Play</string>
|
<string name="pref_player_trick_play">Trick Play</string>
|
||||||
<string name="pref_player_trick_play_summary">Requires <i>nicknsy\'s</i> <b>Jellyscrub</b> plugin to be installed on the server</string>
|
<string name="pref_player_trick_play_summary">Requires <i>nicknsy\'s</i> <b>Jellyscrub</b> plugin to be installed on the server</string>
|
||||||
|
<string name="pref_player_trick_play_summary">Requires nicknsy\'s Jellyscrub plugin to be installed on the server</string>
|
||||||
|
<string name="pref_player_chapter_markers">Chapter markers</string>
|
||||||
|
<string name="pref_player_chapter_markers_summary">Display chapter markers on the timebar</string>
|
||||||
<string name="addresses">Addresses</string>
|
<string name="addresses">Addresses</string>
|
||||||
<string name="add_address">Add address</string>
|
<string name="add_address">Add address</string>
|
||||||
<string name="add_server_address">Add server address</string>
|
<string name="add_server_address">Add server address</string>
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
app:title="@string/pref_player_mpv_hwdec"
|
app:title="@string/pref_player_mpv_hwdec"
|
||||||
app:useSimpleSummaryProvider="true" />
|
app:useSimpleSummaryProvider="true" />
|
||||||
<ListPreference
|
<ListPreference
|
||||||
app:defaultValue="gpu"
|
app:defaultValue="gpu-next"
|
||||||
app:dependency="pref_player_mpv"
|
app:dependency="pref_player_mpv"
|
||||||
app:entries="@array/mpv_vos"
|
app:entries="@array/mpv_vos"
|
||||||
app:entryValues="@array/mpv_vos"
|
app:entryValues="@array/mpv_vos"
|
||||||
|
@ -59,10 +59,21 @@
|
||||||
app:key="pref_player_gestures_seek"
|
app:key="pref_player_gestures_seek"
|
||||||
app:summary="@string/player_gestures_seek_summary"
|
app:summary="@string/player_gestures_seek_summary"
|
||||||
app:title="@string/player_gestures_seek" />
|
app:title="@string/player_gestures_seek" />
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:defaultValue="true"
|
||||||
|
app:dependency="pref_player_gestures"
|
||||||
|
app:key="pref_player_gestures_chapter_skip"
|
||||||
|
app:summary="@string/player_gestures_chapter_skip_summary"
|
||||||
|
app:title="@string/player_gestures_chapter_skip" />
|
||||||
<SwitchPreferenceCompat
|
<SwitchPreferenceCompat
|
||||||
app:dependency="pref_player_gestures_vb"
|
app:dependency="pref_player_gestures_vb"
|
||||||
app:key="pref_player_brightness_remember"
|
app:key="pref_player_brightness_remember"
|
||||||
app:title="@string/player_brightness_remember" />
|
app:title="@string/player_brightness_remember" />
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:dependency="pref_player_gestures_zoom"
|
||||||
|
app:key="pref_player_start_maximized"
|
||||||
|
app:summary="@string/player_start_maximized_summary"
|
||||||
|
app:title="@string/player_start_maximized" />
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory app:title="@string/seeking">
|
<PreferenceCategory app:title="@string/seeking">
|
||||||
|
@ -92,6 +103,13 @@
|
||||||
app:title="@string/pref_player_trick_play"
|
app:title="@string/pref_player_trick_play"
|
||||||
app:widgetLayout="@layout/preference_material3_switch" />
|
app:widgetLayout="@layout/preference_material3_switch" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:defaultValue="true"
|
||||||
|
app:key="pref_player_chapter_markers"
|
||||||
|
app:summary="@string/pref_player_chapter_markers_summary"
|
||||||
|
app:title="@string/pref_player_chapter_markers"
|
||||||
|
app:widgetLayout="@layout/preference_material3_switch" />
|
||||||
|
|
||||||
<PreferenceCategory app:title="@string/picture_in_picture">
|
<PreferenceCategory app:title="@string/picture_in_picture">
|
||||||
<SwitchPreferenceCompat
|
<SwitchPreferenceCompat
|
||||||
app:key="pref_player_picture_in_picture_gesture"
|
app:key="pref_player_picture_in_picture_gesture"
|
||||||
|
|
831
data/schemas/dev.jdtech.jellyfin.database.ServerDatabase/4.json
Normal file
831
data/schemas/dev.jdtech.jellyfin.database.ServerDatabase/4.json
Normal file
|
@ -0,0 +1,831 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 4,
|
||||||
|
"identityHash": "45100d543bb99759e0a8886a70d5caa2",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "servers",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `currentServerAddressId` TEXT, `currentUserId` TEXT, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "currentServerAddressId",
|
||||||
|
"columnName": "currentServerAddressId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "currentUserId",
|
||||||
|
"columnName": "currentUserId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "serverAddresses",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` TEXT NOT NULL, `address` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "address",
|
||||||
|
"columnName": "address",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_serverAddresses_serverId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_serverAddresses_serverId` ON `${TABLE_NAME}` (`serverId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "servers",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "users",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `serverId` TEXT NOT NULL, `accessToken` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessToken",
|
||||||
|
"columnName": "accessToken",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_users_serverId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "servers",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "movies",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` TEXT, `name` TEXT NOT NULL, `originalTitle` TEXT, `overview` TEXT NOT NULL, `runtimeTicks` INTEGER NOT NULL, `premiereDate` INTEGER, `communityRating` REAL, `officialRating` TEXT, `status` TEXT NOT NULL, `productionYear` INTEGER, `endDate` INTEGER, `chapters` TEXT, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "originalTitle",
|
||||||
|
"columnName": "originalTitle",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "overview",
|
||||||
|
"columnName": "overview",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "runtimeTicks",
|
||||||
|
"columnName": "runtimeTicks",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "premiereDate",
|
||||||
|
"columnName": "premiereDate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "communityRating",
|
||||||
|
"columnName": "communityRating",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "officialRating",
|
||||||
|
"columnName": "officialRating",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "status",
|
||||||
|
"columnName": "status",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "productionYear",
|
||||||
|
"columnName": "productionYear",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "endDate",
|
||||||
|
"columnName": "endDate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "chapters",
|
||||||
|
"columnName": "chapters",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "shows",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` TEXT, `name` TEXT NOT NULL, `originalTitle` TEXT, `overview` TEXT NOT NULL, `runtimeTicks` INTEGER NOT NULL, `communityRating` REAL, `officialRating` TEXT, `status` TEXT NOT NULL, `productionYear` INTEGER, `endDate` INTEGER, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "originalTitle",
|
||||||
|
"columnName": "originalTitle",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "overview",
|
||||||
|
"columnName": "overview",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "runtimeTicks",
|
||||||
|
"columnName": "runtimeTicks",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "communityRating",
|
||||||
|
"columnName": "communityRating",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "officialRating",
|
||||||
|
"columnName": "officialRating",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "status",
|
||||||
|
"columnName": "status",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "productionYear",
|
||||||
|
"columnName": "productionYear",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "endDate",
|
||||||
|
"columnName": "endDate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "seasons",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `seriesId` TEXT NOT NULL, `name` TEXT NOT NULL, `seriesName` TEXT NOT NULL, `overview` TEXT NOT NULL, `indexNumber` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`seriesId`) REFERENCES `shows`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "seriesId",
|
||||||
|
"columnName": "seriesId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "seriesName",
|
||||||
|
"columnName": "seriesName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "overview",
|
||||||
|
"columnName": "overview",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "indexNumber",
|
||||||
|
"columnName": "indexNumber",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_seasons_seriesId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"seriesId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_seasons_seriesId` ON `${TABLE_NAME}` (`seriesId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "shows",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"seriesId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "episodes",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` TEXT, `seasonId` TEXT NOT NULL, `seriesId` TEXT NOT NULL, `name` TEXT NOT NULL, `seriesName` TEXT NOT NULL, `overview` TEXT NOT NULL, `indexNumber` INTEGER NOT NULL, `indexNumberEnd` INTEGER, `parentIndexNumber` INTEGER NOT NULL, `runtimeTicks` INTEGER NOT NULL, `premiereDate` INTEGER, `communityRating` REAL, `chapters` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`seasonId`) REFERENCES `seasons`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`seriesId`) REFERENCES `shows`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "seasonId",
|
||||||
|
"columnName": "seasonId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "seriesId",
|
||||||
|
"columnName": "seriesId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "seriesName",
|
||||||
|
"columnName": "seriesName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "overview",
|
||||||
|
"columnName": "overview",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "indexNumber",
|
||||||
|
"columnName": "indexNumber",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "indexNumberEnd",
|
||||||
|
"columnName": "indexNumberEnd",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "parentIndexNumber",
|
||||||
|
"columnName": "parentIndexNumber",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "runtimeTicks",
|
||||||
|
"columnName": "runtimeTicks",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "premiereDate",
|
||||||
|
"columnName": "premiereDate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "communityRating",
|
||||||
|
"columnName": "communityRating",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "chapters",
|
||||||
|
"columnName": "chapters",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_episodes_seasonId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"seasonId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_episodes_seasonId` ON `${TABLE_NAME}` (`seasonId`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_episodes_seriesId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"seriesId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_episodes_seriesId` ON `${TABLE_NAME}` (`seriesId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "seasons",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"seasonId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "shows",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"seriesId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "sources",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itemId` TEXT NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `path` TEXT NOT NULL, `downloadId` INTEGER, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "itemId",
|
||||||
|
"columnName": "itemId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "path",
|
||||||
|
"columnName": "path",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "downloadId",
|
||||||
|
"columnName": "downloadId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "mediastreams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `sourceId` TEXT NOT NULL, `title` TEXT NOT NULL, `displayTitle` TEXT, `language` TEXT NOT NULL, `type` TEXT NOT NULL, `codec` TEXT NOT NULL, `isExternal` INTEGER NOT NULL, `path` TEXT NOT NULL, `channelLayout` TEXT, `videoRangeType` TEXT, `height` INTEGER, `width` INTEGER, `videoDoViTitle` TEXT, `downloadId` INTEGER, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sourceId",
|
||||||
|
"columnName": "sourceId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayTitle",
|
||||||
|
"columnName": "displayTitle",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "language",
|
||||||
|
"columnName": "language",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "codec",
|
||||||
|
"columnName": "codec",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isExternal",
|
||||||
|
"columnName": "isExternal",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "path",
|
||||||
|
"columnName": "path",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "channelLayout",
|
||||||
|
"columnName": "channelLayout",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoRangeType",
|
||||||
|
"columnName": "videoRangeType",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "height",
|
||||||
|
"columnName": "height",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "width",
|
||||||
|
"columnName": "width",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoDoViTitle",
|
||||||
|
"columnName": "videoDoViTitle",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "downloadId",
|
||||||
|
"columnName": "downloadId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "trickPlayManifests",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`itemId` TEXT NOT NULL, `version` TEXT NOT NULL, `resolution` INTEGER NOT NULL, PRIMARY KEY(`itemId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "itemId",
|
||||||
|
"columnName": "itemId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "resolution",
|
||||||
|
"columnName": "resolution",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"itemId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "intros",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`itemId` TEXT NOT NULL, `start` REAL NOT NULL, `end` REAL NOT NULL, `showAt` REAL NOT NULL, `hideAt` REAL NOT NULL, PRIMARY KEY(`itemId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "itemId",
|
||||||
|
"columnName": "itemId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "start",
|
||||||
|
"columnName": "start",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "end",
|
||||||
|
"columnName": "end",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "showAt",
|
||||||
|
"columnName": "showAt",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "hideAt",
|
||||||
|
"columnName": "hideAt",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"itemId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "userdata",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `itemId` TEXT NOT NULL, `played` INTEGER NOT NULL, `favorite` INTEGER NOT NULL, `playbackPositionTicks` INTEGER NOT NULL, `toBeSynced` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "userId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "itemId",
|
||||||
|
"columnName": "itemId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "played",
|
||||||
|
"columnName": "played",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favorite",
|
||||||
|
"columnName": "favorite",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "playbackPositionTicks",
|
||||||
|
"columnName": "playbackPositionTicks",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "toBeSynced",
|
||||||
|
"columnName": "toBeSynced",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"userId",
|
||||||
|
"itemId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '45100d543bb99759e0a8886a70d5caa2')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
package dev.jdtech.jellyfin.database
|
package dev.jdtech.jellyfin.database
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
|
import dev.jdtech.jellyfin.models.FindroidChapter
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import org.jellyfin.sdk.model.DateTime
|
import org.jellyfin.sdk.model.DateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
@ -25,4 +28,14 @@ class Converters {
|
||||||
fun fromLongToDatetime(value: Long?): DateTime? {
|
fun fromLongToDatetime(value: Long?): DateTime? {
|
||||||
return value?.let { DateTime.ofEpochSecond(it, 0, ZoneOffset.UTC) }
|
return value?.let { DateTime.ofEpochSecond(it, 0, ZoneOffset.UTC) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromFindroidChaptersToString(value: List<FindroidChapter>?): String? {
|
||||||
|
return value?.let { Json.encodeToString(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromStringToFindroidChapters(value: String?): List<FindroidChapter>? {
|
||||||
|
return value?.let { Json.decodeFromString(value) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,10 @@ import dev.jdtech.jellyfin.models.User
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Server::class, ServerAddress::class, User::class, FindroidMovieDto::class, FindroidShowDto::class, FindroidSeasonDto::class, FindroidEpisodeDto::class, FindroidSourceDto::class, FindroidMediaStreamDto::class, TrickPlayManifestDto::class, IntroDto::class, CreditDto::class, FindroidUserDataDto::class],
|
entities = [Server::class, ServerAddress::class, User::class, FindroidMovieDto::class, FindroidShowDto::class, FindroidSeasonDto::class, FindroidEpisodeDto::class, FindroidSourceDto::class, FindroidMediaStreamDto::class, TrickPlayManifestDto::class, IntroDto::class, CreditDto::class, FindroidUserDataDto::class],
|
||||||
version = 3,
|
version = 4,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(from = 2, to = 3),
|
AutoMigration(from = 2, to = 3),
|
||||||
|
AutoMigration(from = 3, to = 4),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|
|
@ -205,7 +205,7 @@ interface ServerDatabaseDao {
|
||||||
@Query("SELECT * FROM episodes WHERE serverId = :serverId ORDER BY seriesName ASC, parentIndexNumber ASC, indexNumber ASC")
|
@Query("SELECT * FROM episodes WHERE serverId = :serverId ORDER BY seriesName ASC, parentIndexNumber ASC, indexNumber ASC")
|
||||||
fun getEpisodesByServerId(serverId: String): List<FindroidEpisodeDto>
|
fun getEpisodesByServerId(serverId: String): List<FindroidEpisodeDto>
|
||||||
|
|
||||||
@Query("SELECT episodes.id, episodes.serverId, episodes.seasonId, episodes.seriesId, episodes.name, episodes.seriesName, episodes.overview, episodes.indexNumber, episodes.indexNumberEnd, episodes.parentIndexNumber, episodes.runtimeTicks, episodes.premiereDate, episodes.communityRating FROM episodes INNER JOIN userdata ON episodes.id = userdata.itemId WHERE serverId = :serverId AND playbackPositionTicks > 0 ORDER BY episodes.parentIndexNumber ASC, episodes.indexNumber ASC")
|
@Query("SELECT episodes.id, episodes.serverId, episodes.seasonId, episodes.seriesId, episodes.name, episodes.seriesName, episodes.overview, episodes.indexNumber, episodes.indexNumberEnd, episodes.parentIndexNumber, episodes.runtimeTicks, episodes.premiereDate, episodes.communityRating, episodes.chapters FROM episodes INNER JOIN userdata ON episodes.id = userdata.itemId WHERE serverId = :serverId AND playbackPositionTicks > 0 ORDER BY episodes.parentIndexNumber ASC, episodes.indexNumber ASC")
|
||||||
fun getEpisodeResumeItems(serverId: String): List<FindroidEpisodeDto>
|
fun getEpisodeResumeItems(serverId: String): List<FindroidEpisodeDto>
|
||||||
|
|
||||||
@Query("DELETE FROM episodes WHERE id = :id")
|
@Query("DELETE FROM episodes WHERE id = :id")
|
||||||
|
|
|
@ -9,6 +9,7 @@ enum class CollectionType(val type: String) {
|
||||||
Books("books"),
|
Books("books"),
|
||||||
LiveTv("livetv"),
|
LiveTv("livetv"),
|
||||||
BoxSets("boxsets"),
|
BoxSets("boxsets"),
|
||||||
|
Mixed("null"),
|
||||||
Unknown("unknown"),
|
Unknown("unknown"),
|
||||||
;
|
;
|
||||||
|
|
||||||
|
@ -19,11 +20,12 @@ enum class CollectionType(val type: String) {
|
||||||
Movies,
|
Movies,
|
||||||
TvShows,
|
TvShows,
|
||||||
BoxSets,
|
BoxSets,
|
||||||
|
Mixed,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun fromString(string: String?): CollectionType {
|
fun fromString(string: String?): CollectionType {
|
||||||
if (string == null) {
|
if (string == null) { // TODO jellyfin returns null as the collectiontype for mixed libraries. This is obviously wrong, but probably an upstream issue. Should be fixed whenever upstream fixes this
|
||||||
return defaultValue
|
return Mixed
|
||||||
}
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
|
|
|
@ -18,6 +18,7 @@ data class FindroidBoxSet(
|
||||||
override val playbackPositionTicks: Long = 0L,
|
override val playbackPositionTicks: Long = 0L,
|
||||||
override val unplayedItemCount: Int? = null,
|
override val unplayedItemCount: Int? = null,
|
||||||
override val images: FindroidImages,
|
override val images: FindroidImages,
|
||||||
|
override val chapters: List<FindroidChapter>? = null,
|
||||||
) : FindroidItem
|
) : FindroidItem
|
||||||
|
|
||||||
fun BaseItemDto.toFindroidBoxSet(
|
fun BaseItemDto.toFindroidBoxSet(
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
package dev.jdtech.jellyfin.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FindroidChapter(
|
||||||
|
/**
|
||||||
|
* The start position.
|
||||||
|
*/
|
||||||
|
val startPosition: Long,
|
||||||
|
/**
|
||||||
|
* The name.
|
||||||
|
*/
|
||||||
|
val name: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun BaseItemDto.toFindroidChapters(): List<FindroidChapter>? {
|
||||||
|
return chapters?.map { chapter ->
|
||||||
|
FindroidChapter(
|
||||||
|
startPosition = chapter.startPositionTicks / 10000,
|
||||||
|
name = chapter.name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ data class FindroidCollection(
|
||||||
override val unplayedItemCount: Int? = null,
|
override val unplayedItemCount: Int? = null,
|
||||||
val type: CollectionType,
|
val type: CollectionType,
|
||||||
override val images: FindroidImages,
|
override val images: FindroidImages,
|
||||||
|
override val chapters: List<FindroidChapter>? = null,
|
||||||
) : FindroidItem
|
) : FindroidItem
|
||||||
|
|
||||||
fun BaseItemDto.toFindroidCollection(
|
fun BaseItemDto.toFindroidCollection(
|
||||||
|
|
|
@ -31,6 +31,7 @@ data class FindroidEpisode(
|
||||||
override val unplayedItemCount: Int? = null,
|
override val unplayedItemCount: Int? = null,
|
||||||
val missing: Boolean = false,
|
val missing: Boolean = false,
|
||||||
override val images: FindroidImages,
|
override val images: FindroidImages,
|
||||||
|
override val chapters: List<FindroidChapter>?,
|
||||||
) : FindroidItem, FindroidSources
|
) : FindroidItem, FindroidSources
|
||||||
|
|
||||||
suspend fun BaseItemDto.toFindroidEpisode(
|
suspend fun BaseItemDto.toFindroidEpisode(
|
||||||
|
@ -65,6 +66,7 @@ suspend fun BaseItemDto.toFindroidEpisode(
|
||||||
communityRating = communityRating,
|
communityRating = communityRating,
|
||||||
missing = locationType == LocationType.VIRTUAL,
|
missing = locationType == LocationType.VIRTUAL,
|
||||||
images = toFindroidImages(jellyfinRepository),
|
images = toFindroidImages(jellyfinRepository),
|
||||||
|
chapters = toFindroidChapters(),
|
||||||
)
|
)
|
||||||
} catch (_: NullPointerException) {
|
} catch (_: NullPointerException) {
|
||||||
null
|
null
|
||||||
|
@ -94,5 +96,6 @@ fun FindroidEpisodeDto.toFindroidEpisode(database: ServerDatabaseDao, userId: UU
|
||||||
seasonId = seasonId,
|
seasonId = seasonId,
|
||||||
communityRating = communityRating,
|
communityRating = communityRating,
|
||||||
images = FindroidImages(),
|
images = FindroidImages(),
|
||||||
|
chapters = chapters,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ data class FindroidEpisodeDto(
|
||||||
val runtimeTicks: Long,
|
val runtimeTicks: Long,
|
||||||
val premiereDate: LocalDateTime?,
|
val premiereDate: LocalDateTime?,
|
||||||
val communityRating: Float?,
|
val communityRating: Float?,
|
||||||
|
val chapters: List<FindroidChapter>?,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun FindroidEpisode.toFindroidEpisodeDto(serverId: String? = null): FindroidEpisodeDto {
|
fun FindroidEpisode.toFindroidEpisodeDto(serverId: String? = null): FindroidEpisodeDto {
|
||||||
|
@ -60,5 +61,6 @@ fun FindroidEpisode.toFindroidEpisodeDto(serverId: String? = null): FindroidEpis
|
||||||
runtimeTicks = runtimeTicks,
|
runtimeTicks = runtimeTicks,
|
||||||
premiereDate = premiereDate,
|
premiereDate = premiereDate,
|
||||||
communityRating = communityRating,
|
communityRating = communityRating,
|
||||||
|
chapters = chapters,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
package dev.jdtech.jellyfin.models
|
||||||
|
|
||||||
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
data class FindroidFolder(
|
||||||
|
override val id: UUID,
|
||||||
|
override val name: String,
|
||||||
|
override val originalTitle: String? = null,
|
||||||
|
override val overview: String = "",
|
||||||
|
override val played: Boolean,
|
||||||
|
override val favorite: Boolean,
|
||||||
|
override val canPlay: Boolean = false,
|
||||||
|
override val canDownload: Boolean = false,
|
||||||
|
override val sources: List<FindroidSource> = emptyList(),
|
||||||
|
override val runtimeTicks: Long = 0L,
|
||||||
|
override val playbackPositionTicks: Long = 0L,
|
||||||
|
override val unplayedItemCount: Int?,
|
||||||
|
override val images: FindroidImages,
|
||||||
|
override val chapters: List<FindroidChapter>? = null,
|
||||||
|
) : FindroidItem
|
||||||
|
|
||||||
|
fun BaseItemDto.toFindroidFolder(
|
||||||
|
jellyfinRepository: JellyfinRepository,
|
||||||
|
): FindroidFolder {
|
||||||
|
return FindroidFolder(
|
||||||
|
id = id,
|
||||||
|
name = name.orEmpty(),
|
||||||
|
played = userData?.played ?: false,
|
||||||
|
favorite = userData?.isFavorite ?: false,
|
||||||
|
unplayedItemCount = userData?.unplayedItemCount,
|
||||||
|
images = toFindroidImages(jellyfinRepository),
|
||||||
|
)
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ interface FindroidItem {
|
||||||
val playbackPositionTicks: Long
|
val playbackPositionTicks: Long
|
||||||
val unplayedItemCount: Int?
|
val unplayedItemCount: Int?
|
||||||
val images: FindroidImages
|
val images: FindroidImages
|
||||||
|
val chapters: List<FindroidChapter>?
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun BaseItemDto.toFindroidItem(
|
suspend fun BaseItemDto.toFindroidItem(
|
||||||
|
@ -32,6 +33,7 @@ suspend fun BaseItemDto.toFindroidItem(
|
||||||
BaseItemKind.SEASON -> toFindroidSeason(jellyfinRepository)
|
BaseItemKind.SEASON -> toFindroidSeason(jellyfinRepository)
|
||||||
BaseItemKind.SERIES -> toFindroidShow(jellyfinRepository)
|
BaseItemKind.SERIES -> toFindroidShow(jellyfinRepository)
|
||||||
BaseItemKind.BOX_SET -> toFindroidBoxSet(jellyfinRepository)
|
BaseItemKind.BOX_SET -> toFindroidBoxSet(jellyfinRepository)
|
||||||
|
BaseItemKind.FOLDER -> toFindroidFolder(jellyfinRepository)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ data class FindroidMovie(
|
||||||
val trailer: String?,
|
val trailer: String?,
|
||||||
override val unplayedItemCount: Int? = null,
|
override val unplayedItemCount: Int? = null,
|
||||||
override val images: FindroidImages,
|
override val images: FindroidImages,
|
||||||
|
override val chapters: List<FindroidChapter>?,
|
||||||
) : FindroidItem, FindroidSources
|
) : FindroidItem, FindroidSources
|
||||||
|
|
||||||
suspend fun BaseItemDto.toFindroidMovie(
|
suspend fun BaseItemDto.toFindroidMovie(
|
||||||
|
@ -64,6 +65,7 @@ suspend fun BaseItemDto.toFindroidMovie(
|
||||||
endDate = endDate,
|
endDate = endDate,
|
||||||
trailer = remoteTrailers?.getOrNull(0)?.url,
|
trailer = remoteTrailers?.getOrNull(0)?.url,
|
||||||
images = toFindroidImages(jellyfinRepository),
|
images = toFindroidImages(jellyfinRepository),
|
||||||
|
chapters = toFindroidChapters(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,5 +93,6 @@ fun FindroidMovieDto.toFindroidMovie(database: ServerDatabaseDao, userId: UUID):
|
||||||
sources = database.getSources(id).map { it.toFindroidSource(database) },
|
sources = database.getSources(id).map { it.toFindroidSource(database) },
|
||||||
trailer = null,
|
trailer = null,
|
||||||
images = FindroidImages(),
|
images = FindroidImages(),
|
||||||
|
chapters = chapters,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ data class FindroidMovieDto(
|
||||||
val status: String,
|
val status: String,
|
||||||
val productionYear: Int?,
|
val productionYear: Int?,
|
||||||
val endDate: LocalDateTime?,
|
val endDate: LocalDateTime?,
|
||||||
|
val chapters: List<FindroidChapter>?,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun FindroidMovie.toFindroidMovieDto(serverId: String? = null): FindroidMovieDto {
|
fun FindroidMovie.toFindroidMovieDto(serverId: String? = null): FindroidMovieDto {
|
||||||
|
@ -36,5 +37,6 @@ fun FindroidMovie.toFindroidMovieDto(serverId: String? = null): FindroidMovieDto
|
||||||
status = status,
|
status = status,
|
||||||
productionYear = productionYear,
|
productionYear = productionYear,
|
||||||
endDate = endDate,
|
endDate = endDate,
|
||||||
|
chapters = chapters,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ data class FindroidSeason(
|
||||||
override val playbackPositionTicks: Long = 0L,
|
override val playbackPositionTicks: Long = 0L,
|
||||||
override val unplayedItemCount: Int?,
|
override val unplayedItemCount: Int?,
|
||||||
override val images: FindroidImages,
|
override val images: FindroidImages,
|
||||||
|
override val chapters: List<FindroidChapter>? = null,
|
||||||
) : FindroidItem
|
) : FindroidItem
|
||||||
|
|
||||||
fun BaseItemDto.toFindroidSeason(
|
fun BaseItemDto.toFindroidSeason(
|
||||||
|
|
|
@ -31,6 +31,7 @@ data class FindroidShow(
|
||||||
val endDate: DateTime?,
|
val endDate: DateTime?,
|
||||||
val trailer: String?,
|
val trailer: String?,
|
||||||
override val images: FindroidImages,
|
override val images: FindroidImages,
|
||||||
|
override val chapters: List<FindroidChapter>? = null,
|
||||||
) : FindroidItem
|
) : FindroidItem
|
||||||
|
|
||||||
fun BaseItemDto.toFindroidShow(
|
fun BaseItemDto.toFindroidShow(
|
||||||
|
|
|
@ -215,7 +215,7 @@ class JellyfinRepositoryImpl(
|
||||||
val items = withContext(Dispatchers.IO) {
|
val items = withContext(Dispatchers.IO) {
|
||||||
jellyfinApi.itemsApi.getResumeItems(
|
jellyfinApi.itemsApi.getResumeItems(
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
limit = 6,
|
limit = 12,
|
||||||
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE),
|
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE),
|
||||||
).content.items.orEmpty()
|
).content.items.orEmpty()
|
||||||
}
|
}
|
||||||
|
@ -229,7 +229,7 @@ class JellyfinRepositoryImpl(
|
||||||
jellyfinApi.userLibraryApi.getLatestMedia(
|
jellyfinApi.userLibraryApi.getLatestMedia(
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
parentId = parentId,
|
parentId = parentId,
|
||||||
limit = 12,
|
limit = 16,
|
||||||
).content
|
).content
|
||||||
}
|
}
|
||||||
return items.mapNotNull {
|
return items.mapNotNull {
|
||||||
|
@ -252,7 +252,7 @@ class JellyfinRepositoryImpl(
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
jellyfinApi.showsApi.getNextUp(
|
jellyfinApi.showsApi.getNextUp(
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
limit = 9,
|
limit = 24,
|
||||||
seriesId = seriesId?.toString(),
|
seriesId = seriesId?.toString(),
|
||||||
).content.items
|
).content.items
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
[versions]
|
[versions]
|
||||||
aboutlibraries = "10.10.0"
|
aboutlibraries = "10.10.0"
|
||||||
android-plugin = "8.2.1"
|
android-plugin = "8.2.2"
|
||||||
androidx-activity = "1.8.2"
|
androidx-activity = "1.8.2"
|
||||||
androidx-appcompat = "1.6.1"
|
androidx-appcompat = "1.6.1"
|
||||||
androidx-compose-material3 = "1.2.0-alpha09"
|
androidx-compose-bom = "2024.02.01"
|
||||||
androidx-compose-ui = "1.6.0-alpha07"
|
androidx-compose-material3 = "1.2.0"
|
||||||
androidx-constraintlayout = "2.1.4"
|
androidx-constraintlayout = "2.1.4"
|
||||||
androidx-core = "1.12.0"
|
androidx-core = "1.12.0"
|
||||||
androidx-hilt = "1.1.0"
|
androidx-hilt = "1.2.0"
|
||||||
androidx-lifecycle = "2.6.2"
|
androidx-lifecycle = "2.7.0"
|
||||||
androidx-media3 = "1.2.1"
|
androidx-media3 = "1.2.1"
|
||||||
androidx-navigation = "2.7.6"
|
androidx-navigation = "2.7.7"
|
||||||
androidx-paging = "3.2.1"
|
androidx-paging = "3.2.1"
|
||||||
androidx-preference = "1.2.1"
|
androidx-preference = "1.2.1"
|
||||||
androidx-recyclerview = "1.3.2"
|
androidx-recyclerview = "1.3.2"
|
||||||
|
@ -18,15 +18,15 @@ androidx-room = "2.6.1"
|
||||||
androidx-swiperefreshlayout = "1.1.0"
|
androidx-swiperefreshlayout = "1.1.0"
|
||||||
androidx-tv = "1.0.0-alpha10"
|
androidx-tv = "1.0.0-alpha10"
|
||||||
androidx-work = "2.9.0"
|
androidx-work = "2.9.0"
|
||||||
coil = "2.5.0"
|
coil = "2.6.0"
|
||||||
hilt = "2.50"
|
hilt = "2.50"
|
||||||
compose-destinations = "1.9.62"
|
compose-destinations = "1.10.1"
|
||||||
jellyfin = "1.4.6"
|
jellyfin = "1.4.6"
|
||||||
kotlin = "1.9.22"
|
kotlin = "1.9.22"
|
||||||
kotlinx-serialization = "1.6.2"
|
kotlinx-serialization = "1.6.3"
|
||||||
ksp = "1.9.22-1.0.17"
|
ksp = "1.9.22-1.0.17"
|
||||||
ktlint = "12.1.0"
|
ktlint = "12.1.0"
|
||||||
libmpv = "0.1.4"
|
libmpv = "0.2.0"
|
||||||
material = "1.11.0"
|
material = "1.11.0"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
|
|
||||||
|
@ -36,10 +36,11 @@ aboutlibraries = { module = "com.mikepenz:aboutlibraries", version.ref = "aboutl
|
||||||
androidx-activity = { module = "androidx.activity:activity", version.ref = "androidx-activity" }
|
androidx-activity = { module = "androidx.activity:activity", version.ref = "androidx-activity" }
|
||||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
||||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
||||||
|
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidx-compose-bom" }
|
||||||
androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "androidx-compose-material3" }
|
androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "androidx-compose-material3" }
|
||||||
androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "androidx-compose-ui" }
|
androidx-compose-ui = { module = "androidx.compose.ui:ui" }
|
||||||
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "androidx-compose-ui" }
|
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||||
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "androidx-compose-ui" }
|
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||||
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" }
|
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" }
|
||||||
androidx-core = { module = "androidx.core:core", version.ref = "androidx-core" }
|
androidx-core = { module = "androidx.core:core", version.ref = "androidx-core" }
|
||||||
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidx-hilt" }
|
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidx-hilt" }
|
||||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|
20
gradlew.bat
vendored
20
gradlew.bat
vendored
|
@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package dev.jdtech.jellyfin.models
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class PlayerChapter(
|
||||||
|
/**
|
||||||
|
* The start position.
|
||||||
|
*/
|
||||||
|
val startPosition: Long,
|
||||||
|
/**
|
||||||
|
* The name.
|
||||||
|
*/
|
||||||
|
val name: String? = null,
|
||||||
|
) : Parcelable
|
|
@ -15,4 +15,5 @@ data class PlayerItem(
|
||||||
val indexNumber: Int? = null,
|
val indexNumber: Int? = null,
|
||||||
val indexNumberEnd: Int? = null,
|
val indexNumberEnd: Int? = null,
|
||||||
val externalSubtitles: List<ExternalSubtitle> = emptyList(),
|
val externalSubtitles: List<ExternalSubtitle> = emptyList(),
|
||||||
|
val chapters: List<PlayerChapter>? = null,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
|
@ -50,7 +50,7 @@ class MPVPlayer(
|
||||||
private var trackSelectionParameters: TrackSelectionParameters = TrackSelectionParameters.Builder(context).build(),
|
private var trackSelectionParameters: TrackSelectionParameters = TrackSelectionParameters.Builder(context).build(),
|
||||||
private val seekBackIncrement: Long = C.DEFAULT_SEEK_BACK_INCREMENT_MS,
|
private val seekBackIncrement: Long = C.DEFAULT_SEEK_BACK_INCREMENT_MS,
|
||||||
private val seekForwardIncrement: Long = C.DEFAULT_SEEK_FORWARD_INCREMENT_MS,
|
private val seekForwardIncrement: Long = C.DEFAULT_SEEK_FORWARD_INCREMENT_MS,
|
||||||
videoOutput: String = "gpu",
|
videoOutput: String = "gpu-next",
|
||||||
audioOutput: String = "audiotrack",
|
audioOutput: String = "audiotrack",
|
||||||
hwDec: String = "mediacodec",
|
hwDec: String = "mediacodec",
|
||||||
) : BasePlayer(), MPVLib.EventObserver, AudioManager.OnAudioFocusChangeListener {
|
) : BasePlayer(), MPVLib.EventObserver, AudioManager.OnAudioFocusChangeListener {
|
||||||
|
@ -77,9 +77,11 @@ class MPVPlayer(
|
||||||
// General
|
// General
|
||||||
MPVLib.setOptionString("config", "yes")
|
MPVLib.setOptionString("config", "yes")
|
||||||
MPVLib.setOptionString("config-dir", mpvDir.path)
|
MPVLib.setOptionString("config-dir", mpvDir.path)
|
||||||
|
MPVLib.setOptionString("profile", "fast")
|
||||||
MPVLib.setOptionString("vo", videoOutput)
|
MPVLib.setOptionString("vo", videoOutput)
|
||||||
MPVLib.setOptionString("ao", audioOutput)
|
MPVLib.setOptionString("ao", audioOutput)
|
||||||
MPVLib.setOptionString("gpu-context", "android")
|
MPVLib.setOptionString("gpu-context", "android")
|
||||||
|
MPVLib.setOptionString("opengl-es", "yes")
|
||||||
|
|
||||||
// Hardware video decoding
|
// Hardware video decoding
|
||||||
MPVLib.setOptionString("hwdec", hwDec)
|
MPVLib.setOptionString("hwdec", hwDec)
|
||||||
|
@ -108,8 +110,6 @@ class MPVPlayer(
|
||||||
MPVLib.setOptionString("save-position-on-quit", "no")
|
MPVLib.setOptionString("save-position-on-quit", "no")
|
||||||
MPVLib.setOptionString("sub-font-provider", "none")
|
MPVLib.setOptionString("sub-font-provider", "none")
|
||||||
MPVLib.setOptionString("ytdl", "no")
|
MPVLib.setOptionString("ytdl", "no")
|
||||||
// DR is known to ruin performance at least on Exynos devices, see mpv-android#508
|
|
||||||
MPVLib.setOptionString("vd-lavc-dr", "no")
|
|
||||||
|
|
||||||
MPVLib.init()
|
MPVLib.init()
|
||||||
|
|
||||||
|
@ -869,8 +869,11 @@ class MPVPlayer(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
currentIndex = index
|
currentIndex = index
|
||||||
MPVLib.command(arrayOf("playlist-play-index", "$index"))
|
// Only set the playlist index when the index is not the currently playing item. Otherwise playback will be restarted.
|
||||||
MPVLib.setPropertyBoolean("pause", true)
|
// This is a problem on initial load when the first item is still loading causing duplicate external subtitle entries.
|
||||||
|
if (MPVLib.getPropertyInt("playlist-current-pos") != index) {
|
||||||
|
MPVLib.command(arrayOf("playlist-play-index", "$index"))
|
||||||
|
}
|
||||||
listeners.sendEvent(Player.EVENT_TIMELINE_CHANGED) { listener ->
|
listeners.sendEvent(Player.EVENT_TIMELINE_CHANGED) { listener ->
|
||||||
listener.onTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)
|
listener.onTimelineChanged(timeline, Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dev.jdtech.jellyfin.AppPreferences
|
import dev.jdtech.jellyfin.AppPreferences
|
||||||
import dev.jdtech.jellyfin.models.Credits
|
import dev.jdtech.jellyfin.models.Credits
|
||||||
import dev.jdtech.jellyfin.models.Intro
|
import dev.jdtech.jellyfin.models.Intro
|
||||||
|
import dev.jdtech.jellyfin.models.PlayerChapter
|
||||||
import dev.jdtech.jellyfin.models.PlayerItem
|
import dev.jdtech.jellyfin.models.PlayerItem
|
||||||
import dev.jdtech.jellyfin.mpv.MPVPlayer
|
import dev.jdtech.jellyfin.mpv.MPVPlayer
|
||||||
import dev.jdtech.jellyfin.player.video.R
|
import dev.jdtech.jellyfin.player.video.R
|
||||||
|
@ -58,6 +59,7 @@ constructor(
|
||||||
currentIntro = null,
|
currentIntro = null,
|
||||||
currentCredit = null,
|
currentCredit = null,
|
||||||
currentTrickPlay = null,
|
currentTrickPlay = null,
|
||||||
|
currentChapters = null,
|
||||||
fileLoaded = false,
|
fileLoaded = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -76,12 +78,13 @@ constructor(
|
||||||
val currentIntro: Intro?,
|
val currentIntro: Intro?,
|
||||||
val currentCredit: Credits?,
|
val currentCredit: Credits?,
|
||||||
val currentTrickPlay: BifData?,
|
val currentTrickPlay: BifData?,
|
||||||
|
val currentChapters: List<PlayerChapter>?,
|
||||||
val fileLoaded: Boolean,
|
val fileLoaded: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
private var items: Array<PlayerItem> = arrayOf()
|
private var items: Array<PlayerItem> = arrayOf()
|
||||||
|
|
||||||
val trackSelector = DefaultTrackSelector(application)
|
private val trackSelector = DefaultTrackSelector(application)
|
||||||
var playWhenReady = true
|
var playWhenReady = true
|
||||||
private var currentMediaItemIndex = savedStateHandle["mediaItemIndex"] ?: 0
|
private var currentMediaItemIndex = savedStateHandle["mediaItemIndex"] ?: 0
|
||||||
private var playbackPosition: Long = savedStateHandle["position"] ?: 0
|
private var playbackPosition: Long = savedStateHandle["position"] ?: 0
|
||||||
|
@ -295,7 +298,7 @@ constructor(
|
||||||
} else {
|
} else {
|
||||||
item.name
|
item.name
|
||||||
}
|
}
|
||||||
_uiState.update { it.copy(currentItemTitle = itemTitle) }
|
_uiState.update { it.copy(currentItemTitle = itemTitle, currentChapters = item.chapters, fileLoaded = false) }
|
||||||
|
|
||||||
_uiState.update { it.copy(currentCredit = null) }
|
_uiState.update { it.copy(currentCredit = null) }
|
||||||
|
|
||||||
|
@ -386,8 +389,97 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get chapters of current item
|
||||||
|
* @return list of [PlayerChapter]
|
||||||
|
*/
|
||||||
|
private fun getChapters(): List<PlayerChapter>? {
|
||||||
|
return uiState.value.currentChapters
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the index of the current chapter
|
||||||
|
* @return the index of the current chapter
|
||||||
|
*/
|
||||||
|
private fun getCurrentChapterIndex(): Int? {
|
||||||
|
val chapters = getChapters() ?: return null
|
||||||
|
|
||||||
|
for (i in chapters.indices.reversed()) {
|
||||||
|
if (chapters[i].startPosition < player.currentPosition) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the index of the next chapter
|
||||||
|
* @return the index of the next chapter
|
||||||
|
*/
|
||||||
|
private fun getNextChapterIndex(): Int? {
|
||||||
|
val chapters = getChapters() ?: return null
|
||||||
|
val currentChapterIndex = getCurrentChapterIndex() ?: return null
|
||||||
|
|
||||||
|
return minOf(chapters.size - 1, currentChapterIndex + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the index of the previous chapter.
|
||||||
|
* Only use this for seeking as it will return the current chapter when player position is more than 5 seconds past the start of the chapter
|
||||||
|
* @return the index of the previous chapter
|
||||||
|
*/
|
||||||
|
private fun getPreviousChapterIndex(): Int? {
|
||||||
|
val chapters = getChapters() ?: return null
|
||||||
|
val currentChapterIndex = getCurrentChapterIndex() ?: return null
|
||||||
|
|
||||||
|
// Return current chapter when more than 5 seconds past chapter start
|
||||||
|
if (player.currentPosition > chapters[currentChapterIndex].startPosition + 5000L) {
|
||||||
|
return currentChapterIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxOf(0, currentChapterIndex - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isFirstChapter(): Boolean? = getChapters()?.let { getCurrentChapterIndex() == 0 }
|
||||||
|
fun isLastChapter(): Boolean? = getChapters()?.let { chapters -> getCurrentChapterIndex() == chapters.size - 1 }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek to chapter
|
||||||
|
* @param [chapterIndex] the index of the chapter to seek to
|
||||||
|
* @return the [PlayerChapter] which has been sought to
|
||||||
|
*/
|
||||||
|
private fun seekToChapter(chapterIndex: Int): PlayerChapter? {
|
||||||
|
return getChapters()?.getOrNull(chapterIndex)?.also { chapter ->
|
||||||
|
player.seekTo(chapter.startPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek to the next chapter
|
||||||
|
* @return the [PlayerChapter] which has been sought to
|
||||||
|
*/
|
||||||
|
fun seekToNextChapter(): PlayerChapter? {
|
||||||
|
return getNextChapterIndex()?.let { seekToChapter(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek to the previous chapter
|
||||||
|
* Will seek to start of current chapter if player position is more than 5 seconds past start of chapter
|
||||||
|
* @return the [PlayerChapter] which has been sought to
|
||||||
|
*/
|
||||||
|
fun seekToPreviousChapter(): PlayerChapter? {
|
||||||
|
return getPreviousChapterIndex()?.let { seekToChapter(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
|
super.onIsPlayingChanged(isPlaying)
|
||||||
|
eventsChannel.trySend(PlayerEvents.IsPlayingChanged(isPlaying))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface PlayerEvents {
|
sealed interface PlayerEvents {
|
||||||
data object NavigateBack : PlayerEvents
|
data object NavigateBack : PlayerEvents
|
||||||
|
data class IsPlayingChanged(val isPlaying: Boolean) : PlayerEvents
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,14 @@ import androidx.lifecycle.viewModelScope
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dev.jdtech.jellyfin.models.ExternalSubtitle
|
import dev.jdtech.jellyfin.models.ExternalSubtitle
|
||||||
|
import dev.jdtech.jellyfin.models.FindroidChapter
|
||||||
import dev.jdtech.jellyfin.models.FindroidEpisode
|
import dev.jdtech.jellyfin.models.FindroidEpisode
|
||||||
import dev.jdtech.jellyfin.models.FindroidItem
|
import dev.jdtech.jellyfin.models.FindroidItem
|
||||||
import dev.jdtech.jellyfin.models.FindroidMovie
|
import dev.jdtech.jellyfin.models.FindroidMovie
|
||||||
import dev.jdtech.jellyfin.models.FindroidSeason
|
import dev.jdtech.jellyfin.models.FindroidSeason
|
||||||
import dev.jdtech.jellyfin.models.FindroidShow
|
import dev.jdtech.jellyfin.models.FindroidShow
|
||||||
import dev.jdtech.jellyfin.models.FindroidSourceType
|
import dev.jdtech.jellyfin.models.FindroidSourceType
|
||||||
|
import dev.jdtech.jellyfin.models.PlayerChapter
|
||||||
import dev.jdtech.jellyfin.models.PlayerItem
|
import dev.jdtech.jellyfin.models.PlayerItem
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
@ -113,7 +115,7 @@ class PlayerViewModel @Inject internal constructor(
|
||||||
.getEpisodes(
|
.getEpisodes(
|
||||||
seriesId = item.seriesId,
|
seriesId = item.seriesId,
|
||||||
seasonId = item.seasonId,
|
seasonId = item.seasonId,
|
||||||
fields = listOf(ItemFields.MEDIA_SOURCES),
|
fields = listOf(ItemFields.MEDIA_SOURCES, ItemFields.CHAPTERS),
|
||||||
startItemId = item.id,
|
startItemId = item.id,
|
||||||
limit = if (userConfig?.enableNextEpisodeAutoPlay != false) null else 1,
|
limit = if (userConfig?.enableNextEpisodeAutoPlay != false) null else 1,
|
||||||
)
|
)
|
||||||
|
@ -166,8 +168,18 @@ class PlayerViewModel @Inject internal constructor(
|
||||||
indexNumber = if (this is FindroidEpisode) indexNumber else null,
|
indexNumber = if (this is FindroidEpisode) indexNumber else null,
|
||||||
indexNumberEnd = if (this is FindroidEpisode) indexNumberEnd else null,
|
indexNumberEnd = if (this is FindroidEpisode) indexNumberEnd else null,
|
||||||
externalSubtitles = externalSubtitles,
|
externalSubtitles = externalSubtitles,
|
||||||
|
chapters = chapters.toPlayerChapters(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun List<FindroidChapter>?.toPlayerChapters(): List<PlayerChapter>? {
|
||||||
|
return this?.map { chapter ->
|
||||||
|
PlayerChapter(
|
||||||
|
startPosition = chapter.startPosition,
|
||||||
|
name = chapter.name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface PlayerItemsEvent {
|
sealed interface PlayerItemsEvent {
|
||||||
|
|
20
player/video/src/main/res/values-da/strings.xml
Normal file
20
player/video/src/main/res/values-da/strings.xml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="select_audio_track">Vælg lyd spor</string>
|
||||||
|
<string name="player_controls_play_pause">Afspilnings pause</string>
|
||||||
|
<string name="select_subtile_track">Vælg undertekster</string>
|
||||||
|
<string name="select_playback_speed">Vælg afspilnings hastighed</string>
|
||||||
|
<string name="select_a_version">Vælg en version</string>
|
||||||
|
<string name="external">Ekstern</string>
|
||||||
|
<string name="player_controls_picture_in_picture">Start billed i billed</string>
|
||||||
|
<string name="player_controls_lock">Låser afspilleren</string>
|
||||||
|
<string name="player_controls_skip_back">Hop tilbage</string>
|
||||||
|
<string name="player_controls_exit">Stop afspiller</string>
|
||||||
|
<string name="player_controls_skip_intro">Spring over intro</string>
|
||||||
|
<string name="player_controls_fast_forward">Spol frem</string>
|
||||||
|
<string name="player_controls_skip_forward">Spring frem</string>
|
||||||
|
<string name="player_trickplay">Spole afspille</string>
|
||||||
|
<string name="none">Ingen</string>
|
||||||
|
<string name="player_controls_progress">Process indikator</string>
|
||||||
|
<string name="player_controls_rewind">Spol tilbage</string>
|
||||||
|
</resources>
|
|
@ -15,4 +15,5 @@
|
||||||
<string name="player_controls_progress">Idővonal</string>
|
<string name="player_controls_progress">Idővonal</string>
|
||||||
<string name="player_trickplay">Trükkös játék</string>
|
<string name="player_trickplay">Trükkös játék</string>
|
||||||
<string name="player_controls_picture_in_picture">Belépés kép a képben</string>
|
<string name="player_controls_picture_in_picture">Belépés kép a képben</string>
|
||||||
|
<string name="none">Nincs</string>
|
||||||
</resources>
|
</resources>
|
2
player/video/src/main/res/values-tr/strings.xml
Normal file
2
player/video/src/main/res/values-tr/strings.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources></resources>
|
|
@ -15,4 +15,5 @@
|
||||||
<string name="player_controls_progress">进度条</string>
|
<string name="player_controls_progress">进度条</string>
|
||||||
<string name="player_trickplay">特技播放</string>
|
<string name="player_trickplay">特技播放</string>
|
||||||
<string name="player_controls_picture_in_picture">进入画中画</string>
|
<string name="player_controls_picture_in_picture">进入画中画</string>
|
||||||
|
<string name="none">无</string>
|
||||||
</resources>
|
</resources>
|
|
@ -47,10 +47,14 @@ constructor(
|
||||||
val playerGesturesVB get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_VB, true)
|
val playerGesturesVB get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_VB, true)
|
||||||
val playerGesturesZoom get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_ZOOM, true)
|
val playerGesturesZoom get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_ZOOM, true)
|
||||||
val playerGesturesSeek get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_SEEK, true)
|
val playerGesturesSeek get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_SEEK, true)
|
||||||
|
val playerGesturesChapterSkip get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_CHAPTER_SKIP, true)
|
||||||
|
|
||||||
val playerBrightnessRemember get() =
|
val playerBrightnessRemember get() =
|
||||||
sharedPreferences.getBoolean(Constants.PREF_PLAYER_BRIGHTNESS_REMEMBER, false)
|
sharedPreferences.getBoolean(Constants.PREF_PLAYER_BRIGHTNESS_REMEMBER, false)
|
||||||
|
|
||||||
|
val playerStartMaximized get() =
|
||||||
|
sharedPreferences.getBoolean(Constants.PREF_PLAYER_START_MAXIMIZED, false)
|
||||||
|
|
||||||
var playerBrightness: Float
|
var playerBrightness: Float
|
||||||
get() = sharedPreferences.getFloat(
|
get() = sharedPreferences.getFloat(
|
||||||
Constants.PREF_PLAYER_BRIGHTNESS,
|
Constants.PREF_PLAYER_BRIGHTNESS,
|
||||||
|
@ -70,11 +74,12 @@ constructor(
|
||||||
DEFAULT_SEEK_FORWARD_INCREMENT_MS.toString(),
|
DEFAULT_SEEK_FORWARD_INCREMENT_MS.toString(),
|
||||||
)!!.toLongOrNull() ?: DEFAULT_SEEK_FORWARD_INCREMENT_MS
|
)!!.toLongOrNull() ?: DEFAULT_SEEK_FORWARD_INCREMENT_MS
|
||||||
val playerMpv get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_MPV, false)
|
val playerMpv get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_MPV, false)
|
||||||
val playerMpvHwdec get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_HWDEC, "mediacodec-copy")!!
|
val playerMpvHwdec get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_HWDEC, "mediacodec")!!
|
||||||
val playerMpvVo get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_VO, "gpu")!!
|
val playerMpvVo get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_VO, "gpu-next")!!
|
||||||
val playerMpvAo get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_AO, "audiotrack")!!
|
val playerMpvAo get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_AO, "audiotrack")!!
|
||||||
val playerIntroSkipper get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_INTRO_SKIPPER, true)
|
val playerIntroSkipper get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_INTRO_SKIPPER, true)
|
||||||
val playerTrickPlay get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_TRICK_PLAY, true)
|
val playerTrickPlay get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_TRICK_PLAY, true)
|
||||||
|
val showChapterMarkers get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_CHAPTER_MARKERS, true)
|
||||||
|
|
||||||
val playerPipGesture get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_PIP_GESTURE, false)
|
val playerPipGesture get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_PIP_GESTURE, false)
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,9 @@ object Constants {
|
||||||
const val PREF_PLAYER_GESTURES_VB = "pref_player_gestures_vb"
|
const val PREF_PLAYER_GESTURES_VB = "pref_player_gestures_vb"
|
||||||
const val PREF_PLAYER_GESTURES_ZOOM = "pref_player_gestures_zoom"
|
const val PREF_PLAYER_GESTURES_ZOOM = "pref_player_gestures_zoom"
|
||||||
const val PREF_PLAYER_GESTURES_SEEK = "pref_player_gestures_seek"
|
const val PREF_PLAYER_GESTURES_SEEK = "pref_player_gestures_seek"
|
||||||
|
const val PREF_PLAYER_GESTURES_CHAPTER_SKIP = "pref_player_gestures_chapter_skip"
|
||||||
const val PREF_PLAYER_BRIGHTNESS_REMEMBER = "pref_player_brightness_remember"
|
const val PREF_PLAYER_BRIGHTNESS_REMEMBER = "pref_player_brightness_remember"
|
||||||
|
const val PREF_PLAYER_START_MAXIMIZED = "pref_player_start_maximized"
|
||||||
const val PREF_PLAYER_BRIGHTNESS = "pref_player_brightness"
|
const val PREF_PLAYER_BRIGHTNESS = "pref_player_brightness"
|
||||||
const val PREF_PLAYER_SEEK_BACK_INC = "pref_player_seek_back_inc"
|
const val PREF_PLAYER_SEEK_BACK_INC = "pref_player_seek_back_inc"
|
||||||
const val PREF_PLAYER_SEEK_FORWARD_INC = "pref_player_seek_forward_inc"
|
const val PREF_PLAYER_SEEK_FORWARD_INC = "pref_player_seek_forward_inc"
|
||||||
|
@ -26,6 +28,7 @@ object Constants {
|
||||||
const val PREF_PLAYER_MPV_AO = "pref_player_mpv_ao"
|
const val PREF_PLAYER_MPV_AO = "pref_player_mpv_ao"
|
||||||
const val PREF_PLAYER_INTRO_SKIPPER = "pref_player_intro_skipper"
|
const val PREF_PLAYER_INTRO_SKIPPER = "pref_player_intro_skipper"
|
||||||
const val PREF_PLAYER_TRICK_PLAY = "pref_player_trick_play"
|
const val PREF_PLAYER_TRICK_PLAY = "pref_player_trick_play"
|
||||||
|
const val PREF_PLAYER_CHAPTER_MARKERS = "pref_player_chapter_markers"
|
||||||
const val PREF_PLAYER_PIP_GESTURE = "pref_player_picture_in_picture_gesture"
|
const val PREF_PLAYER_PIP_GESTURE = "pref_player_picture_in_picture_gesture"
|
||||||
const val PREF_AUDIO_LANGUAGE = "pref_audio_language"
|
const val PREF_AUDIO_LANGUAGE = "pref_audio_language"
|
||||||
const val PREF_SUBTITLE_LANGUAGE = "pref_subtitle_language"
|
const val PREF_SUBTITLE_LANGUAGE = "pref_subtitle_language"
|
||||||
|
|
Loading…
Reference in a new issue