Search, favourites, person page and settings

This commit is contained in:
2026-10-20 12:00:01 +03:00
parent 757ef213f8
commit 4b8427310f
45 changed files with 6494 additions and 968 deletions

View File

@@ -2,7 +2,9 @@ package ru.stenfredd.movies.playback.core
import android.content.Context
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -12,6 +14,7 @@ import kotlinx.coroutines.flow.map
import org.json.JSONObject
import ru.stenfredd.movies.core.jellyfin.JellyfinSession
import ru.stenfredd.movies.playback.api.SubtitleStyle
import ru.stenfredd.movies.playback.api.VideoPathSwitchable
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
@@ -32,12 +35,22 @@ data class TrackPick(val language: String?, val name: String?, val codec: String
?.first
}
/**
* Playback defaults from the settings screen, for everyone on this TV (they depend on the TV and the network).
* [maxBitrate] null = the original file; [dolbyVision] = decode Dolby Vision files with the DV decoder.
*/
data class PlaybackDefaults(
val maxBitrate: Int? = null,
val videoPath: VideoPathSwitchable.Mode = VideoPathSwitchable.Mode.AUTO,
val dolbyVision: Boolean = false,
)
/** What the user picked last for a movie or a series. [subtitle] null with [subtitleOff] = subtitles off. */
data class RememberedTracks(val audio: TrackPick?, val subtitle: TrackPick?, val subtitleOff: Boolean)
/**
* Player settings kept on the device: the look of text subtitles (for everyone on this TV) and the tracks chosen
* per movie or series (per account: family members watch with different tracks).
* Player settings kept on the device: the look of text subtitles and the playback defaults (for everyone on this TV),
* and the tracks chosen per movie or series (per account: family members watch with different tracks).
*/
@Singleton
class PlaybackPreferences @Inject constructor(
@@ -52,6 +65,24 @@ class PlaybackPreferences @Inject constructor(
context.playbackStore.edit { it[KEY_SUBTITLE_STYLE] = "${style.size}|${style.color}|${style.background}" }
}
val defaults: Flow<PlaybackDefaults> = context.playbackStore.data.map { prefs ->
PlaybackDefaults(
maxBitrate = prefs[KEY_MAX_BITRATE]?.takeIf { it > 0 },
videoPath = prefs[KEY_VIDEO_PATH]?.let { runCatching { VideoPathSwitchable.Mode.valueOf(it) }.getOrNull() } ?: VideoPathSwitchable.Mode.AUTO,
dolbyVision = prefs[KEY_DOLBY_VISION] ?: false,
)
}
suspend fun defaults(): PlaybackDefaults = defaults.first()
suspend fun setDefaults(defaults: PlaybackDefaults) {
context.playbackStore.edit {
it[KEY_MAX_BITRATE] = defaults.maxBitrate ?: 0
it[KEY_VIDEO_PATH] = defaults.videoPath.name
it[KEY_DOLBY_VISION] = defaults.dolbyVision
}
}
/** [key]: the series of an episode, else the item itself. */
suspend fun tracks(key: UUID): RememberedTracks? {
val raw = context.playbackStore.data.first()[tracksKey(key) ?: return null] ?: return null
@@ -104,5 +135,8 @@ class PlaybackPreferences @Inject constructor(
private companion object {
val KEY_SUBTITLE_STYLE = stringPreferencesKey("subtitle_style")
val KEY_MAX_BITRATE = intPreferencesKey("max_bitrate")
val KEY_VIDEO_PATH = stringPreferencesKey("video_path")
val KEY_DOLBY_VISION = booleanPreferencesKey("dolby_vision")
}
}

View File

@@ -127,12 +127,16 @@ class PlaybackSession(
private var transcodeTried = false
private var nextVideoPath: VideoPathSwitchable.Mode? = null
private var nextDolbyVision: Boolean? = null
/** Set by [switchToAuto]: the next engine keeps its own default decoder choice instead of the old engine's. */
/** Set by [switchToAuto]: the next engine gets the default decoder choice instead of the old engine's. */
private var defaultDolbyVision = false
/** Settings screen defaults, read at [start]. */
private var defaults = PlaybackDefaults()
/** The user picked the engine: the stall watchdog only logs instead of switching away from their choice. */
private var manualEngine = false
private var lastSeekAt = 0L
private var maxBitrate: Int? = null
/** [setMaxBitrate] was called before [start] (quality kept from the previous episode): the default doesn't apply. */
private var maxBitrateSet = false
private var subtitleStyle = SubtitleStyle()
private var subtitleLift = 0f
@@ -160,6 +164,11 @@ class PlaybackSession(
item = session.call { libraryApi.getItem(itemId, user.userId).content }
source = item.mediaSources?.firstOrNull() ?: error("У элемента нет медиаисточников")
subtitleStyle = prefs.subtitleStyle()
defaults = prefs.defaults()
if (!maxBitrateSet) {
maxBitrate = defaults.maxBitrate
_state.update { it.copy(maxBitrate = maxBitrate) }
}
selection = streams ?: rememberedSelection() ?: StreamSelection(source.defaultAudioStreamIndex, source.defaultSubtitleStreamIndex)
val startMs = startFraction?.let { ((item.runTimeTicks ?: 0) / TICKS_PER_MS * it).toLong() } ?: startPositionMs
ranking = selector.rank(source, selection, device)
@@ -250,6 +259,7 @@ class PlaybackSession(
/** Quality: null = original (direct play where possible), else the server transcodes down to [bitrate]. */
fun setMaxBitrate(bitrate: Int?) {
maxBitrateSet = true
if (bitrate == maxBitrate) return
maxBitrate = bitrate
_state.update { it.copy(maxBitrate = bitrate) }
@@ -317,7 +327,7 @@ class PlaybackSession(
scope.launch {
tried.clear()
transcodeTried = false
nextVideoPath = VideoPathSwitchable.Mode.AUTO
nextVideoPath = defaults.videoPath
nextDolbyVision = null
defaultDolbyVision = true
manualEngine = false
@@ -423,9 +433,9 @@ class PlaybackSession(
.map { ExternalSubtitle(it.index, session.authorizedUrl(it.deliveryUrl!!), it.codec, it.language, it.displayTitle) }
val newEngine = provider.create(device)
nextVideoPath?.let { (newEngine as? VideoPathSwitchable)?.setVideoPathMode(it) }
(newEngine as? VideoPathSwitchable)?.setVideoPathMode(nextVideoPath ?: defaults.videoPath)
nextVideoPath = null
nextDolbyVision?.let { (newEngine as? DolbyVisionSwitchable)?.useDolbyVisionDecoder = it }
(newEngine as? DolbyVisionSwitchable)?.useDolbyVisionDecoder = nextDolbyVision ?: defaults.dolbyVision
(newEngine as? SubtitleStyleable)?.let {
it.setSubtitleStyle(subtitleStyle)
it.setSubtitleLift(subtitleLift)