Wire selected Jellyfin users to Conductarr TV data
This commit is contained in:
parent
f53defad90
commit
59c2728d9c
38 changed files with 1879 additions and 110 deletions
|
|
@ -38,8 +38,17 @@ import org.justinvisser.wolfskin.design.WolfskinTokens.Requests as T
|
|||
fun ComingInSheet(shell: ShellState, modifier: Modifier = Modifier) {
|
||||
val state = shell.requests
|
||||
val data = state.snapshot
|
||||
val ratingsReader = LocalScreenRatings.current
|
||||
LaunchedEffect(state.focusedId, data.signedIn, data.status) {
|
||||
state.awaitFocusedRatings()
|
||||
if (ratingsReader == null) state.awaitFocusedRatings()
|
||||
else if (data.signedIn) state.focused?.let { item ->
|
||||
ratingsReader.read(item.tmdbId, item.mediaType, null, null)?.let { values ->
|
||||
state.updateRatings(item.id, values.map { rating ->
|
||||
val mapped = rating.homeRating()
|
||||
ComingInRating(mapped.mark, mapped.score, mapped.percentage)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(modifier.fillMaxSize().background(C.sheetScrim).clickable { shell.closeRequests() }.testTag("coming-in-scrim"))
|
||||
Column(modifier.fillMaxHeight().width(L.sheetWidth)
|
||||
|
|
|
|||
|
|
@ -85,6 +85,12 @@ class ComingInState {
|
|||
ratingsVisible = snapshot.signedIn && focused?.ratingKey in cachedRatings
|
||||
}
|
||||
|
||||
fun updateRatings(id: String, values: List<ComingInRating>) {
|
||||
snapshot = snapshot.copy(items = snapshot.items.map { if (it.id == id) it.copy(ratings = values) else it })
|
||||
snapshot.items.firstOrNull { it.id == id }?.let { cachedRatings.add(it.ratingKey) }
|
||||
ratingsVisible = snapshot.signedIn && focused?.ratingKey in cachedRatings
|
||||
}
|
||||
|
||||
/** Coroutine cancellation and the captured request id prevent stale focus reads from painting. */
|
||||
suspend fun awaitFocusedRatings() {
|
||||
val item = focused ?: return
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ internal fun DiscoverTitle.libraryCard(state: DiscoverState): LibraryTitle {
|
|||
}
|
||||
return LibraryTitle(id = key, title = title, type = type, year = year,
|
||||
runtimeSeconds = runtimeSeconds?.toLong(), seasons = seasonCount, poster = poster,
|
||||
ratings = ratings.map { LibraryRating(it.source, it.score, it.label) }, availability = availability,
|
||||
ratings = state.ratingsFor(this).map { LibraryRating(it.source, it.score, it.label) }, availability = availability,
|
||||
badge = when {
|
||||
held -> "held"
|
||||
receipts.isEmpty() -> null
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ internal fun projectDiscoverLanes(snapshot: DiscoverSnapshot, heroes: List<Disco
|
|||
snapshot.lanes.forEach { lane ->
|
||||
val source = lane.fullTitles.filter { matches(it) && it.key !in displayed && it.key !in hidden }
|
||||
val items = source.take(if (lane.id.startsWith("new-")) 12 else WolfskinTokens.Behaviour.rowLimit.toInt())
|
||||
if (items.isNotEmpty() || lane.tail == DiscoverTail.Retry || lane.tail == DiscoverTail.ShowMore) {
|
||||
if (items.isNotEmpty() || lane.pending || lane.tail == DiscoverTail.Retry || lane.tail == DiscoverTail.ShowMore) {
|
||||
result += lane.copy(titles = items, fullTitles = source, total = if (lane.id.startsWith("new-")) items.size else source.size,
|
||||
tail = if (lane.tail == DiscoverTail.Retry || lane.tail == DiscoverTail.ShowMore) lane.tail else if (!lane.id.startsWith("new-") && source.size > items.size) DiscoverTail.SeeAll else DiscoverTail.None)
|
||||
if (!snapshot.reusesFixtureLanes) items.forEach { displayed.add(it.key) }
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ data class DiscoverSnapshot(
|
|||
val status: DiscoverStatus = DiscoverStatus.NeedsSignIn,
|
||||
val savedAt: String? = null,
|
||||
val reusesFixtureLanes: Boolean = false,
|
||||
val reachable: Boolean = true,
|
||||
val requestsAllowed: Boolean = true,
|
||||
)
|
||||
|
||||
/** Lane identity survives lazy recycling and provider refreshes. */
|
||||
|
|
@ -57,6 +59,7 @@ data class DiscoverLane(
|
|||
val pick: Boolean = false,
|
||||
val hidden: Boolean = false,
|
||||
val fullTitles: List<DiscoverTitle> = titles,
|
||||
val pending: Boolean = false,
|
||||
)
|
||||
|
||||
/** Caller identity is deliberately absent: the authenticated client supplies the actor. */
|
||||
|
|
|
|||
|
|
@ -48,13 +48,21 @@ fun DiscoverScreen(state: DiscoverState, modifier: Modifier = Modifier) {
|
|||
}
|
||||
LaunchedEffect(state.toast) { if (state.toast != null) { kotlinx.coroutines.delay(WolfskinTokens.Motion.toast.toLong()); state.toast = null } }
|
||||
val focusedTitle = if (state.collection != null) null else state.focusedLane?.titles?.getOrNull(state.column)
|
||||
val ratingsReader = LocalScreenRatings.current
|
||||
LaunchedEffect(focusedTitle?.key, state.contentFocused) {
|
||||
state.plateKey = null
|
||||
if (state.contentFocused && focusedTitle != null) {
|
||||
if (focusedTitle.key !in state.observedRatings) kotlinx.coroutines.delay(WolfskinTokens.Motion.ratingDwell.toLong())
|
||||
if (state.isSignedIn && ratingsReader != null) ratingsReader.read(focusedTitle.tmdb, focusedTitle.type, null, null)?.let { state.updateRatings(focusedTitle, it) }
|
||||
state.observedRatings.add(focusedTitle.key); state.plateKey = focusedTitle.key
|
||||
}
|
||||
}
|
||||
LaunchedEffect(state.hero?.key, state.title?.key, state.isSignedIn) {
|
||||
if (state.isSignedIn && ratingsReader != null) {
|
||||
val title = state.title ?: state.hero
|
||||
if (title != null) ratingsReader.read(title.tmdb, title.type, null, null)?.let { state.updateRatings(title, it) }
|
||||
}
|
||||
}
|
||||
if (state.title != null) { DiscoverTitleDestination(state, modifier); return }
|
||||
state.collection?.let {
|
||||
LibraryScreen(shell, it, modifier, ratingsSignedIn = state.isSignedIn, offlineArt = LocalDiscoverArt.current, registerFocus = false)
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ fun DiscoverShelf(state: DiscoverState, lane: DiscoverLane, row: Int) {
|
|||
.clickable { state.focus(row, index); state.openService(service) }, contentAlignment = Alignment.BottomStart) {
|
||||
Text(service.name, Modifier.padding(start = D.entryPaddingX, bottom = D.entryPaddingY), style = DiscoverType.entry, color = C.fg)
|
||||
}
|
||||
} else if (lane.pending) {
|
||||
items(slots, key = { "pending:$it" }) {
|
||||
Box(Modifier.size(K.posterW, K.posterH).background(C.surface3, RoundedCornerShape(WolfskinTokens.Radius.card)))
|
||||
}
|
||||
} else {
|
||||
itemsIndexed(lane.titles, key = { _, item -> item.key }, contentType = { _, _ -> "poster" }) { index, item ->
|
||||
DiscoverPoster(state, item, active && index == column, Modifier.clickable {
|
||||
|
|
@ -88,7 +92,7 @@ fun DiscoverShelf(state: DiscoverState, lane: DiscoverLane, row: Int) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (!services && active) {
|
||||
if (!services && active && !lane.pending) {
|
||||
val item = lane.titles.getOrNull(column)
|
||||
Caption(
|
||||
item?.title ?: if (lane.tail == DiscoverTail.SeeAll) "See all ${lane.title}" else if (lane.tail == DiscoverTail.Retry) "Try again" else "Show more",
|
||||
|
|
@ -105,7 +109,7 @@ fun DiscoverShelf(state: DiscoverState, lane: DiscoverLane, row: Int) {
|
|||
@Composable
|
||||
fun DiscoverPoster(state: DiscoverState, title: DiscoverTitle, focused: Boolean, modifier: Modifier = Modifier) {
|
||||
val receipts = state.requests[title.key]
|
||||
val card = remember(title, receipts, state.shell.person.key) { title.libraryCard(state) }
|
||||
val card = remember(title, receipts, state.ratingsFor(title), state.shell.person.key) { title.libraryCard(state) }
|
||||
LibraryPoster(card, focused, focused && state.plateKey == title.key && state.isSignedIn, true,
|
||||
modifier.zIndex(if (focused) 1f else 0f), offlineArt = LocalDiscoverArt.current)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ class DiscoverState(
|
|||
private val onPush: (String) -> Unit = {},
|
||||
private val onPop: () -> Unit = {},
|
||||
private val serviceLanes: (DiscoverService, String) -> List<DiscoverLane> = { _, _ -> emptyList() },
|
||||
private val requesters: List<ShellPerson> = shell.people,
|
||||
private val onMore: (suspend (String) -> List<DiscoverLane>?)? = null,
|
||||
) : ShellContentFocus {
|
||||
var snapshot by mutableStateOf(initial)
|
||||
private set
|
||||
|
|
@ -46,6 +48,7 @@ class DiscoverState(
|
|||
val requests = mutableStateMapOf<String, List<DiscoverRequestResult>>()
|
||||
private val requestSheets = mutableMapOf<Pair<String, String>, RequestSheetState>()
|
||||
private val hidden = mutableStateMapOf<String, DiscoverTitle>().apply { initial.hidden.forEach { put(it.key, it) } }
|
||||
private val ratingValues = mutableStateMapOf<String, List<DiscoverRating>>()
|
||||
private lateinit var collectionCards: LinkedHashMap<String, LibraryTitle>
|
||||
private var revision by mutableStateOf(0)
|
||||
private var cachedRevision = -1
|
||||
|
|
@ -59,11 +62,44 @@ class DiscoverState(
|
|||
private val dismissalVersions = mutableMapOf<String, Int>()
|
||||
private var rootFilter = "all"
|
||||
|
||||
fun updateSnapshot(value: DiscoverSnapshot) {
|
||||
snapshot = value
|
||||
hidden.clear()
|
||||
value.hidden.forEach { hidden[it.key] = it }
|
||||
revision++
|
||||
}
|
||||
|
||||
fun invalidateLanes() { revision++ }
|
||||
fun updateLanes(value: List<DiscoverLane>) { snapshot = snapshot.copy(lanes = value); revision++ }
|
||||
|
||||
private suspend fun loadLane(id: String) {
|
||||
val loaded = onMore?.invoke(id)
|
||||
if (loaded != null) updateLanes(loaded) else { snapshot = operations.refresh(); revision++ }
|
||||
}
|
||||
|
||||
fun updateTitle(value: DiscoverTitle) {
|
||||
if (title?.key == value.key) title = value
|
||||
}
|
||||
|
||||
fun ratingsFor(item: DiscoverTitle): List<DiscoverRating> = ratingValues[item.key] ?: item.ratings
|
||||
|
||||
fun updateRatings(item: DiscoverTitle, values: List<LibraryRating>) {
|
||||
val ratings = values.map { DiscoverRating(it.source, it.score, it.label) }
|
||||
ratingValues[item.key] = ratings
|
||||
fun replace(source: List<DiscoverTitle>) = source.map { if (it.key == item.key) it.copy(ratings = ratings) else it }
|
||||
snapshot = snapshot.copy(picks = replace(snapshot.picks),
|
||||
lanes = snapshot.lanes.map { it.copy(titles = replace(it.titles), fullTitles = replace(it.fullTitles)) },
|
||||
seeds = snapshot.seeds.map { it.copy(items = replace(it.items)) })
|
||||
title?.takeIf { it.key == item.key }?.let { title = it.copy(ratings = ratings) }
|
||||
revision++
|
||||
}
|
||||
|
||||
val isSignedIn: Boolean get() = snapshot.status != DiscoverStatus.NeedsSignIn
|
||||
val canRequest: Boolean get() = when (snapshot.status) {
|
||||
DiscoverStatus.Ready, DiscoverStatus.NoSeeds, DiscoverStatus.PicksUnavailable -> true
|
||||
else -> false
|
||||
}
|
||||
val requestsEnabled: Boolean get() = canRequest && snapshot.reachable && snapshot.requestsAllowed
|
||||
val hasHero: Boolean get() = service == null && collection == null && heroes.isNotEmpty() && canRequest
|
||||
val heroes: List<DiscoverTitle> get() {
|
||||
if (cachedHeroesRevision != revision || cachedHeroesFilter != filter) {
|
||||
|
|
@ -88,7 +124,7 @@ class DiscoverState(
|
|||
val focusedLane: DiscoverLane? get() = lanes.getOrNull(focusedRow - firstShelf)
|
||||
val column: Int get() = shell.view.cards[rowKey].orZero().coerceAtMost(maxColumn())
|
||||
val rowKey: String get() = when (focusedRow) { 0 -> "phero"; 1 -> "dchips"; else -> focusedLane?.id.orEmpty() }
|
||||
val isCard: Boolean get() = title == null && focusedRow >= firstShelf && focusedLane != null
|
||||
val isCard: Boolean get() = title == null && focusedRow >= firstShelf && focusedLane != null && focusedLane?.pending != true
|
||||
val contentFocused: Boolean get() = !shell.navFocused && !shell.pickerVisible && overlay == null
|
||||
override val cardKey: String? get() = when {
|
||||
overlay != null || title != null -> null
|
||||
|
|
@ -127,7 +163,7 @@ class DiscoverState(
|
|||
grid = LibraryGridState(lane.title, collectionCards.values.filter { it.id !in hidden }, catalogue = true, observedRatings = observedRatings,
|
||||
onTitle = { openTitle(titles.getValue(it.id)) }, onDiscover = { collection = null; onPop() }, onRecover = {
|
||||
scope.launch {
|
||||
snapshot = operations.refresh(); revision++
|
||||
loadLane(lane.id)
|
||||
if (collection !== grid) return@launch
|
||||
val fresh = lanes.firstOrNull { it.id == lane.id }
|
||||
if (canRequest && fresh != null) {
|
||||
|
|
@ -142,6 +178,16 @@ class DiscoverState(
|
|||
}).apply {
|
||||
tail = when (lane.tail) { DiscoverTail.Retry -> GridTail.Retry; DiscoverTail.ShowMore -> GridTail.ShowMore; else -> GridTail.None }
|
||||
}
|
||||
if (onMore != null && lane.tail != DiscoverTail.None) grid.pagination = LibraryPagination(grid) {
|
||||
loadLane(lane.id)
|
||||
val fresh = lanes.firstOrNull { it.id == lane.id } ?: error("Catalogue lane unavailable")
|
||||
if (fresh.tail == DiscoverTail.Retry) error("Catalogue read unavailable")
|
||||
titles = fresh.fullTitles.associateBy { it.key }
|
||||
collectionCards = LinkedHashMap<String, LibraryTitle>(titles.size).apply {
|
||||
titles.values.forEach { put(it.key, it.libraryCard(this@DiscoverState)) }
|
||||
}
|
||||
LibraryPage(collectionCards.values.filter { it.id !in hidden }, fresh.tail != DiscoverTail.None)
|
||||
}
|
||||
collection = grid
|
||||
onPush("discover-see-all/${lane.id}")
|
||||
}
|
||||
|
|
@ -217,7 +263,7 @@ class DiscoverState(
|
|||
if (lane.id == "svc") { snapshot.services.getOrNull(column)?.let(::openService); return }
|
||||
val item = lane.titles.getOrNull(column)
|
||||
if (item == null) {
|
||||
if (lane.tail in setOf(DiscoverTail.Retry, DiscoverTail.ShowMore)) refresh()
|
||||
if (lane.tail in setOf(DiscoverTail.Retry, DiscoverTail.ShowMore)) scope.launch { loadLane(lane.id) }
|
||||
else openCollection(lane)
|
||||
} else if (lane.hidden) overlay = DiscoverOverlayState.Restore(item)
|
||||
else openTitle(item)
|
||||
|
|
@ -243,7 +289,8 @@ class DiscoverState(
|
|||
|
||||
fun openRequest(other: Boolean, item: DiscoverTitle = requireNotNull(title), phase: RequestPhase = RequestPhase.Choose) {
|
||||
if (!canRequest) { refresh(); return }
|
||||
val choice = RequestSheetState(item, shell.person, shell.people, other, phase)
|
||||
if (!requestsEnabled) { toast = "Can’t request right now. Try again."; return }
|
||||
val choice = RequestSheetState(item, shell.person, requesters, other, phase)
|
||||
val previous = requestSheets[item.key to choice.selectedPerson.key]
|
||||
overlay = DiscoverOverlayState.Request(
|
||||
if (phase == RequestPhase.Choose && previous?.phase in setOf(RequestPhase.Sending, RequestPhase.NotConfirmed, RequestPhase.Pending)) requireNotNull(previous) else choice,
|
||||
|
|
@ -276,7 +323,7 @@ class DiscoverState(
|
|||
}
|
||||
|
||||
private fun send(sheet: RequestSheetState, check: Boolean) {
|
||||
if (!canRequest || (!check && !sheet.canSubmit)) return
|
||||
if (!requestsEnabled || (!check && !sheet.canSubmit)) return
|
||||
if (!check) {
|
||||
val previous = requestSheets[sheet.title.key to sheet.selectedPerson.key]
|
||||
if (previous?.phase in setOf(RequestPhase.Sending, RequestPhase.NotConfirmed, RequestPhase.Pending)) {
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ fun HomeScreen(
|
|||
view.listState.animateScrollBy(target - current, tween(WolfskinTokens.Motion.scroll, easing = CubicBezierEasing(.33f, 1f, .68f, 1f)))
|
||||
}
|
||||
val cache = view.home.ratings
|
||||
val ratingsReader = LocalScreenRatings.current
|
||||
val row = snapshot.rows.getOrNull(view.row)
|
||||
val title = row?.cards?.getOrNull(focus.column)
|
||||
val hero = snapshot.rows.firstOrNull { it.kind == HomeRowKind.Hero }?.cards?.firstOrNull()
|
||||
|
|
@ -87,13 +88,14 @@ fun HomeScreen(
|
|||
var focusedRatings by remember(title?.id, shell.navFocused) { mutableStateOf(title?.let(cache::read).orEmpty()) }
|
||||
LaunchedEffect(title?.id, hero?.id, shell.navFocused, shell.pickerVisible, snapshot.conductarr) {
|
||||
if (hero != null && snapshot.conductarr == HomeAvailability.Ready) {
|
||||
heroRatings = hero.ratings
|
||||
cache.save(hero, heroRatings)
|
||||
val values = if (ratingsReader == null) hero.ratings else hero.tmdbId?.let { ratingsReader.read(it.toLong(), hero.type, hero.season, hero.episode)?.map(LibraryRating::homeRating) }
|
||||
if (values != null) { heroRatings = values; cache.save(hero, values) }
|
||||
}
|
||||
if (title != null && !shell.navFocused && !shell.pickerVisible && snapshot.conductarr == HomeAvailability.Ready) {
|
||||
val saved = cache.read(title)
|
||||
if (saved == null && row.kind != HomeRowKind.Hero) delay(WolfskinTokens.Motion.ratingDwell.toLong())
|
||||
focusedRatings = saved ?: title.ratings.also { cache.save(title, it) }
|
||||
val values = saved ?: if (ratingsReader == null) title.ratings else title.tmdbId?.let { ratingsReader.read(it.toLong(), title.type, title.season, title.episode)?.map(LibraryRating::homeRating) }
|
||||
if (values != null) { focusedRatings = values; cache.save(title, values) }
|
||||
}
|
||||
}
|
||||
val showRatings = snapshot.conductarr != HomeAvailability.SignedOut
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ fun LibraryScreen(
|
|||
val focused = grid.focusedTitle
|
||||
val count = if (grid.catalogue) items.size else grid.source.size
|
||||
var plateKey by remember { mutableStateOf<String?>(null) }
|
||||
val ratingsReader = LocalScreenRatings.current
|
||||
DisposableEffect(shell, grid, registerFocus) {
|
||||
if (registerFocus) shell.contentFocus = grid
|
||||
onDispose { if (registerFocus && shell.contentFocus === grid) shell.contentFocus = null }
|
||||
|
|
@ -72,6 +73,11 @@ fun LibraryScreen(
|
|||
plateKey = null
|
||||
if (active && ratingsSignedIn && focused != null && (ratingsReachable || focused.id in cachedRatings || focused.id in grid.observedRatings)) {
|
||||
if (focused.id !in cachedRatings && focused.id !in grid.observedRatings) delay(WolfskinTokens.Motion.ratingDwell.toLong())
|
||||
if (ratingsReader != null) {
|
||||
val tmdb = focused.tmdbId?.toLongOrNull() ?: focused.id.substringAfter(':', "").toLongOrNull()
|
||||
val ratings = tmdb?.let { ratingsReader.read(it, focused.type, null, null) }
|
||||
if (ratings != null) grid.source = grid.source.map { if (it.id == focused.id) it.copy(ratings = ratings) else it }
|
||||
}
|
||||
if (focused.ratings.isNotEmpty()) grid.observedRatings.add(focused.id)
|
||||
plateKey = focused.id
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
package org.justinvisser.wolfskin.design
|
||||
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
|
||||
/** One focused-item collector reads through this seam; null keeps deterministic fixture ratings. */
|
||||
fun interface ScreenRatings {
|
||||
suspend fun read(tmdb: Long, type: String, season: Int?, episode: Int?): List<LibraryRating>?
|
||||
}
|
||||
|
||||
val LocalScreenRatings = compositionLocalOf<ScreenRatings?> { null }
|
||||
|
||||
/** Convert supplier marks without inferring a Rotten Tomatoes label from a percentage. */
|
||||
fun LibraryRating.homeRating(): HomeRating = HomeRating(
|
||||
mark = when (label) { "Fresh", "Certified Fresh" -> "fresh"; "Rotten" -> "rotten"; "Upright" -> "upright"; "Spilled" -> "spilled"; else -> when (source) { "rtCritics" -> "tomato"; "rtAudience" -> "popcorn"; else -> source } },
|
||||
score = score, percentage = source.startsWith("rt"), episode = source == "episode",
|
||||
)
|
||||
|
|
@ -7,7 +7,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.setValue
|
||||
|
||||
/** Household identity data contains no credentials or server objects. */
|
||||
data class ShellPerson(val key: String, val name: String, val initial: String, val hasAnime: Boolean)
|
||||
data class ShellPerson(val key: String, val name: String, val initial: String, val hasAnime: Boolean, val jellyfinUserId: String? = null)
|
||||
|
||||
/** The shell owns keys; the integration owns navigation and authenticated user changes. */
|
||||
sealed interface ShellAction {
|
||||
|
|
@ -42,10 +42,21 @@ class RememberedView {
|
|||
|
||||
/** Focus and Back transitions are independent of drawing and server availability. */
|
||||
class ShellState(
|
||||
val people: List<ShellPerson>,
|
||||
people: List<ShellPerson>,
|
||||
initialPerson: String,
|
||||
private val onAction: (ShellAction) -> Unit = {},
|
||||
) {
|
||||
var people by mutableStateOf(people)
|
||||
private set
|
||||
|
||||
/** Refresh household labels without discarding this person's navigation memory. */
|
||||
fun updatePeople(value: List<ShellPerson>) {
|
||||
val selected = value.firstOrNull { it.jellyfinUserId != null && it.jellyfinUserId == person.jellyfinUserId }
|
||||
?: value.firstOrNull { it.key == person.key }
|
||||
people = value
|
||||
if (selected != null) person = selected
|
||||
pickerIndex = people.indexOf(person).coerceAtLeast(0)
|
||||
}
|
||||
var person by mutableStateOf(people.firstOrNull { it.key == initialPerson } ?: people.first())
|
||||
private set
|
||||
var route by mutableStateOf("home")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue