This commit is contained in:
ponzischeme89
2026-08-25 23:33:48 +12:00
parent d0f3ed94de
commit 3c247a0afd
9 changed files with 79 additions and 7 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 KiB

+1 -1
View File
@@ -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.24"
val defaultVersionName = "0.3.25"
val membyVersionName: String =
(project.findProperty("memby.versionName") as String?)
?.trim()
+3 -3
View File
@@ -64,14 +64,14 @@
android:name="android.hardware.microphone"
android:required="false" />
<!-- The launcher icon is circular; the square mark remains an in-app asset. -->
<application
android:name=".MembyApp"
android:allowBackup="true"
android:banner="@drawable/app_banner"
<!-- The launcher icon is circular; the square mark remains an in-app asset. -->
android:icon="@drawable/glossy_m"
android:icon="@drawable/app_icon"
android:label="@string/app_name"
android:roundIcon="@drawable/glossy_m"
android:roundIcon="@drawable/app_icon"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.Memby">
@@ -620,7 +620,21 @@ private fun DetailHero(
// screensaver honoured it. The dark-logo fallback comes with it — a black title
// treatment on this near-black scrim is an invisible heading.
val repository = ServiceLocator.repository
val logoUrl = remember(item.id, item.imageTags, repository.showTitleLogo) {
// Keyed on the parent-logo identity as well as the item's own tags, because an episode
// carries no Logo of its own: its title treatment is the series', named by
// ParentLogoItemId/ParentLogoImageTag. Those two arrive on a route object that a
// thinner one — a re-focus behind the overlay, a lightweight recommendation card — can
// briefly replace, and a memo that could not see them resolved "no logo" once and kept
// that answer for the life of the page. It is the same key the launcher's hero and its
// metadata panel already use; the three must agree or the page contradicts the row it
// was opened from.
val logoUrl = remember(
item.id,
item.imageTags,
item.parentLogoItemId,
item.parentLogoImageTag,
repository.showTitleLogo,
) {
if (repository.showTitleLogo) repository.logoUrl(item) else null
}
val logo = logoUrl.takeIf { !useTextTitleForLogo(it) }
@@ -676,8 +676,14 @@ internal fun HomeScreen(
var timeout: TimeoutCancellationException? = null
repeat(2) {
try {
// The request, not the card. `playbackRequestForLaunch`
// above is what resolves an episode's title treatment from
// its series, and re-deriving the request from the item
// here threw that away — so a cold start (no resume
// position, which is every unwatched pick) opened the
// player with no logo while a resume opened with one.
return@run withTimeout(PLAYBACK_SOURCE_RESOLUTION_TIMEOUT) {
repo.resolvePlayableForLaunch(item)
repo.resolvePlayableForLaunch(request)
}
} catch (error: TimeoutCancellationException) {
timeout = error
@@ -722,7 +722,7 @@ class HomeViewModel(private val repository: EmbyRepository) : ViewModel() {
metadataCache[itemId]?.takeIf { detailMetadataComplete(itemId, it) }?.let { return it }
}
return try {
repository.getItemDetailsUncached(itemId).also { details ->
withSeriesTitleLogo(repository.getItemDetailsUncached(itemId)).also { details ->
synchronized(metadataCache) { metadataCache[itemId] = details }
}
} catch (cancelled: kotlinx.coroutines.CancellationException) {
@@ -732,6 +732,33 @@ class HomeViewModel(private val repository: EmbyRepository) : ViewModel() {
}
}
/**
* Completes an episode record that names no title treatment of its own.
*
* An episode has no Logo image; its treatment is the series', named by
* ParentLogoItemId/ParentLogoImageTag — and a route or a detail response that did not
* ask for the Logo image type carries neither. Resolving it here rather than at each
* reader is what makes the launcher's hero, the detail page, the player and the pause
* hero draw the same artwork: the record they all merge from already has the answer, so
* none of them has to work it out, and none of them can work it out differently.
*
* The series lookup is the repository's cached, single-flighted one, so a show already
* opened this session costs nothing, and a series with genuinely no logo leaves the
* record exactly as it was rather than being asked about again.
*/
private suspend fun withSeriesTitleLogo(details: BaseItem): BaseItem {
if (!details.isEpisode || details.hasTitleLogoMetadata) return details
val seriesId = details.seriesId?.takeIf(String::isNotBlank) ?: return details
val series = try {
repository.getItemDetails(seriesId)
} catch (cancelled: kotlinx.coroutines.CancellationException) {
throw cancelled
} catch (_: Throwable) {
null
}
return details.withSeriesTitleLogo(series)
}
/** Must be called while synchronised on [metadataCache]. */
private fun acceptSeriesStatusRevision(revision: Long) {
if (revision <= 0L || revision == seriesStatusRevision) return
@@ -751,6 +778,7 @@ class HomeViewModel(private val repository: EmbyRepository) : ViewModel() {
try {
runCatching { repository.getItemDetails(itemId) }
.getOrNull()
?.let { withSeriesTitleLogo(it) }
?.also { details ->
synchronized(metadataCache) {
if (seriesStatusRevision == revisionAtStart) {
@@ -107,6 +107,7 @@ import com.ponzischeme89.memby.ui.components.TvSettingsMenuOption
import com.ponzischeme89.memby.ui.components.showTvSettingsMenu
import com.ponzischeme89.memby.ui.components.showTvSettingsMultiChoiceMenu
import com.ponzischeme89.memby.ui.randomWelcomeQuote
import com.ponzischeme89.memby.ui.warmTitleLogo
import com.ponzischeme89.memby.ui.theme.AppFontFamily
import com.ponzischeme89.memby.ui.theme.MembyTheme
import com.ponzischeme89.memby.ui.theme.applyMembyTypeface
@@ -235,6 +236,9 @@ class PlayerActivity : ComponentActivity() {
private var launchTraceCookie = NO_TRACE
private var firstFrameTraceCookie = NO_TRACE
private var logoUrl: String? = null
/** The logo [warmPauseHeroLogo] has already fetched and judged, so a pause never re-asks. */
private var warmedLogoUrl: String? = null
private var loadingBackdropUrl: String? = null
private var playbackSeriesName: String? = null
private var playbackTitle = ""
@@ -4458,7 +4462,27 @@ class PlayerActivity : ComponentActivity() {
updatePauseHeroMetadata()
}
/**
* Fetches and judges the title treatment before a pause can ask for it.
*
* The pause hero draws a logo only once it has been judged legible against its own
* near-black, and until that verdict lands it shows the plain-text name so a set that
* came straight into playback without the launcher having warmed this logo showed the
* text and swapped, on a surface that appears for a moment and is looked at directly.
* The transport's own identity strip draws the same artwork with no such verdict, which
* is what made the two disagree. Warming here costs nothing on a logo already seen: the
* verdict is remembered for the URL and is process-wide, so the launcher's probe and
* this one are the same probe.
*/
private fun warmPauseHeroLogo() {
val url = logoUrl?.takeIf(String::isNotBlank) ?: return
if (url == warmedLogoUrl) return
warmedLogoUrl = url
lifecycleScope.launch { warmTitleLogo(url) }
}
private fun updatePauseHeroMetadata() {
warmPauseHeroLogo()
pauseHeroMetadata.value = PauseMediaHeroMetadata(
title = playbackTitle,
seriesName = playbackSeriesName,
Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB