0.3.25
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1012 KiB |
@@ -38,7 +38,7 @@ val membyGatewayUrl: String = (project.findProperty("memby.gatewayUrl") as Strin
|
|||||||
val membyDiagnosticLogLevel: String = (project.findProperty("memby.diagnosticLogLevel") as String?)
|
val membyDiagnosticLogLevel: String = (project.findProperty("memby.diagnosticLogLevel") as String?)
|
||||||
?.trim()?.uppercase()?.takeIf { it in setOf("INFO", "DEBUG", "TRACE") } ?: "INFO"
|
?.trim()?.uppercase()?.takeIf { it in setOf("INFO", "DEBUG", "TRACE") } ?: "INFO"
|
||||||
|
|
||||||
val defaultVersionName = "0.3.24"
|
val defaultVersionName = "0.3.25"
|
||||||
val membyVersionName: String =
|
val membyVersionName: String =
|
||||||
(project.findProperty("memby.versionName") as String?)
|
(project.findProperty("memby.versionName") as String?)
|
||||||
?.trim()
|
?.trim()
|
||||||
|
|||||||
@@ -64,14 +64,14 @@
|
|||||||
android:name="android.hardware.microphone"
|
android:name="android.hardware.microphone"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
|
<!-- The launcher icon is circular; the square mark remains an in-app asset. -->
|
||||||
<application
|
<application
|
||||||
android:name=".MembyApp"
|
android:name=".MembyApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:banner="@drawable/app_banner"
|
android:banner="@drawable/app_banner"
|
||||||
<!-- The launcher icon is circular; the square mark remains an in-app asset. -->
|
android:icon="@drawable/app_icon"
|
||||||
android:icon="@drawable/glossy_m"
|
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@drawable/glossy_m"
|
android:roundIcon="@drawable/app_icon"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
android:theme="@style/Theme.Memby">
|
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
|
// screensaver honoured it. The dark-logo fallback comes with it — a black title
|
||||||
// treatment on this near-black scrim is an invisible heading.
|
// treatment on this near-black scrim is an invisible heading.
|
||||||
val repository = ServiceLocator.repository
|
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
|
if (repository.showTitleLogo) repository.logoUrl(item) else null
|
||||||
}
|
}
|
||||||
val logo = logoUrl.takeIf { !useTextTitleForLogo(it) }
|
val logo = logoUrl.takeIf { !useTextTitleForLogo(it) }
|
||||||
|
|||||||
@@ -676,8 +676,14 @@ internal fun HomeScreen(
|
|||||||
var timeout: TimeoutCancellationException? = null
|
var timeout: TimeoutCancellationException? = null
|
||||||
repeat(2) {
|
repeat(2) {
|
||||||
try {
|
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) {
|
return@run withTimeout(PLAYBACK_SOURCE_RESOLUTION_TIMEOUT) {
|
||||||
repo.resolvePlayableForLaunch(item)
|
repo.resolvePlayableForLaunch(request)
|
||||||
}
|
}
|
||||||
} catch (error: TimeoutCancellationException) {
|
} catch (error: TimeoutCancellationException) {
|
||||||
timeout = error
|
timeout = error
|
||||||
|
|||||||
@@ -722,7 +722,7 @@ class HomeViewModel(private val repository: EmbyRepository) : ViewModel() {
|
|||||||
metadataCache[itemId]?.takeIf { detailMetadataComplete(itemId, it) }?.let { return it }
|
metadataCache[itemId]?.takeIf { detailMetadataComplete(itemId, it) }?.let { return it }
|
||||||
}
|
}
|
||||||
return try {
|
return try {
|
||||||
repository.getItemDetailsUncached(itemId).also { details ->
|
withSeriesTitleLogo(repository.getItemDetailsUncached(itemId)).also { details ->
|
||||||
synchronized(metadataCache) { metadataCache[itemId] = details }
|
synchronized(metadataCache) { metadataCache[itemId] = details }
|
||||||
}
|
}
|
||||||
} catch (cancelled: kotlinx.coroutines.CancellationException) {
|
} 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]. */
|
/** Must be called while synchronised on [metadataCache]. */
|
||||||
private fun acceptSeriesStatusRevision(revision: Long) {
|
private fun acceptSeriesStatusRevision(revision: Long) {
|
||||||
if (revision <= 0L || revision == seriesStatusRevision) return
|
if (revision <= 0L || revision == seriesStatusRevision) return
|
||||||
@@ -751,6 +778,7 @@ class HomeViewModel(private val repository: EmbyRepository) : ViewModel() {
|
|||||||
try {
|
try {
|
||||||
runCatching { repository.getItemDetails(itemId) }
|
runCatching { repository.getItemDetails(itemId) }
|
||||||
.getOrNull()
|
.getOrNull()
|
||||||
|
?.let { withSeriesTitleLogo(it) }
|
||||||
?.also { details ->
|
?.also { details ->
|
||||||
synchronized(metadataCache) {
|
synchronized(metadataCache) {
|
||||||
if (seriesStatusRevision == revisionAtStart) {
|
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.showTvSettingsMenu
|
||||||
import com.ponzischeme89.memby.ui.components.showTvSettingsMultiChoiceMenu
|
import com.ponzischeme89.memby.ui.components.showTvSettingsMultiChoiceMenu
|
||||||
import com.ponzischeme89.memby.ui.randomWelcomeQuote
|
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.AppFontFamily
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyTheme
|
import com.ponzischeme89.memby.ui.theme.MembyTheme
|
||||||
import com.ponzischeme89.memby.ui.theme.applyMembyTypeface
|
import com.ponzischeme89.memby.ui.theme.applyMembyTypeface
|
||||||
@@ -235,6 +236,9 @@ class PlayerActivity : ComponentActivity() {
|
|||||||
private var launchTraceCookie = NO_TRACE
|
private var launchTraceCookie = NO_TRACE
|
||||||
private var firstFrameTraceCookie = NO_TRACE
|
private var firstFrameTraceCookie = NO_TRACE
|
||||||
private var logoUrl: String? = null
|
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 loadingBackdropUrl: String? = null
|
||||||
private var playbackSeriesName: String? = null
|
private var playbackSeriesName: String? = null
|
||||||
private var playbackTitle = ""
|
private var playbackTitle = ""
|
||||||
@@ -4458,7 +4462,27 @@ class PlayerActivity : ComponentActivity() {
|
|||||||
updatePauseHeroMetadata()
|
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() {
|
private fun updatePauseHeroMetadata() {
|
||||||
|
warmPauseHeroLogo()
|
||||||
pauseHeroMetadata.value = PauseMediaHeroMetadata(
|
pauseHeroMetadata.value = PauseMediaHeroMetadata(
|
||||||
title = playbackTitle,
|
title = playbackTitle,
|
||||||
seriesName = playbackSeriesName,
|
seriesName = playbackSeriesName,
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 183 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 MiB |
Reference in New Issue
Block a user