diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index 72db24bc89..102f8b4c6c 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -7,6 +7,7 @@ import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.content.res.Configuration +import android.graphics.Color import android.graphics.Rect import android.media.AudioManager import android.os.Build @@ -29,6 +30,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.media3.common.C import androidx.media3.ui.DefaultTimeBar +import androidx.media3.ui.PlayerControlView import androidx.media3.ui.PlayerView import androidx.navigation.navArgs import dagger.hilt.android.AndroidEntryPoint @@ -144,6 +146,18 @@ class PlayerActivity : BasePlayerActivity() { it.currentTrickPlay = currentTrickPlay } + // Chapters + if (appPreferences.showChapterMarkers && currentChapters != null) { + currentChapters?.let { chapters -> + val playerControlView = findViewById(R.id.exo_controller) + val numOfChapters = chapters.size + playerControlView.setExtraAdGroupMarkers( + LongArray(numOfChapters) { index -> chapters[index].startPosition }, + BooleanArray(numOfChapters) { false }, + ) + } + } + // File Loaded if (fileLoaded) { audioButton.isEnabled = true @@ -239,9 +253,12 @@ class PlayerActivity : BasePlayerActivity() { pictureInPicture() } + // Set marker color + val timeBar = binding.playerView.findViewById(R.id.exo_progress) + timeBar.setAdMarkerColor(Color.WHITE) + if (appPreferences.playerTrickPlay) { val imagePreview = binding.playerView.findViewById(R.id.image_preview) - val timeBar = binding.playerView.findViewById(R.id.exo_progress) previewScrubListener = PreviewScrubListener( imagePreview, timeBar, diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/utils/PlayerGestureHelper.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/utils/PlayerGestureHelper.kt index 615a905ea1..c3a3951338 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/utils/PlayerGestureHelper.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/utils/PlayerGestureHelper.kt @@ -23,6 +23,7 @@ import dev.jdtech.jellyfin.AppPreferences import dev.jdtech.jellyfin.Constants import dev.jdtech.jellyfin.PlayerActivity import dev.jdtech.jellyfin.isControlsLocked +import dev.jdtech.jellyfin.models.PlayerChapter import dev.jdtech.jellyfin.mpv.MPVPlayer import timber.log.Timber import kotlin.math.abs @@ -74,7 +75,6 @@ class PlayerGestureHelper( return true } - @SuppressLint("SetTextI18n") override fun onLongPress(e: MotionEvent) { // Disables long press gesture if view is locked if (isControlsLocked) return @@ -82,13 +82,12 @@ class PlayerGestureHelper( // Stop long press gesture when more than 1 pointer if (currentNumberOfPointers > 1) return - 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 - } + // This is a temporary solution for chapter skipping. + // TODO: Remove this after implementing #636 + if (appPreferences.playerGesturesChapterSkip) { + handleChapterSkip(e) + } else { + enableSpeedIncrease() } } @@ -123,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() { val currentPosition = playerView.player?.currentPosition ?: 0 val fastForwardPosition = currentPosition + appPreferences.playerSeekForwardIncrement diff --git a/app/phone/src/main/res/layout/exo_player_view.xml b/app/phone/src/main/res/layout/exo_player_view.xml index 03b28bbd54..cf739e154b 100644 --- a/app/phone/src/main/res/layout/exo_player_view.xml +++ b/app/phone/src/main/res/layout/exo_player_view.xml @@ -1,7 +1,8 @@ + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/exo_controller"> - + android:layout_height="match_parent" + app:animation_enabled="false"/> \ No newline at end of file diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dummy/Episodes.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dummy/Episodes.kt index f02a8a312d..8775f2913d 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dummy/Episodes.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dummy/Episodes.kt @@ -55,6 +55,7 @@ val dummyEpisode = FindroidEpisode( seasonId = UUID.randomUUID(), communityRating = 9.2f, images = FindroidImages(), + chapters = null, ) val dummyEpisodes = listOf( diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dummy/Movies.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dummy/Movies.kt index 8ba5506aeb..f6598e3987 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dummy/Movies.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dummy/Movies.kt @@ -55,6 +55,7 @@ val dummyMovie = FindroidMovie( endDate = null, trailer = "https://www.youtube.com/watch?v=puKWa8hrvA8", images = FindroidImages(), + chapters = null, ) val dummyMovies = listOf( diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 1ce1def728..1a410c8f8f 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -103,8 +103,10 @@ Volume and brightness gestures Zoom gesture Seek gesture + Chapter gesture Swipe up and down on the right side of the screen to change the volume and on the left side to change the brightness Pinch to fill the screen with the video + Long press on Left / Right side to skip chapters (overrides 2x speed gesture) Swipe horizontally to seek forwards or backwards Remember brightness level Start maximized @@ -145,6 +147,8 @@ Requires ConfusedPolarBear\'s Intro Skipper plugin to be installed on the server Trick Play Requires nicknsy\'s Jellyscrub plugin to be installed on the server + Chapter markers + Display chapter markers on the timebar Addresses Add address Add server address diff --git a/core/src/main/res/xml/fragment_settings_player.xml b/core/src/main/res/xml/fragment_settings_player.xml index 495e03883b..d70eef5b81 100644 --- a/core/src/main/res/xml/fragment_settings_player.xml +++ b/core/src/main/res/xml/fragment_settings_player.xml @@ -59,6 +59,12 @@ app:key="pref_player_gestures_seek" app:summary="@string/player_gestures_seek_summary" app:title="@string/player_gestures_seek" /> + + + ?): String? { + return value?.let { Json.encodeToString(value) } + } + + @TypeConverter + fun fromStringToFindroidChapters(value: String?): List? { + return value?.let { Json.decodeFromString(value) } + } } diff --git a/data/src/main/java/dev/jdtech/jellyfin/database/ServerDatabase.kt b/data/src/main/java/dev/jdtech/jellyfin/database/ServerDatabase.kt index 1d84c00476..319311f5f2 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/database/ServerDatabase.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/database/ServerDatabase.kt @@ -19,9 +19,10 @@ import dev.jdtech.jellyfin.models.User @Database( entities = [Server::class, ServerAddress::class, User::class, FindroidMovieDto::class, FindroidShowDto::class, FindroidSeasonDto::class, FindroidEpisodeDto::class, FindroidSourceDto::class, FindroidMediaStreamDto::class, TrickPlayManifestDto::class, IntroDto::class, FindroidUserDataDto::class], - version = 3, + version = 4, autoMigrations = [ AutoMigration(from = 2, to = 3), + AutoMigration(from = 3, to = 4), ], ) @TypeConverters(Converters::class) diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidBoxSet.kt b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidBoxSet.kt index f612f67467..9b4446ac0d 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidBoxSet.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidBoxSet.kt @@ -18,6 +18,7 @@ data class FindroidBoxSet( override val playbackPositionTicks: Long = 0L, override val unplayedItemCount: Int? = null, override val images: FindroidImages, + override val chapters: List? = null, ) : FindroidItem fun BaseItemDto.toFindroidBoxSet( diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidChapter.kt b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidChapter.kt new file mode 100644 index 0000000000..b48b3bdd64 --- /dev/null +++ b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidChapter.kt @@ -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? { + return chapters?.map { chapter -> + FindroidChapter( + startPosition = chapter.startPositionTicks / 10000, + name = chapter.name, + ) + } +} diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidCollection.kt b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidCollection.kt index 72980fcbe4..6607011c2f 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidCollection.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidCollection.kt @@ -19,6 +19,7 @@ data class FindroidCollection( override val unplayedItemCount: Int? = null, val type: CollectionType, override val images: FindroidImages, + override val chapters: List? = null, ) : FindroidItem fun BaseItemDto.toFindroidCollection( diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidEpisode.kt b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidEpisode.kt index 93b62bcc91..c1fbee4ede 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidEpisode.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidEpisode.kt @@ -31,6 +31,7 @@ data class FindroidEpisode( override val unplayedItemCount: Int? = null, val missing: Boolean = false, override val images: FindroidImages, + override val chapters: List?, ) : FindroidItem, FindroidSources suspend fun BaseItemDto.toFindroidEpisode( @@ -65,6 +66,7 @@ suspend fun BaseItemDto.toFindroidEpisode( communityRating = communityRating, missing = locationType == LocationType.VIRTUAL, images = toFindroidImages(jellyfinRepository), + chapters = toFindroidChapters(), ) } catch (_: NullPointerException) { null @@ -94,5 +96,6 @@ fun FindroidEpisodeDto.toFindroidEpisode(database: ServerDatabaseDao, userId: UU seasonId = seasonId, communityRating = communityRating, images = FindroidImages(), + chapters = chapters, ) } diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidEpisodeDto.kt b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidEpisodeDto.kt index 57a48ccc58..accea2c477 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidEpisodeDto.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidEpisodeDto.kt @@ -43,6 +43,7 @@ data class FindroidEpisodeDto( val runtimeTicks: Long, val premiereDate: LocalDateTime?, val communityRating: Float?, + val chapters: List?, ) fun FindroidEpisode.toFindroidEpisodeDto(serverId: String? = null): FindroidEpisodeDto { @@ -60,5 +61,6 @@ fun FindroidEpisode.toFindroidEpisodeDto(serverId: String? = null): FindroidEpis runtimeTicks = runtimeTicks, premiereDate = premiereDate, communityRating = communityRating, + chapters = chapters, ) } diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidItem.kt b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidItem.kt index 850a1913f4..2cdabe0425 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidItem.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidItem.kt @@ -20,6 +20,7 @@ interface FindroidItem { val playbackPositionTicks: Long val unplayedItemCount: Int? val images: FindroidImages + val chapters: List? } suspend fun BaseItemDto.toFindroidItem( diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidMovie.kt b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidMovie.kt index 3936a53156..abeec9798a 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidMovie.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidMovie.kt @@ -31,6 +31,7 @@ data class FindroidMovie( val trailer: String?, override val unplayedItemCount: Int? = null, override val images: FindroidImages, + override val chapters: List?, ) : FindroidItem, FindroidSources suspend fun BaseItemDto.toFindroidMovie( @@ -64,6 +65,7 @@ suspend fun BaseItemDto.toFindroidMovie( endDate = endDate, trailer = remoteTrailers?.getOrNull(0)?.url, images = toFindroidImages(jellyfinRepository), + chapters = toFindroidChapters(), ) } @@ -91,5 +93,6 @@ fun FindroidMovieDto.toFindroidMovie(database: ServerDatabaseDao, userId: UUID): sources = database.getSources(id).map { it.toFindroidSource(database) }, trailer = null, images = FindroidImages(), + chapters = chapters, ) } diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidMovieDto.kt b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidMovieDto.kt index aaf3a58562..1667bff0a4 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidMovieDto.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidMovieDto.kt @@ -20,6 +20,7 @@ data class FindroidMovieDto( val status: String, val productionYear: Int?, val endDate: LocalDateTime?, + val chapters: List?, ) fun FindroidMovie.toFindroidMovieDto(serverId: String? = null): FindroidMovieDto { @@ -36,5 +37,6 @@ fun FindroidMovie.toFindroidMovieDto(serverId: String? = null): FindroidMovieDto status = status, productionYear = productionYear, endDate = endDate, + chapters = chapters, ) } diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidSeason.kt b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidSeason.kt index 5121f84e60..af1c98dd81 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidSeason.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidSeason.kt @@ -24,6 +24,7 @@ data class FindroidSeason( override val playbackPositionTicks: Long = 0L, override val unplayedItemCount: Int?, override val images: FindroidImages, + override val chapters: List? = null, ) : FindroidItem fun BaseItemDto.toFindroidSeason( diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidShow.kt b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidShow.kt index 7ed6c6ceb4..7cb6da34f2 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidShow.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidShow.kt @@ -31,6 +31,7 @@ data class FindroidShow( val endDate: DateTime?, val trailer: String?, override val images: FindroidImages, + override val chapters: List? = null, ) : FindroidItem fun BaseItemDto.toFindroidShow( diff --git a/player/core/src/main/java/dev/jdtech/jellyfin/models/PlayerChapter.kt b/player/core/src/main/java/dev/jdtech/jellyfin/models/PlayerChapter.kt new file mode 100644 index 0000000000..c05e7e84d6 --- /dev/null +++ b/player/core/src/main/java/dev/jdtech/jellyfin/models/PlayerChapter.kt @@ -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 diff --git a/player/core/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt b/player/core/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt index f011809add..bf4e1e4d34 100644 --- a/player/core/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt +++ b/player/core/src/main/java/dev/jdtech/jellyfin/models/PlayerItem.kt @@ -15,4 +15,5 @@ data class PlayerItem( val indexNumber: Int? = null, val indexNumberEnd: Int? = null, val externalSubtitles: List = emptyList(), + val chapters: List? = null, ) : Parcelable diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index f5734c75c1..01ffab3beb 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -19,6 +19,7 @@ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import dagger.hilt.android.lifecycle.HiltViewModel import dev.jdtech.jellyfin.AppPreferences import dev.jdtech.jellyfin.models.Intro +import dev.jdtech.jellyfin.models.PlayerChapter import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.mpv.MPVPlayer import dev.jdtech.jellyfin.player.video.R @@ -56,6 +57,7 @@ constructor( currentItemTitle = "", currentIntro = null, currentTrickPlay = null, + currentChapters = null, fileLoaded = false, ), ) @@ -72,12 +74,13 @@ constructor( val currentItemTitle: String, val currentIntro: Intro?, val currentTrickPlay: BifData?, + val currentChapters: List?, val fileLoaded: Boolean, ) private var items: Array = arrayOf() - val trackSelector = DefaultTrackSelector(application) + private val trackSelector = DefaultTrackSelector(application) var playWhenReady = true private var currentMediaItemIndex = savedStateHandle["mediaItemIndex"] ?: 0 private var playbackPosition: Long = savedStateHandle["position"] ?: 0 @@ -277,7 +280,7 @@ constructor( } else { item.name } - _uiState.update { it.copy(currentItemTitle = itemTitle, fileLoaded = false) } + _uiState.update { it.copy(currentItemTitle = itemTitle, currentChapters = item.chapters, fileLoaded = false) } jellyfinRepository.postPlaybackStart(item.itemId) @@ -367,6 +370,89 @@ constructor( } } + /** + * Get chapters of current item + * @return list of [PlayerChapter] + */ + private fun getChapters(): List? { + 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)) diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt index 23b312daf2..d01c8e0325 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt @@ -6,12 +6,14 @@ import androidx.lifecycle.viewModelScope import androidx.media3.common.MimeTypes import dagger.hilt.android.lifecycle.HiltViewModel import dev.jdtech.jellyfin.models.ExternalSubtitle +import dev.jdtech.jellyfin.models.FindroidChapter import dev.jdtech.jellyfin.models.FindroidEpisode import dev.jdtech.jellyfin.models.FindroidItem import dev.jdtech.jellyfin.models.FindroidMovie import dev.jdtech.jellyfin.models.FindroidSeason import dev.jdtech.jellyfin.models.FindroidShow import dev.jdtech.jellyfin.models.FindroidSourceType +import dev.jdtech.jellyfin.models.PlayerChapter import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.repository.JellyfinRepository import kotlinx.coroutines.channels.Channel @@ -113,7 +115,7 @@ class PlayerViewModel @Inject internal constructor( .getEpisodes( seriesId = item.seriesId, seasonId = item.seasonId, - fields = listOf(ItemFields.MEDIA_SOURCES), + fields = listOf(ItemFields.MEDIA_SOURCES, ItemFields.CHAPTERS), startItemId = item.id, 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, indexNumberEnd = if (this is FindroidEpisode) indexNumberEnd else null, externalSubtitles = externalSubtitles, + chapters = chapters.toPlayerChapters(), ) } + + private fun List?.toPlayerChapters(): List? { + return this?.map { chapter -> + PlayerChapter( + startPosition = chapter.startPosition, + name = chapter.name, + ) + } + } } sealed interface PlayerItemsEvent { diff --git a/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt b/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt index 132e3c5d54..a2da80858b 100644 --- a/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt +++ b/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt @@ -47,6 +47,7 @@ constructor( val playerGesturesVB get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_VB, true) val playerGesturesZoom get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_ZOOM, 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() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_BRIGHTNESS_REMEMBER, false) @@ -78,6 +79,7 @@ constructor( val playerMpvAo get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_AO, "audiotrack")!! val playerIntroSkipper get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_INTRO_SKIPPER, 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) diff --git a/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt b/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt index 89403118d3..2006ac3857 100644 --- a/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt +++ b/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt @@ -16,6 +16,7 @@ object Constants { 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_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_START_MAXIMIZED = "pref_player_start_maximized" const val PREF_PLAYER_BRIGHTNESS = "pref_player_brightness" @@ -27,6 +28,7 @@ object Constants { const val PREF_PLAYER_MPV_AO = "pref_player_mpv_ao" const val PREF_PLAYER_INTRO_SKIPPER = "pref_player_intro_skipper" 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_AUDIO_LANGUAGE = "pref_audio_language" const val PREF_SUBTITLE_LANGUAGE = "pref_subtitle_language"