0.3.31 - Studio logo to station indent
This commit is contained in:
@@ -38,7 +38,7 @@ val membyGatewayUrl: String = (project.findProperty("memby.gatewayUrl") as Strin
|
||||
val membyDiagnosticLogLevel: String = (project.findProperty("memby.diagnosticLogLevel") as String?)
|
||||
?.trim()?.uppercase()?.takeIf { it in setOf("INFO", "DEBUG", "TRACE") } ?: "INFO"
|
||||
|
||||
val defaultVersionName = "0.3.29"
|
||||
val defaultVersionName = "0.3.31"
|
||||
val membyVersionName: String =
|
||||
(project.findProperty("memby.versionName") as String?)
|
||||
?.trim()
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.ponzischeme89.memby.data.model.GatewayFeatures
|
||||
import com.ponzischeme89.memby.data.model.HomeRow
|
||||
import com.ponzischeme89.memby.data.model.PlaybackReport
|
||||
import com.ponzischeme89.memby.data.model.RadarrMovieDetail
|
||||
import com.ponzischeme89.memby.data.model.StreamingService
|
||||
import com.ponzischeme89.memby.data.model.PlaybackInfoRequest
|
||||
import com.ponzischeme89.memby.data.model.MediaSourceInfo
|
||||
import com.ponzischeme89.memby.data.model.h264TranscodeFallback
|
||||
@@ -172,6 +173,14 @@ data class Playable(
|
||||
* to lose the credits pane with it.
|
||||
*/
|
||||
val endCreditsAvailable: Boolean = false,
|
||||
/**
|
||||
* Which streaming service, if any, this title is licensed through, by Emby's own
|
||||
* Studios field — see [BaseItem.streamingService]. Drives the service mark on the
|
||||
* station ident, in the corner opposite the title's own logo. Never asked of the
|
||||
* backend: it rides whatever the launcher's card already knew, so it is missing rather
|
||||
* than wrong wherever that card's fields did not include Studios.
|
||||
*/
|
||||
val streamingService: StreamingService? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -237,6 +246,13 @@ data class PlaybackRequest(
|
||||
val overview: String? = null,
|
||||
val episodeCode: String? = null,
|
||||
val runtimeMs: Long = 0L,
|
||||
/**
|
||||
* The streaming service the launcher's own copy of this item already named among its
|
||||
* Studios, if any. Carried through rather than re-asked, the [logoUrl] precedent:
|
||||
* nobody but the card the viewer pressed knows this at launch time, and no server call
|
||||
* sits on the critical path to learn it. See [Playable.streamingService].
|
||||
*/
|
||||
val streamingService: StreamingService? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -2214,28 +2230,35 @@ class EmbyRepository internal constructor(
|
||||
overview = item.overview,
|
||||
episodeCode = episodeCode(item),
|
||||
runtimeMs = item.runTimeTicks?.div(10_000L)?.coerceAtLeast(0L) ?: 0L,
|
||||
streamingService = item.streamingService,
|
||||
)
|
||||
|
||||
/**
|
||||
* Builds the request used by a real Play action.
|
||||
*
|
||||
* Episode rows are intentionally lightweight and some entry points can therefore omit
|
||||
* the parent-logo identity. Resolve that identity from the associated series before the
|
||||
* player opens, so the immediate and server-resolved launch paths receive the same title
|
||||
* artwork regardless of which row supplied the episode.
|
||||
* the parent-logo identity, or the Studios field [BaseItem.streamingService] reads.
|
||||
* Resolve both from the associated series before the player opens, so the immediate
|
||||
* and server-resolved launch paths receive the same title artwork and service badge
|
||||
* regardless of which row supplied the episode.
|
||||
*/
|
||||
suspend fun playbackRequestForLaunch(item: BaseItem): PlaybackRequest {
|
||||
val request = playbackRequest(item)
|
||||
if (!item.isEpisode || request.logoUrl != null) return request
|
||||
val needsLogo = item.isEpisode && request.logoUrl == null
|
||||
val needsStreamingService = item.isEpisode && request.streamingService == null
|
||||
if (!needsLogo && !needsStreamingService) return request
|
||||
val seriesId = item.seriesId?.takeIf(String::isNotBlank) ?: return request
|
||||
val seriesLogo = try {
|
||||
getItemDetails(seriesId)?.let(::logoUrl)
|
||||
val series = try {
|
||||
getItemDetails(seriesId)
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
return request.copy(logoUrl = seriesLogo)
|
||||
return request.copy(
|
||||
logoUrl = if (needsLogo) series?.let(::logoUrl) else request.logoUrl,
|
||||
streamingService = if (needsStreamingService) series?.streamingService else request.streamingService,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2533,6 +2556,7 @@ class EmbyRepository internal constructor(
|
||||
trickplayAvailable = playback.trickplayAvailable,
|
||||
skipIntroAvailable = playback.skipIntroAvailable,
|
||||
endCreditsAvailable = playback.endCreditsAvailable,
|
||||
streamingService = item.streamingService,
|
||||
)
|
||||
}
|
||||
if (item.isSeries) {
|
||||
@@ -2568,6 +2592,7 @@ class EmbyRepository internal constructor(
|
||||
overview = episode.overview,
|
||||
episodeCode = episodeCode(episode),
|
||||
runtimeMs = episode.runTimeTicks?.div(10_000L)?.coerceAtLeast(0L) ?: 0L,
|
||||
streamingService = item.streamingService,
|
||||
)
|
||||
}
|
||||
val discovery = directPlayback(
|
||||
@@ -2594,6 +2619,7 @@ class EmbyRepository internal constructor(
|
||||
overview = item.overview,
|
||||
episodeCode = item.episodeCode,
|
||||
runtimeMs = item.runtimeMs,
|
||||
streamingService = item.streamingService,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -316,6 +316,45 @@ data class Studio(
|
||||
@SerialName("Name") val name: String = "",
|
||||
)
|
||||
|
||||
/**
|
||||
* The Emby `Studios` spellings that mean Apple TV+, shared between [BaseItem.streamingService]
|
||||
* and the Services shortcut strip's Apple TV+ category in `ui/genre/GenreCategories.kt` — one
|
||||
* vocabulary, so a title's playback badge and its place in that shelf can never disagree.
|
||||
*
|
||||
* Like the genre aliases in `GenreCategories.kt`, this is two vocabularies rather than one,
|
||||
* and for the same reason: TMDb records a *network* for television ("Apple TV+") but a
|
||||
* *production company* for film, and it does not use one name for that — "Apple Studios"
|
||||
* credits an Apple-produced film outright, "Apple Original Films" credits one distributed
|
||||
* by Apple, and both show up on real libraries. Missing either meant the shelf found every
|
||||
* Apple TV+ *series* and almost none of its films — Emby's `Studios` filter is an exact
|
||||
* match, so a spelling not listed here is a title that cannot be found.
|
||||
*/
|
||||
val AppleTvPlusStudioNames = listOf(
|
||||
"Apple TV+",
|
||||
"Apple TV Plus",
|
||||
"AppleTV+",
|
||||
"Apple Studios",
|
||||
"Apple Original Films",
|
||||
)
|
||||
|
||||
/**
|
||||
* The Emby `Studios` spellings that mean Netflix, the [AppleTvPlusStudioNames] precedent —
|
||||
* shared between [BaseItem.streamingService] and the Services strip's Netflix category.
|
||||
*/
|
||||
val NetflixStudioNames = listOf("Netflix")
|
||||
|
||||
/**
|
||||
* A streaming service a title is licensed through, as far as Emby's `Studios` field says so.
|
||||
* Drives the station ident's service mark during playback (see [BaseItem.streamingService])
|
||||
* and is entirely a client-side judgement — never asked of the backend, so it is missing
|
||||
* rather than wrong wherever a title's Studios were not fetched.
|
||||
*/
|
||||
@Serializable
|
||||
enum class StreamingService {
|
||||
APPLE_TV,
|
||||
NETFLIX,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MediaStream(
|
||||
@SerialName("Index") val index: Int = -1,
|
||||
@@ -503,6 +542,20 @@ data class BaseItem(
|
||||
val isMovieSchedule: Boolean get() = membySource == "radarr"
|
||||
val isSchedule: Boolean get() = isTvSchedule || isMovieSchedule
|
||||
|
||||
/**
|
||||
* Which streaming service, if any, Emby's [studios] names for this title — the same
|
||||
* spellings the Services shortcut strip in `ui/genre` filters on. Drives the streaming
|
||||
* service mark on the playback station ident; a title Emby has not tagged this way
|
||||
* simply carries no badge rather than a guess. Apple TV+ is checked first only because
|
||||
* a title is never genuinely both — the order is not a priority rule.
|
||||
*/
|
||||
val streamingService: StreamingService?
|
||||
get() = when {
|
||||
studios.any { it.name in AppleTvPlusStudioNames } -> StreamingService.APPLE_TV
|
||||
studios.any { it.name in NetflixStudioNames } -> StreamingService.NETFLIX
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* A film Radarr is tracking that Emby has no copy of, which is the one card in the app
|
||||
* with a page of its own rather than an Emby one. The moment the library imports it the
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
@@ -304,6 +305,7 @@ fun GenreBrowseScreen(
|
||||
// Down out of the strip lands on the top genre, in whatever
|
||||
// order personalisation put it in — never a fixed one.
|
||||
downFocusRequester = railFocusRequesters[state.categories.firstOrNull()?.id],
|
||||
onEnterContent = ::enterGrid,
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -771,6 +773,14 @@ internal fun ServicesRail(
|
||||
selectedServiceId: String? = null,
|
||||
/** The [GenreRailItem] precedent: lets a screenshot render one icon as though focused. */
|
||||
focusForCapture: String? = null,
|
||||
/**
|
||||
* The [GenreRail] `LazyColumn` precedent: Right belongs to this strip as a whole, and
|
||||
* has to scroll the remembered card back into composition before anything can be
|
||||
* focused, which a `focusProperties` target could not do. Without this, Right on a
|
||||
* service icon fell through to default focus search, which landed on whichever genre
|
||||
* happened to sit below it rather than entering the grid.
|
||||
*/
|
||||
onEnterContent: () -> Boolean = { false },
|
||||
) {
|
||||
val ownedFocusRequesters = remember(services.map { it.id }) {
|
||||
services.associate { it.id to FocusRequester() }
|
||||
@@ -778,6 +788,18 @@ internal fun ServicesRail(
|
||||
val requesterFor: (String) -> FocusRequester = { id ->
|
||||
itemFocusRequesters[id] ?: ownedFocusRequesters.getValue(id)
|
||||
}
|
||||
// The strip is narrower than four icons at this diameter, and a fifth is only a matter
|
||||
// of time — the GenreRail precedent applies here too, only sideways. A plain Row does
|
||||
// not clip, so an icon past the rail's width used to draw itself over the content pane
|
||||
// rather than being reachable by scrolling to it.
|
||||
val stripState = rememberLazyListState()
|
||||
// Only on arrival: once the viewer is in the strip, LazyRow brings the focused icon
|
||||
// into view on its own as the D-pad travels, and a second scroll chasing the selection
|
||||
// would fight it.
|
||||
LaunchedEffect(Unit) {
|
||||
val index = services.indexOfFirst { it.id == selectedServiceId }
|
||||
if (index > 0) runCatching { stripState.scrollToItem(index) }
|
||||
}
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
"SERVICES",
|
||||
@@ -787,14 +809,19 @@ internal fun ServicesRail(
|
||||
letterSpacing = 1.6.sp,
|
||||
modifier = Modifier.padding(start = 22.dp, top = 22.dp, bottom = 14.dp),
|
||||
)
|
||||
Row(
|
||||
Modifier
|
||||
LazyRow(
|
||||
state = stripState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 14.dp)
|
||||
.focusGroup(),
|
||||
.focusGroup()
|
||||
.onKeyEvent { event ->
|
||||
if (event.type != KeyEventType.KeyDown) return@onKeyEvent false
|
||||
if (event.key == Key.DirectionRight) onEnterContent() else false
|
||||
},
|
||||
contentPadding = PaddingValues(start = 16.dp, end = 14.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
) {
|
||||
services.forEachIndexed { index, service ->
|
||||
rowItemsIndexed(services, key = { _, service -> service.id }) { index, service ->
|
||||
val requester = requesterFor(service.id)
|
||||
ServiceIconButton(
|
||||
service = service,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.ponzischeme89.memby.ui.genre
|
||||
|
||||
import com.ponzischeme89.memby.data.model.AppleTvPlusStudioNames
|
||||
import com.ponzischeme89.memby.data.model.BaseItem
|
||||
import com.ponzischeme89.memby.data.model.NetflixStudioNames
|
||||
import com.ponzischeme89.memby.data.model.UserItemData
|
||||
|
||||
/**
|
||||
@@ -185,14 +187,14 @@ private val coreServiceCategories = listOf(
|
||||
GenreCategory(
|
||||
"service-apple-tv-plus",
|
||||
"Apple TV+",
|
||||
listOf("Apple TV+", "Apple TV Plus", "AppleTV+"),
|
||||
AppleTvPlusStudioNames,
|
||||
GenreCategoryIcon.APPLE_TV,
|
||||
BrowseFilterKind.STUDIO,
|
||||
),
|
||||
GenreCategory(
|
||||
"service-netflix",
|
||||
"Netflix",
|
||||
listOf("Netflix"),
|
||||
NetflixStudioNames,
|
||||
GenreCategoryIcon.NETFLIX,
|
||||
BrowseFilterKind.STUDIO,
|
||||
),
|
||||
|
||||
@@ -96,6 +96,7 @@ import com.ponzischeme89.memby.data.model.GatewayPrerollEntry
|
||||
import com.ponzischeme89.memby.data.model.GatewayPrerollSchedule
|
||||
import com.ponzischeme89.memby.data.model.GatewaySeasonFinale
|
||||
import com.ponzischeme89.memby.data.model.GatewaySubtitleCandidate
|
||||
import com.ponzischeme89.memby.data.model.StreamingService
|
||||
import com.ponzischeme89.memby.data.model.TrailerPlaybackRequest
|
||||
import com.ponzischeme89.memby.data.playback.AudioPassthroughMode
|
||||
import com.ponzischeme89.memby.data.playback.AudioPassthroughPreference
|
||||
@@ -254,6 +255,10 @@ class PlayerActivity : ComponentActivity() {
|
||||
private var nowPlayingGroup: View? = null
|
||||
private var playbackIdentityView: View? = null
|
||||
private var playbackIdentityHideJob: Job? = null
|
||||
/** The streaming-service mark, top-end — raised and cleared alongside [playbackIdentityView]. */
|
||||
private var playbackServiceBadge: ImageView? = null
|
||||
/** Which service the title now playing is licensed through, if any — see [Playable.streamingService]. */
|
||||
private var streamingService: StreamingService? = null
|
||||
|
||||
/**
|
||||
* The ident is one-shot per programme, so its phase — never a boolean — is what decides
|
||||
@@ -658,6 +663,13 @@ class PlayerActivity : ComponentActivity() {
|
||||
?: intent.getBooleanExtra(EXTRA_SKIP_INTRO, false)
|
||||
endCreditsAvailable = savedInstanceState?.getBoolean(STATE_END_CREDITS)
|
||||
?: intent.getBooleanExtra(EXTRA_END_CREDITS, false)
|
||||
// Same rule and default: a missing extra must never draw a badge for a title Emby
|
||||
// never told this build was licensed through a service. On the request form this
|
||||
// is corrected by adoptPlayable once the server settles.
|
||||
streamingService = decodeStreamingService(
|
||||
savedInstanceState?.getString(STATE_STREAMING_SERVICE)
|
||||
?: intent.getStringExtra(EXTRA_STREAMING_SERVICE),
|
||||
)
|
||||
// Carried across the recreate a Magic press causes, so the button keeps its memory
|
||||
// of what it has already put in front of this viewer.
|
||||
savedInstanceState?.getStringArrayList(STATE_MAGIC_OFFERED)?.let {
|
||||
@@ -1335,6 +1347,7 @@ class PlayerActivity : ComponentActivity() {
|
||||
trickplayAvailable = playable.trickplayAvailable
|
||||
skipIntroAvailable = playable.skipIntroAvailable
|
||||
endCreditsAvailable = playable.endCreditsAvailable
|
||||
streamingService = playable.streamingService
|
||||
subtitleAutoSelectionAttempted = false
|
||||
initialResumePositionMs = playable.resumePositionMs.coerceAtLeast(0L)
|
||||
playable.runtimeMs.takeIf { it > 0L }?.let { prerollRuntimeMs = it }
|
||||
@@ -2318,6 +2331,7 @@ class PlayerActivity : ComponentActivity() {
|
||||
logoUrl: String?,
|
||||
) {
|
||||
playbackIdentityView = findViewById(R.id.player_playback_identity)
|
||||
playbackServiceBadge = findViewById(R.id.player_playback_service_badge)
|
||||
val logo = findViewById<ImageView>(R.id.player_playback_identity_logo)
|
||||
val fallback = findViewById<TextView>(R.id.player_playback_identity_title).apply {
|
||||
text = seriesName?.takeIf(String::isNotBlank) ?: title.ifBlank { "Now playing" }
|
||||
@@ -2366,9 +2380,51 @@ class PlayerActivity : ComponentActivity() {
|
||||
alpha = 0f
|
||||
visibility = View.GONE
|
||||
}
|
||||
playbackServiceBadge?.apply {
|
||||
animate().cancel()
|
||||
alpha = 0f
|
||||
visibility = View.GONE
|
||||
}
|
||||
applyIdentityRegion()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fades [playbackServiceBadge] in step with the ident it shares a window with — never
|
||||
* on its own timer, so it can never linger after the ident's turn is over or appear
|
||||
* ahead of it. Withheld when [streamingService] is null: most titles earn no badge at
|
||||
* all, and showing an empty corner is not the same as showing nothing.
|
||||
*/
|
||||
private fun fadeServiceBadge(visible: Boolean) {
|
||||
val badge = playbackServiceBadge ?: return
|
||||
badge.animate().cancel()
|
||||
val service = streamingService
|
||||
if (visible && service != null) {
|
||||
badge.setImageResource(streamingServiceBadgeDrawableRes(service))
|
||||
badge.contentDescription = getString(streamingServiceBadgeDescriptionRes(service))
|
||||
badge.alpha = 0f
|
||||
badge.visibility = View.VISIBLE
|
||||
badge.animate().alpha(1f).setDuration(PLAYBACK_IDENTITY_FADE_MS).start()
|
||||
} else {
|
||||
badge.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(PLAYBACK_IDENTITY_FADE_MS)
|
||||
.withEndAction { badge.visibility = View.GONE }
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
/** Which mark [fadeServiceBadge] draws for a given [StreamingService]. */
|
||||
private fun streamingServiceBadgeDrawableRes(service: StreamingService): Int = when (service) {
|
||||
StreamingService.APPLE_TV -> R.drawable.ic_playback_apple_tv
|
||||
StreamingService.NETFLIX -> R.drawable.ic_playback_netflix
|
||||
}
|
||||
|
||||
/** The content description that goes with [streamingServiceBadgeDrawableRes]. */
|
||||
private fun streamingServiceBadgeDescriptionRes(service: StreamingService): Int = when (service) {
|
||||
StreamingService.APPLE_TV -> R.string.player_apple_tv_badge
|
||||
StreamingService.NETFLIX -> R.string.player_netflix_badge
|
||||
}
|
||||
|
||||
/**
|
||||
* Raises the ident, once, if the corner is actually free.
|
||||
*
|
||||
@@ -2407,6 +2463,7 @@ class PlayerActivity : ComponentActivity() {
|
||||
.alpha(1f)
|
||||
.setDuration(PLAYBACK_IDENTITY_FADE_MS)
|
||||
.start()
|
||||
fadeServiceBadge(visible = true)
|
||||
MembyDiagnostics.info(
|
||||
"station_ident_shown",
|
||||
"playback" to playSessionId,
|
||||
@@ -2447,6 +2504,7 @@ class PlayerActivity : ComponentActivity() {
|
||||
.withEndAction { visibility = View.GONE }
|
||||
.start()
|
||||
}
|
||||
fadeServiceBadge(visible = false)
|
||||
applyIdentityRegion()
|
||||
}
|
||||
|
||||
@@ -5556,6 +5614,7 @@ class PlayerActivity : ComponentActivity() {
|
||||
outState.putBoolean(STATE_TRICKPLAY, trickplayAvailable)
|
||||
outState.putBoolean(STATE_SKIP_INTRO, skipIntroAvailable)
|
||||
outState.putBoolean(STATE_END_CREDITS, endCreditsAvailable)
|
||||
streamingService?.let { outState.putString(STATE_STREAMING_SERVICE, it.name) }
|
||||
outState.putString(STATE_TITLE, if (savingPreview) previewResumeTitle else playbackTitle)
|
||||
outState.putString(STATE_LOGO_URL, if (savingPreview) previewResumeLogoUrl else logoUrl)
|
||||
outState.putString(
|
||||
@@ -5906,6 +5965,7 @@ class PlayerActivity : ComponentActivity() {
|
||||
private const val EXTRA_TRICKPLAY = "extra_trickplay_available"
|
||||
private const val EXTRA_SKIP_INTRO = "extra_skip_intro_available"
|
||||
private const val EXTRA_END_CREDITS = "extra_end_credits_available"
|
||||
private const val EXTRA_STREAMING_SERVICE = "extra_streaming_service"
|
||||
private const val EXTRA_MEDIA_SOURCE_ID = "extra_media_source_id"
|
||||
private const val EXTRA_PLAY_SESSION_ID = "extra_play_session_id"
|
||||
private const val EXTRA_PLAY_METHOD = "extra_play_method"
|
||||
@@ -5933,6 +5993,7 @@ class PlayerActivity : ComponentActivity() {
|
||||
private const val STATE_TRICKPLAY = "state_trickplay"
|
||||
private const val STATE_SKIP_INTRO = "state_skip_intro"
|
||||
private const val STATE_END_CREDITS = "state_end_credits"
|
||||
private const val STATE_STREAMING_SERVICE = "state_streaming_service"
|
||||
private const val STATE_TITLE = "state_title"
|
||||
private const val STATE_LOGO_URL = "state_logo_url"
|
||||
private const val STATE_BACKDROP_URL = "state_backdrop_url"
|
||||
@@ -6043,6 +6104,7 @@ class PlayerActivity : ComponentActivity() {
|
||||
trickplayAvailable = playable.trickplayAvailable,
|
||||
skipIntroAvailable = playable.skipIntroAvailable,
|
||||
endCreditsAvailable = playable.endCreditsAvailable,
|
||||
streamingService = playable.streamingService,
|
||||
mediaSourceId = playable.mediaSourceId,
|
||||
playSessionId = playable.playSessionId,
|
||||
playMethod = playable.playMethod,
|
||||
@@ -6072,6 +6134,7 @@ class PlayerActivity : ComponentActivity() {
|
||||
trickplayAvailable: Boolean = false,
|
||||
skipIntroAvailable: Boolean = false,
|
||||
endCreditsAvailable: Boolean = false,
|
||||
streamingService: StreamingService? = null,
|
||||
mediaSourceId: String = "",
|
||||
playSessionId: String = "",
|
||||
playMethod: String = "DirectPlay",
|
||||
@@ -6100,6 +6163,7 @@ class PlayerActivity : ComponentActivity() {
|
||||
putExtra(EXTRA_TRICKPLAY, trickplayAvailable)
|
||||
putExtra(EXTRA_SKIP_INTRO, skipIntroAvailable)
|
||||
putExtra(EXTRA_END_CREDITS, endCreditsAvailable)
|
||||
streamingService?.let { putExtra(EXTRA_STREAMING_SERVICE, it.name) }
|
||||
putExtra(EXTRA_MEDIA_SOURCE_ID, mediaSourceId)
|
||||
putExtra(EXTRA_PLAY_SESSION_ID, playSessionId)
|
||||
putExtra(EXTRA_PLAY_METHOD, playMethod)
|
||||
@@ -6119,6 +6183,10 @@ class PlayerActivity : ComponentActivity() {
|
||||
runCatching { playerJson.decodeFromString<TrailerPlaybackRequest>(it) }.getOrNull()
|
||||
}?.takeIf { it.subjectId.isNotBlank() }
|
||||
|
||||
/** An unrecognised name is a build newer than this one's vocabulary, not a fault. */
|
||||
private fun decodeStreamingService(raw: String?): StreamingService? =
|
||||
raw?.let { runCatching { StreamingService.valueOf(it) }.getOrNull() }
|
||||
|
||||
private fun mediaItem(url: String, subtitles: List<PlayableSubtitle>): MediaItem {
|
||||
val configurations = subtitles.filter {
|
||||
it.deliveryMethod.equals("External", true) && it.url.isNotBlank() && it.mimeType.isNotBlank()
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 6.6 KiB |
@@ -22,6 +22,10 @@
|
||||
controller, so it can appear briefly without opening the transport controls. -->
|
||||
<include layout="@layout/player_playback_identity" />
|
||||
|
||||
<!-- The streaming-service mark, top-end — the ident's opposite corner and its same
|
||||
window. See player_playback_service_badge.xml. -->
|
||||
<include layout="@layout/player_playback_service_badge" />
|
||||
|
||||
<!-- Skipping. Declared before the timing cues so those sit above it: a lower third on
|
||||
the left and this centred chip do not overlap, and if they ever do, the cue that
|
||||
appears once matters more than one that appears on every press. -->
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- The streaming-service mark for the title now playing — Apple TV+ or Netflix today, by
|
||||
Emby's own Studios field (BaseItem.streamingService). It shares the station ident's
|
||||
window: raised and cleared together with player_playback_identity, in the opposite
|
||||
top-end corner, so it never outlives the ident's turn or appears once the transport or
|
||||
the pause hero has taken the identity over. See PlaybackIdentity.kt and
|
||||
setUpPlaybackIdentity / showPlaybackIdentity / dismissPlaybackIdentity in PlayerActivity.
|
||||
|
||||
The image itself (src, contentDescription) is set at runtime by fadeServiceBadge, which
|
||||
is what picks the right mark for whichever service the title belongs to. Width and
|
||||
height are both bounded rather than one fixed and the other free: Apple's mark is a wide
|
||||
wordmark and Netflix's is a tall glyph, and a box that only bounded height would let the
|
||||
tall one balloon wider than the corner has room for. -->
|
||||
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/player_playback_service_badge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|top"
|
||||
android:layout_marginTop="34dp"
|
||||
android:layout_marginEnd="48dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:alpha="0"
|
||||
android:contentDescription="@string/player_apple_tv_badge"
|
||||
android:focusable="false"
|
||||
android:maxWidth="120dp"
|
||||
android:maxHeight="44dp"
|
||||
android:scaleType="fitEnd"
|
||||
android:src="@drawable/ic_playback_apple_tv"
|
||||
android:visibility="gone" />
|
||||
@@ -1,5 +1,7 @@
|
||||
<resources>
|
||||
<string name="player_title_logo">Title logo</string>
|
||||
<string name="player_apple_tv_badge">Apple TV+ original</string>
|
||||
<string name="player_netflix_badge">Netflix original</string>
|
||||
<string name="playback_loading">Starting playback…</string>
|
||||
<string name="playback_loading_hint">Connecting directly to Emby</string>
|
||||
<string name="playback_reconnecting">Connection interrupted</string>
|
||||
|
||||
Reference in New Issue
Block a user