From edb37f5d69718f697f11b1b1acf4de48f2520c98 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 26 Apr 2026 10:16:26 -0400 Subject: [PATCH] Add support for cinema mode (#1305) ## Description Adds support for cinema mode, which is enabled by default. When enabled and if starting playback from the beginning, the server is queried for any "intros", ie prerolls or trailers, to play first. These are shown in the queue and can be skipped if desired. The server has to provide the intros, so a plugin like https://github.com/jellyfin/jellyfin-plugin-intros or https://github.com/CherryFloors/jellyfin-plugin-cinemamode is required server-side. ### Related issues Closes #418 Closes #1180 ### Testing Emulator and server w/ https://github.com/CherryFloors/jellyfin-plugin-cinemamode ## Screenshots N/A ## AI or LLM usage None --- .../wholphin/data/model/Playlist.kt | 31 ++++- .../wholphin/preferences/AppPreference.kt | 13 ++ .../preferences/AppPreferencesSerializer.kt | 1 + .../wholphin/services/AppUpgradeHandler.kt | 7 + .../wholphin/services/PlaylistCreator.kt | 13 +- .../wholphin/ui/playback/PlayExternalPage.kt | 25 ++-- .../wholphin/ui/playback/PlaybackViewModel.kt | 122 +++++++++++++----- .../ui/playback/SubtitleSearchUtils.kt | 16 ++- .../ui/playback/overlay/QueueRowOverlay.kt | 4 +- app/src/main/proto/WholphinDataStore.proto | 1 + app/src/main/res/values/strings.xml | 1 + 11 files changed, 170 insertions(+), 64 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/Playlist.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/Playlist.kt index 698a5f34..3e3cbe18 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/Playlist.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/Playlist.kt @@ -12,7 +12,7 @@ import java.util.UUID * This is not the same thing as a Jellyfin server playlist */ class Playlist( - items: List, + items: List, startIndex: Int = 0, ) { val items = items.subList(startIndex, items.size) @@ -23,15 +23,15 @@ class Playlist( fun hasNext(): Boolean = (index + 1) < items.size - fun getPreviousAndReverse(): BaseItem = items[--index] + fun getPreviousAndReverse(): PlaylistItem = items[--index] - fun getAndAdvance(): BaseItem = items[++index] + fun getAndAdvance(): PlaylistItem = items[++index] - fun peek(): BaseItem? = items.getOrNull(index + 1) + fun peek(): PlaylistItem? = items.getOrNull(index + 1) - fun upcomingItems(): List = items.subList(index + 1, items.size) + fun upcomingItems(): List = items.subList(index + 1, items.size) - fun advanceTo(id: UUID): BaseItem? { + fun advanceTo(id: UUID): PlaylistItem? { while (hasNext()) { val potential = getAndAdvance() if (potential.id == id) { @@ -52,3 +52,22 @@ data class PlaylistInfo( val count: Int, val mediaType: MediaType, ) + +sealed interface PlaylistItem { + val id: UUID + val item: BaseItem + + data class Media( + override val item: BaseItem, + ) : PlaylistItem { + override val id: UUID + get() = item.id + } + + data class Intro( + override val item: BaseItem, + ) : PlaylistItem { + override val id: UUID + get() = item.id + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt index 3cac262a..ecaa0fac 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt @@ -455,6 +455,18 @@ sealed interface AppPreference { summaryOff = R.string.disabled, ) + val CinemaMode = + AppSwitchPreference( + title = R.string.cinema_mode, + defaultValue = true, + getter = { it.playbackPreferences.cinemaMode }, + setter = { prefs, value -> + prefs.updatePlaybackPreferences { cinemaMode = value } + }, + summaryOn = R.string.enabled, + summaryOff = R.string.disabled, + ) + val RememberSelectedTab = AppSwitchPreference( title = R.string.remember_selected_tab, @@ -1166,6 +1178,7 @@ val advancedPreferences = preferences = listOf( AppPreference.OneClickPause, + AppPreference.CinemaMode, AppPreference.GlobalContentScale, AppPreference.SkipSegments, AppPreference.MaxBitrate, diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt index 9b46de83..278c148c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt @@ -54,6 +54,7 @@ class AppPreferencesSerializer refreshRateSwitching = AppPreference.RefreshRateSwitching.defaultValue resolutionSwitching = AppPreference.ResolutionSwitching.defaultValue + cinemaMode = AppPreference.CinemaMode.defaultValue overrides = PlaybackOverrides diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt index 8899fe8e..778f300c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt @@ -19,6 +19,7 @@ import com.github.damontecres.wholphin.preferences.updateMpvOptions import com.github.damontecres.wholphin.preferences.updateMusicPreferences import com.github.damontecres.wholphin.preferences.updatePhotoPreferences import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides +import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences import com.github.damontecres.wholphin.preferences.updateScreensaverPreferences import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel @@ -327,5 +328,11 @@ class AppUpgradeHandler } } } + + if (previous.isEqualOrBefore(Version.fromString("0.6.2-18-g0"))) { + appPreferences.updateData { + it.updatePlaybackPreferences { cinemaMode = AppPreference.CinemaMode.defaultValue } + } + } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt index fbbc9b70..7e115212 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PlaylistCreator.kt @@ -7,12 +7,14 @@ import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.GetItemsFilter import com.github.damontecres.wholphin.data.model.Playlist import com.github.damontecres.wholphin.data.model.PlaylistInfo +import com.github.damontecres.wholphin.data.model.PlaylistItem import com.github.damontecres.wholphin.ui.DefaultItemFields import com.github.damontecres.wholphin.ui.components.baseItemKinds import com.github.damontecres.wholphin.ui.data.SortAndDirection import com.github.damontecres.wholphin.ui.gt import com.github.damontecres.wholphin.ui.indexOfFirstOrNull import com.github.damontecres.wholphin.ui.playback.playable +import com.github.damontecres.wholphin.ui.toBaseItems import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler @@ -22,6 +24,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.playlistsApi +import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.api.client.extensions.videosApi import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemKind @@ -247,14 +250,14 @@ class PlaylistCreator -> { val list = buildList { - add(BaseItem(item, false)) + add(PlaylistItem.Media(BaseItem(item, false))) if (item.partCount.gt(1)) { api.videosApi .getAdditionalPart(item.id) .content.items .map { - BaseItem(it, false) + PlaylistItem.Media(BaseItem(it, false)) }.let(::addAll) } } @@ -274,14 +277,14 @@ class PlaylistCreator } } - private suspend fun List.convertAndAddParts(useSeriesForPrimary: Boolean = false): List = + private suspend fun List.convertAndAddParts(useSeriesForPrimary: Boolean = false): List = buildList { this@convertAndAddParts.forEach { ep -> - add(BaseItem(ep, useSeriesForPrimary)) + add(PlaylistItem.Media(BaseItem(ep, useSeriesForPrimary))) if (ep.partCount.gt(1)) { val parts = api.videosApi.getAdditionalPart(ep.id).content.items.map { part -> - BaseItem(part, useSeriesForPrimary) + PlaylistItem.Media(BaseItem(part, useSeriesForPrimary)) } addAll(parts) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlayExternalPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlayExternalPage.kt index e3592372..915fc5a7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlayExternalPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlayExternalPage.kt @@ -24,6 +24,7 @@ import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.data.ItemPlaybackDao import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.PlaylistItem import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.PlaylistCreationResult @@ -105,9 +106,9 @@ class PlayExternalViewModel } try { val queriedItem = api.userLibraryApi.getItem(itemId).content - val base = + val playlistItem = if (queriedItem.type.playable) { - queriedItem + PlaylistItem.Media(BaseItem(queriedItem)) } else if (destination is Destination.PlaybackList) { val playlistResult = playlistCreator.createFrom( @@ -134,18 +135,15 @@ class PlayExternalViewModel navigationManager.goBack() return@launchDefault } - r.playlist.items - .first() - .data + r.playlist.items.first() } } } else { throw IllegalArgumentException("Item is not playable and not PlaybackList: ${queriedItem.type}") } - val item = BaseItem(base, false) val playbackConfig = serverRepository.currentUser.value?.let { user -> - itemPlaybackDao.getItem(user, base.id)?.let { + itemPlaybackDao.getItem(user, playlistItem.id)?.let { Timber.v("Fetched itemPlayback from DB: %s", it) if (it.sourceId != null) { it @@ -154,20 +152,25 @@ class PlayExternalViewModel } } } - val mediaSource = streamChoiceService.chooseSource(base, playbackConfig) - val plc = streamChoiceService.getPlaybackLanguageChoice(base) + val item = + when (playlistItem) { + is PlaylistItem.Intro -> playlistItem.item + is PlaylistItem.Media -> playlistItem.item + } + val mediaSource = streamChoiceService.chooseSource(item.data, playbackConfig) + val plc = streamChoiceService.getPlaybackLanguageChoice(item.data) if (mediaSource == null) { Timber.w("Media source is null") return@launchDefault } - savedStateHandle[KEY_ID] = base.id + savedStateHandle[KEY_ID] = playlistItem.id savedStateHandle[KEY_MEDIA_ID] = mediaSource.id val subtitleIndex = streamChoiceService .chooseSubtitleStream( source = mediaSource, audioStream = null, - seriesId = base.seriesId, + seriesId = item.data.seriesId, itemPlayback = playbackConfig, plc = plc, prefs = prefs, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index 551fdd64..680750c3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -35,6 +35,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.Chapter import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.Playlist +import com.github.damontecres.wholphin.data.model.PlaylistItem import com.github.damontecres.wholphin.data.model.TrackIndex import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.PlayerBackend @@ -185,7 +186,7 @@ class PlaybackViewModel private lateinit var preferences: UserPreferences internal lateinit var itemId: UUID - internal lateinit var item: BaseItem + internal lateinit var currentItem: PlaylistItem internal var forceTranscoding: Boolean = false private var activityListener: TrackActivityPlaybackListener? = null private val jobs = mutableListOf() @@ -319,9 +320,9 @@ class PlaybackViewModel } this.itemId = itemId val queriedItem = api.userLibraryApi.getItem(itemId).content - val base = + val playlistItem = if (queriedItem.type.playable) { - queriedItem + PlaylistItem.Media(BaseItem(queriedItem, false)) } else if (destination is Destination.PlaybackList) { val playlistResult = playlistCreator.createFrom( @@ -349,9 +350,7 @@ class PlaybackViewModel this@PlaybackViewModel.playlist.value = r.playlist } } - r.playlist.items - .first() - .data + r.playlist.items.first() } } } else { @@ -359,10 +358,39 @@ class PlaybackViewModel } viewModelScope.launch(ExceptionHandler()) { controllerViewState.observe() } - val item = BaseItem.from(base, api) + + val intros = + // If not resuming playback & cinema mode is enabled, get potential intros + if (positionMs == 0L && preferences.appPreferences.playbackPreferences.cinemaMode) { + api.userLibraryApi + .getIntros( + itemId = playlistItem.id, + userId = serverRepository.currentUser.value?.id, + ).content.items + .map { + PlaylistItem.Intro(BaseItem(it)) + } + } else { + emptyList() + } + val firstItem = + if (intros.isNotEmpty()) { + Timber.v("Got %s intros", intros.size) + val currentPlaylist = + this@PlaybackViewModel + .playlist.value + ?.items + .orEmpty() + val newPlaylist = Playlist(intros + currentPlaylist) + this@PlaybackViewModel.playlist.setValueOnMain(newPlaylist) + intros.first() + } else { + playlistItem + } + val played = play( - item, + firstItem, positionMs, itemPlayback, forceTranscoding, @@ -374,9 +402,13 @@ class PlaybackViewModel if (!isPlaylist && preferences.appPreferences.playbackPreferences.showNextUpWhen != ShowNextUpWhen.NEXT_UP_NEVER) { val result = playlistCreator.createFrom(queriedItem) if (result is PlaylistCreationResult.Success && result.playlist.items.isNotEmpty()) { - withContext(Dispatchers.Main) { - this@PlaybackViewModel.playlist.value = result.playlist - } + val currentPlaylist = + this@PlaybackViewModel + .playlist.value + ?.items + .orEmpty() + val newPlaylist = Playlist(currentPlaylist + result.playlist.items) + this@PlaybackViewModel.playlist.setValueOnMain(newPlaylist) } } } @@ -384,18 +416,24 @@ class PlaybackViewModel /** * Play an item * - * @param item the item to play + * @param currentItem the item to play * @param positionMs the starting playback position in milliseconds * @param itemPlayback the parameters for playback such chosen subtitle or audio streams * @param forceTranscoding whether the user has requested to force playback via transcoding */ private suspend fun play( - item: BaseItem, + playlistItem: PlaylistItem, positionMs: Long, itemPlayback: ItemPlayback? = null, forceTranscoding: Boolean = this.forceTranscoding, ): Boolean = withContext(Dispatchers.IO) { + val item = + when (playlistItem) { + is PlaylistItem.Intro -> playlistItem.item + is PlaylistItem.Media -> playlistItem.item + } + Timber.i("Playing ${item.id}") // New item, so we can clear the media segment tracker & subtitle cues @@ -415,7 +453,7 @@ class PlaybackViewModel ) return@withContext false } - this@PlaybackViewModel.item = item + this@PlaybackViewModel.currentItem = playlistItem this@PlaybackViewModel.itemId = item.id val isLiveTv = item.type == BaseItemKind.TV_CHANNEL @@ -875,7 +913,7 @@ class PlaybackViewModel Timber.d("Changing audio track to %s", index) val itemPlayback = itemPlaybackRepository.saveTrackSelection( - item = item, + item = currentItem.item, itemPlayback = currentItemPlayback.value!!, trackIndex = index, type = MediaStreamType.AUDIO, @@ -889,7 +927,7 @@ class PlaybackViewModel streamChoiceService.resolveSubtitleIndex( source = source, audioStreamIndex = index, - seriesId = item.data.seriesId, + seriesId = currentItem.item.data.seriesId, subtitleIndex = itemPlayback.subtitleIndex, prefs = preferences, ) @@ -898,7 +936,7 @@ class PlaybackViewModel } changeStreams( - item, + currentItem.item, itemPlayback, index, resolvedSubtitleIndex, @@ -913,7 +951,7 @@ class PlaybackViewModel Timber.d("Changing subtitle track to %s", index) val itemPlayback = itemPlaybackRepository.saveTrackSelection( - item = item, + item = currentItem.item, itemPlayback = currentItemPlayback.value!!, trackIndex = index, type = MediaStreamType.SUBTITLE, @@ -927,7 +965,7 @@ class PlaybackViewModel streamChoiceService.resolveSubtitleIndex( source = source, audioStreamIndex = itemPlayback.audioIndex, - seriesId = item.data.seriesId, + seriesId = currentItem.item.data.seriesId, subtitleIndex = index, prefs = preferences, ) @@ -936,7 +974,7 @@ class PlaybackViewModel } changeStreams( - item, + currentItem.item, itemPlayback, itemPlayback.audioIndex, resolvedIndex, @@ -971,7 +1009,7 @@ class PlaybackViewModel mediaSourceId: UUID? = currentItemPlayback.value?.sourceId, ): String? = trickPlayInfo?.let { - val itemId = item.id + val itemId = currentItem.id return api.trickplayApi.getTrickplayTileImageUrl( itemId, trickPlayInfo.width, @@ -982,12 +1020,28 @@ class PlaybackViewModel override fun onPlaybackStateChanged(playbackState: Int) { if (playbackState == Player.STATE_ENDED) { - viewModelScope.launchIO { - val nextItem = playlist.value?.peek() - Timber.v("Setting next up to ${nextItem?.id}") - withContext(Dispatchers.Main) { - nextUp.value = nextItem - if (nextItem == null) { + Timber.v("Playback state is STATE_ENDED") + viewModelScope.launchDefault { + when (val nextItem = playlist.value?.peek()) { + is PlaylistItem.Intro -> { + Timber.v("Next item is intro, so playing immediately") + playNextUp() + } + + is PlaylistItem.Media -> { + if (currentItem is PlaylistItem.Intro) { + Timber.v("Current item is intro, so playing next up immediately") + playNextUp() + } else { + Timber.v("Setting next up to ${nextItem.id}") + withContext(Dispatchers.Main) { + nextUp.value = nextItem.item + } + } + } + + null -> { + Timber.v("No next up") navigationManager.goBack() } } @@ -1053,9 +1107,11 @@ class PlaybackViewModel outroShownSegments.add(currentSegment.id) ) { val nextItem = playlist.peek() - Timber.v("Setting next up during outro to ${nextItem?.id}") - withContext(Dispatchers.Main) { - nextUp.value = nextItem + if (nextItem is PlaylistItem.Media) { + Timber.v("Setting next up during outro to ${nextItem?.id}") + withContext(Dispatchers.Main) { + nextUp.value = nextItem.item + } } } else { val behavior = @@ -1180,7 +1236,7 @@ class PlaybackViewModel fun playNextUp() { playlist.value?.let { if (it.hasNext()) { - viewModelScope.launchIO { + viewModelScope.launchDefault { cancelUpNextEpisode() val item = it.getAndAdvance() val played = play(item, 0) @@ -1195,7 +1251,7 @@ class PlaybackViewModel fun playPrevious() { playlist.value?.let { if (it.hasPrevious()) { - viewModelScope.launchIO { + viewModelScope.launchDefault { cancelUpNextEpisode() val item = it.getPreviousAndReverse() val played = play(item, 0) @@ -1256,7 +1312,7 @@ class PlaybackViewModel PlayMethod.DIRECT_STREAM, PlayMethod.DIRECT_PLAY -> { Timber.w("Playback error during ${it.playMethod}, falling back to transcoding") changeStreams( - item, + currentItem.item, currentItemPlayback.value!!, currentItemPlayback.value?.audioIndex, currentItemPlayback.value?.subtitleIndex, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt index 27188f57..b2e82eae 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/SubtitleSearchUtils.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.text.intl.Locale import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.PlaylistItem import com.github.damontecres.wholphin.data.model.TrackIndex import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.onMain @@ -106,12 +107,13 @@ fun PlaybackViewModel.downloadAndSwitchSubtitles( while (maxAttempts > 0 && subtitleCount == newCount) { maxAttempts-- delay(1500) - item = - BaseItem.from( - api.userLibraryApi.getItem(itemId = it.itemId).content, - api, - ) - mediaSource = streamChoiceService.chooseSource(item.data, it) + val base = BaseItem(api.userLibraryApi.getItem(itemId = it.itemId).content) + currentItem = + when (currentItem) { + is PlaylistItem.Intro -> PlaylistItem.Intro(base) + is PlaylistItem.Media -> PlaylistItem.Media(base) + } + mediaSource = streamChoiceService.chooseSource(currentItem.item.data, it) if (mediaSource == null) { // This shouldn't happen, but just in case showToast( @@ -158,7 +160,7 @@ fun PlaybackViewModel.downloadAndSwitchSubtitles( ) } this@downloadAndSwitchSubtitles.changeStreams( - item, + currentItem.item, currentItemPlayback.value!!, audioIndex, newStream.index, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/QueueRowOverlay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/QueueRowOverlay.kt index 47ee1c85..a44d1fc7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/QueueRowOverlay.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/QueueRowOverlay.kt @@ -79,9 +79,9 @@ fun QueueRowOverlay( if (isFocused) controllerViewState.pulseControls() } SeasonCard( - item = item, + item = item.item, onClick = { - onClickPlaylist.invoke(item) + onClickPlaylist.invoke(item.item) controllerViewState.hideControls() }, onLongClick = {}, diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto index 7b7b543b..69df8689 100644 --- a/app/src/main/proto/WholphinDataStore.proto +++ b/app/src/main/proto/WholphinDataStore.proto @@ -86,6 +86,7 @@ message PlaybackPreferences { bool refresh_rate_switching = 22; bool resolution_switching = 23; string external_player = 24; + bool cinema_mode = 25; } message HomePagePreferences{ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa9364cb..63a83aae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -767,6 +767,7 @@ Direct play with ExoPlayer built-in Burn in/transcode on server SSA/ASS subtitle playback + Cinema mode Show %1$s more Skip behavior For intros, credits, etc