0.3.24
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?)
|
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.23"
|
val defaultVersionName = "0.3.24"
|
||||||
val membyVersionName: String =
|
val membyVersionName: String =
|
||||||
(project.findProperty("memby.versionName") as String?)
|
(project.findProperty("memby.versionName") as String?)
|
||||||
?.trim()
|
?.trim()
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
@@ -68,9 +68,10 @@
|
|||||||
android:name=".MembyApp"
|
android:name=".MembyApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:banner="@drawable/app_banner"
|
android:banner="@drawable/app_banner"
|
||||||
android:icon="@drawable/memby_mark"
|
<!-- The launcher icon is circular; the square mark remains an in-app asset. -->
|
||||||
|
android:icon="@drawable/glossy_m"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@drawable/memby_mark"
|
android:roundIcon="@drawable/glossy_m"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
android:theme="@style/Theme.Memby">
|
android:theme="@style/Theme.Memby">
|
||||||
|
|||||||
@@ -2200,6 +2200,28 @@ class EmbyRepository internal constructor(
|
|||||||
runtimeMs = item.runTimeTicks?.div(10_000L)?.coerceAtLeast(0L) ?: 0L,
|
runtimeMs = item.runTimeTicks?.div(10_000L)?.coerceAtLeast(0L) ?: 0L,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
suspend fun playbackRequestForLaunch(item: BaseItem): PlaybackRequest {
|
||||||
|
val request = playbackRequest(item)
|
||||||
|
if (!item.isEpisode || request.logoUrl != null) return request
|
||||||
|
val seriesId = item.seriesId?.takeIf(String::isNotBlank) ?: return request
|
||||||
|
val seriesLogo = try {
|
||||||
|
getItemDetails(seriesId)?.let(::logoUrl)
|
||||||
|
} catch (error: CancellationException) {
|
||||||
|
throw error
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
return request.copy(logoUrl = seriesLogo)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a prefetched stream if one is sitting ready for [request], without suspending
|
* Returns a prefetched stream if one is sitting ready for [request], without suspending
|
||||||
* and without starting a request. This is what lets the launcher tell instantly whether
|
* and without starting a request. This is what lets the launcher tell instantly whether
|
||||||
@@ -2222,6 +2244,7 @@ class EmbyRepository internal constructor(
|
|||||||
requestedPositionMs = request.resumePositionMs,
|
requestedPositionMs = request.resumePositionMs,
|
||||||
localPositionMs = localResumePositionMs(request.itemId),
|
localPositionMs = localResumePositionMs(request.itemId),
|
||||||
),
|
),
|
||||||
|
logoUrl = request.logoUrl ?: entry.playable.logoUrl,
|
||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
playableMutex.unlock()
|
playableMutex.unlock()
|
||||||
@@ -2256,6 +2279,7 @@ class EmbyRepository internal constructor(
|
|||||||
requestedPositionMs = request.resumePositionMs,
|
requestedPositionMs = request.resumePositionMs,
|
||||||
localPositionMs = localResumePositionMs(request.itemId),
|
localPositionMs = localResumePositionMs(request.itemId),
|
||||||
),
|
),
|
||||||
|
logoUrl = request.logoUrl ?: cached.logoUrl,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (inFlight != null) {
|
if (inFlight != null) {
|
||||||
@@ -2271,6 +2295,7 @@ class EmbyRepository internal constructor(
|
|||||||
requestedPositionMs = request.resumePositionMs,
|
requestedPositionMs = request.resumePositionMs,
|
||||||
localPositionMs = localResumePositionMs(request.itemId),
|
localPositionMs = localResumePositionMs(request.itemId),
|
||||||
),
|
),
|
||||||
|
logoUrl = request.logoUrl ?: resolved.logoUrl,
|
||||||
).also {
|
).also {
|
||||||
playableMutex.withLock { playableCache.remove(request.itemId) }
|
playableMutex.withLock { playableCache.remove(request.itemId) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -407,9 +407,6 @@ internal fun HomeScreen(
|
|||||||
val destinationFocus = remember {
|
val destinationFocus = remember {
|
||||||
mutableMapOf<BrowseDestination, Pair<String, String>>()
|
mutableMapOf<BrowseDestination, Pair<String, String>>()
|
||||||
}
|
}
|
||||||
var showForYouNudge by rememberSaveable(settings.activeProfileId) {
|
|
||||||
mutableStateOf(!settings.hasOpenedForYou)
|
|
||||||
}
|
|
||||||
// Each destination owns a stable requester. Moving one requester between rail items
|
// Each destination owns a stable requester. Moving one requester between rail items
|
||||||
// can leave a key event pointing at the previously selected item for a frame.
|
// can leave a key event pointing at the previously selected item for a frame.
|
||||||
val navigationFocusRequesters = remember {
|
val navigationFocusRequesters = remember {
|
||||||
@@ -633,45 +630,47 @@ internal fun HomeScreen(
|
|||||||
// throws before one is started — a malformed cached item, an activity result
|
// throws before one is started — a malformed cached item, an activity result
|
||||||
// registry that has already been torn down — would leave Play dead for the rest
|
// registry that has already been torn down — would leave Play dead for the rest
|
||||||
// of the session with nothing on screen to say why.
|
// of the session with nothing on screen to say why.
|
||||||
val prepared = runCatching {
|
// Logo enrichment may need the associated series record. Keep it in the same
|
||||||
val request = repo.playbackRequest(item)
|
// cancellable launch job as stream resolution so Back abandons the whole hand-off.
|
||||||
request to repo.readyPlayableForLaunch(request)
|
|
||||||
}.getOrElse {
|
|
||||||
recordPlaybackFailed()
|
|
||||||
Toast.makeText(context, "Couldn’t start playback", Toast.LENGTH_SHORT).show()
|
|
||||||
launchingItem = null
|
|
||||||
homeViewModel.resumeAnalyticsAfterPlayback()
|
|
||||||
return@playItem
|
|
||||||
}
|
|
||||||
val request = prepared.first
|
|
||||||
val ready = prepared.second
|
|
||||||
if (ready == null && request.resumePositionMs > 0L) {
|
|
||||||
val launched = runCatching {
|
|
||||||
playbackLauncher.launch(
|
|
||||||
PlayerActivity.intent(
|
|
||||||
context = context,
|
|
||||||
request = request,
|
|
||||||
posterUrl = repo.primaryUrl(item, maxWidth = ARTWORK_CARD_MAX_WIDTH),
|
|
||||||
backdropUrl = repo.backdropUrl(item, maxWidth = ARTWORK_DETAIL_BACKDROP_MAX_WIDTH)
|
|
||||||
?: repo.primaryUrl(item, maxWidth = ARTWORK_DETAIL_PRIMARY_MAX_WIDTH),
|
|
||||||
requestStartedAtMs = playbackRequestedAtMs,
|
|
||||||
journeySource = entryPoint.id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (launched.isFailure) {
|
|
||||||
recordPlaybackFailed()
|
|
||||||
Toast.makeText(context, "Couldn’t start playback", Toast.LENGTH_SHORT).show()
|
|
||||||
launchingItem = null
|
|
||||||
homeViewModel.resumeAnalyticsAfterPlayback()
|
|
||||||
}
|
|
||||||
return@playItem
|
|
||||||
}
|
|
||||||
// Only the route that waits on the server shows the launcher's own loading screen.
|
|
||||||
// The route that hands over immediately would only be showing it behind the player.
|
|
||||||
resolvingItem = item
|
|
||||||
resolveJob = scope.launch {
|
resolveJob = scope.launch {
|
||||||
try {
|
try {
|
||||||
|
val prepared = runCatching {
|
||||||
|
val request = repo.playbackRequestForLaunch(item)
|
||||||
|
request to repo.readyPlayableForLaunch(request)
|
||||||
|
}.getOrElse {
|
||||||
|
recordPlaybackFailed()
|
||||||
|
Toast.makeText(context, "Couldn’t start playback", Toast.LENGTH_SHORT).show()
|
||||||
|
launchingItem = null
|
||||||
|
homeViewModel.resumeAnalyticsAfterPlayback()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val request = prepared.first
|
||||||
|
val ready = prepared.second
|
||||||
|
if (ready == null && request.resumePositionMs > 0L) {
|
||||||
|
val launched = runCatching {
|
||||||
|
playbackLauncher.launch(
|
||||||
|
PlayerActivity.intent(
|
||||||
|
context = context,
|
||||||
|
request = request,
|
||||||
|
posterUrl = repo.primaryUrl(item, maxWidth = ARTWORK_CARD_MAX_WIDTH),
|
||||||
|
backdropUrl = repo.backdropUrl(item, maxWidth = ARTWORK_DETAIL_BACKDROP_MAX_WIDTH)
|
||||||
|
?: repo.primaryUrl(item, maxWidth = ARTWORK_DETAIL_PRIMARY_MAX_WIDTH),
|
||||||
|
requestStartedAtMs = playbackRequestedAtMs,
|
||||||
|
journeySource = entryPoint.id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (launched.isFailure) {
|
||||||
|
recordPlaybackFailed()
|
||||||
|
Toast.makeText(context, "Couldn’t start playback", Toast.LENGTH_SHORT).show()
|
||||||
|
launchingItem = null
|
||||||
|
homeViewModel.resumeAnalyticsAfterPlayback()
|
||||||
|
}
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
// Only the route that waits on the server shows the launcher's own loading screen.
|
||||||
|
// The route that hands over immediately would only be showing it behind the player.
|
||||||
|
resolvingItem = item
|
||||||
runCatching {
|
runCatching {
|
||||||
val playable = ready ?: run {
|
val playable = ready ?: run {
|
||||||
var timeout: TimeoutCancellationException? = null
|
var timeout: TimeoutCancellationException? = null
|
||||||
@@ -898,16 +897,6 @@ internal fun HomeScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
val latestJourneyScreen by rememberUpdatedState(journeyScreen)
|
val latestJourneyScreen by rememberUpdatedState(journeyScreen)
|
||||||
LaunchedEffect(showForYouNudge, selectedDestination, settings.hasOpenedForYou) {
|
|
||||||
if (
|
|
||||||
showForYouNudge &&
|
|
||||||
selectedDestination == BrowseDestination.HOME &&
|
|
||||||
!settings.hasOpenedForYou
|
|
||||||
) {
|
|
||||||
delay(5.seconds)
|
|
||||||
showForYouNudge = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(
|
LaunchedEffect(
|
||||||
selectedDestination,
|
selectedDestination,
|
||||||
contextualHeroItems.firstOrNull()?.item?.id,
|
contextualHeroItems.firstOrNull()?.item?.id,
|
||||||
@@ -2775,15 +2764,6 @@ internal fun HomeScreen(
|
|||||||
modifier = Modifier.fillMaxSize().zIndex(9f),
|
modifier = Modifier.fillMaxSize().zIndex(9f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ForYouNudgeBanner(
|
|
||||||
visible = showForYouNudge &&
|
|
||||||
selectedDestination == BrowseDestination.HOME &&
|
|
||||||
!settings.hasOpenedForYou &&
|
|
||||||
liveMaintenance == null,
|
|
||||||
username = settings.username,
|
|
||||||
shortName = settings.shortName,
|
|
||||||
modifier = Modifier.align(Alignment.TopCenter),
|
|
||||||
)
|
|
||||||
// Emby has stopped answering. Persistent, unlike the news bar below it, because
|
// Emby has stopped answering. Persistent, unlike the news bar below it, because
|
||||||
// it describes a state rather than an event: a set switched on midway through an
|
// it describes a state rather than an event: a set switched on midway through an
|
||||||
// outage was never told, and this is the only thing that says why nothing plays.
|
// outage was never told, and this is the only thing that says why nothing plays.
|
||||||
@@ -2809,5 +2789,3 @@ internal fun HomeScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -16,6 +17,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
@@ -51,13 +53,18 @@ internal fun PinBoxesField(
|
|||||||
value: String,
|
value: String,
|
||||||
onValueChange: (String) -> Unit,
|
onValueChange: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
onDone: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = { onValueChange(it.filter(Char::isDigit).take(PIN_LENGTH)) },
|
onValueChange = { onValueChange(it.filter(Char::isDigit).take(PIN_LENGTH)) },
|
||||||
textStyle = TextStyle(color = Color.Transparent, fontSize = 1.sp),
|
textStyle = TextStyle(color = Color.Transparent, fontSize = 1.sp),
|
||||||
cursorBrush = SolidColor(Color.Transparent),
|
cursorBrush = SolidColor(Color.Transparent),
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.NumberPassword,
|
||||||
|
imeAction = ImeAction.Done,
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(onDone = { onDone() }),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
decorationBox = {
|
decorationBox = {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
|||||||
@@ -51,7 +51,18 @@ internal fun PinSetupScreen(viewer: MembyViewer, onComplete: () -> Unit) {
|
|||||||
modifier = Modifier.width(460.dp),
|
modifier = Modifier.width(460.dp),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
PinBoxesField(pin, { pin = it; error = null })
|
val submitPin = {
|
||||||
|
if (!saving && pin.length == PIN_LENGTH) {
|
||||||
|
saving = true
|
||||||
|
scope.launch {
|
||||||
|
runCatching { ServiceLocator.repository.setViewerPIN(viewer, pin) }
|
||||||
|
.onSuccess { onComplete() }
|
||||||
|
.onFailure { error = "That PIN could not be saved." }
|
||||||
|
saving = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PinBoxesField(pin, { pin = it; error = null }, onDone = submitPin)
|
||||||
error?.let {
|
error?.let {
|
||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
Text(it, color = MembyAccent, fontSize = 13.sp)
|
Text(it, color = MembyAccent, fontSize = 13.sp)
|
||||||
@@ -60,15 +71,7 @@ internal fun PinSetupScreen(viewer: MembyViewer, onComplete: () -> Unit) {
|
|||||||
Button(
|
Button(
|
||||||
enabled = !saving && pin.length == PIN_LENGTH,
|
enabled = !saving && pin.length == PIN_LENGTH,
|
||||||
contentPadding = PaddingValues(horizontal = 28.dp, vertical = 8.dp),
|
contentPadding = PaddingValues(horizontal = 28.dp, vertical = 8.dp),
|
||||||
onClick = {
|
onClick = submitPin,
|
||||||
saving = true
|
|
||||||
scope.launch {
|
|
||||||
runCatching { ServiceLocator.repository.setViewerPIN(viewer, pin) }
|
|
||||||
.onSuccess { onComplete() }
|
|
||||||
.onFailure { error = "That PIN could not be saved." }
|
|
||||||
saving = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) { Text("Save PIN") }
|
) { Text("Save PIN") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,19 @@ internal fun DeviceRecoveryScreen(
|
|||||||
Text(if (profile.viewer.hasPin) "Enter your PIN" else "Continue as ${profile.viewer.name}", color = MembyQuietText, fontSize = 14.sp)
|
Text(if (profile.viewer.hasPin) "Enter your PIN" else "Continue as ${profile.viewer.name}", color = MembyQuietText, fontSize = 14.sp)
|
||||||
if (profile.viewer.hasPin) {
|
if (profile.viewer.hasPin) {
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
PinBoxesField(pin, { pin = it; error = null })
|
PinBoxesField(
|
||||||
|
pin,
|
||||||
|
{ pin = it; error = null },
|
||||||
|
onDone = {
|
||||||
|
if (pin.length == PIN_LENGTH) {
|
||||||
|
scope.launch {
|
||||||
|
runCatching { ServiceLocator.repository.recoverWithPIN(profile, pin) }
|
||||||
|
.onSuccess { onRecovered() }
|
||||||
|
.onFailure { error = "That PIN was not accepted." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
error?.let {
|
error?.let {
|
||||||
Spacer(Modifier.height(6.dp))
|
Spacer(Modifier.height(6.dp))
|
||||||
|
|||||||
@@ -2704,7 +2704,10 @@ class PlayerActivity : ComponentActivity() {
|
|||||||
/**
|
/**
|
||||||
* True when Left and Right have nothing else to mean, and there is something to seek.
|
* True when Left and Right have nothing else to mean, and there is something to seek.
|
||||||
*
|
*
|
||||||
* The transport being up is the important half: with the controls visible those keys
|
* The transport being up is the important half. Use [transportVisible], the visibility
|
||||||
|
* listener's state, rather than Media3's fully-visible query: during the show/hide
|
||||||
|
* animation the latter can still say hidden while the controls already own the remote.
|
||||||
|
* With the controls visible those keys
|
||||||
* belong to whatever holds focus — the time bar, or the row of buttons — and taking
|
* belong to whatever holds focus — the time bar, or the row of buttons — and taking
|
||||||
* them would leave the viewer unable to reach the subtitle or cast controls at all.
|
* them would leave the viewer unable to reach the subtitle or cast controls at all.
|
||||||
* A stream that cannot be sought (a live recording still being written, a container
|
* A stream that cannot be sought (a live recording still being written, a container
|
||||||
@@ -2718,7 +2721,7 @@ class PlayerActivity : ComponentActivity() {
|
|||||||
playback.isCurrentMediaItemSeekable &&
|
playback.isCurrentMediaItemSeekable &&
|
||||||
playback.duration > 0L &&
|
playback.duration > 0L &&
|
||||||
playback.duration != C.TIME_UNSET &&
|
playback.duration != C.TIME_UNSET &&
|
||||||
playerView?.isControllerFullyVisible != true &&
|
!transportVisible &&
|
||||||
!castPanelVisible.value &&
|
!castPanelVisible.value &&
|
||||||
subtitleOverlay?.isVisible != true &&
|
subtitleOverlay?.isVisible != true &&
|
||||||
creditsView?.isVisible != true &&
|
creditsView?.isVisible != true &&
|
||||||
|
|||||||
@@ -449,7 +449,10 @@ class SearchViewModel(private val repository: EmbyRepository) : ViewModel() {
|
|||||||
// viewer typed exactly. This matters especially in the one-column list,
|
// viewer typed exactly. This matters especially in the one-column list,
|
||||||
// where tenth place is ten rows away rather than the second grid row.
|
// where tenth place is ten rows away rather than the second grid row.
|
||||||
val ranked = if (_state.value.results.isEmpty()) rankSearchResults(term, items) else items
|
val ranked = if (_state.value.results.isEmpty()) rankSearchResults(term, items) else items
|
||||||
_state.update { it.copy(results = ranked, isLoading = false, hasSearched = true, errorMessage = null) }
|
// Keep the loading signal alive after the first snapshot. The stream may still
|
||||||
|
// be waiting on Sonarr or Radarr, but the cards already on screen remain fully
|
||||||
|
// focusable and navigable while those additional results arrive.
|
||||||
|
_state.update { it.copy(results = ranked, isLoading = true, hasSearched = true, errorMessage = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFailure { error ->
|
.onFailure { error ->
|
||||||
@@ -463,6 +466,7 @@ class SearchViewModel(private val repository: EmbyRepository) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_state.value.genre == null && _state.value.query.trim() == term) {
|
if (_state.value.genre == null && _state.value.query.trim() == term) {
|
||||||
|
_state.update { it.copy(isLoading = false) }
|
||||||
cache[term] = _state.value.results
|
cache[term] = _state.value.results
|
||||||
viewModelScope.launch { repository.recordSearch(term) }
|
viewModelScope.launch { repository.recordSearch(term) }
|
||||||
if (_state.value.requestMode) loadRequestCandidates(term)
|
if (_state.value.requestMode) loadRequestCandidates(term)
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import com.ponzischeme89.memby.ui.search.SearchUiState
|
|||||||
import com.ponzischeme89.memby.ui.search.shouldSearch
|
import com.ponzischeme89.memby.ui.search.shouldSearch
|
||||||
import com.ponzischeme89.memby.ui.requests.RequestCard
|
import com.ponzischeme89.memby.ui.requests.RequestCard
|
||||||
import com.ponzischeme89.memby.ui.requests.RequestActionRequest
|
import com.ponzischeme89.memby.ui.requests.RequestActionRequest
|
||||||
|
import com.ponzischeme89.memby.ui.requests.RequestStatus
|
||||||
import com.ponzischeme89.memby.ui.theme.FactSeparator
|
import com.ponzischeme89.memby.ui.theme.FactSeparator
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyAccent
|
import com.ponzischeme89.memby.ui.theme.MembyAccent
|
||||||
import com.ponzischeme89.memby.ui.theme.MembyIcon
|
import com.ponzischeme89.memby.ui.theme.MembyIcon
|
||||||
|
|||||||
@@ -153,7 +153,11 @@ internal fun ViewerPinEntry(
|
|||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text("Use a 4-digit PIN to protect this profile.", color = MembyQuietText)
|
Text("Use a 4-digit PIN to protect this profile.", color = MembyQuietText)
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
PinBoxesField(pin, { pin = it })
|
PinBoxesField(
|
||||||
|
pin,
|
||||||
|
{ pin = it },
|
||||||
|
onDone = { if (!saving && pin.length == PIN_LENGTH) onConfirm(pin) },
|
||||||
|
)
|
||||||
failure?.let { Text(it, color = MembyAccent) }
|
failure?.let { Text(it, color = MembyAccent) }
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(14.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
@@ -1 +1 @@
|
|||||||
0.1.74
|
0.1.75
|
||||||
|
|||||||
Reference in New Issue
Block a user