0.2.99
This commit is contained in:
+1
-17
@@ -1,6 +1,4 @@
|
||||
package com.ponzischeme89.memby.ui.components.media
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -8,7 +6,6 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.ponzischeme89.memby.ServiceLocator
|
||||
import com.ponzischeme89.memby.data.model.BaseItem
|
||||
import com.ponzischeme89.memby.ui.ItemRatingsStrip
|
||||
import com.ponzischeme89.memby.ui.ResumableMediaCard
|
||||
import com.ponzischeme89.memby.ui.responsiveRowCardWidth
|
||||
import com.ponzischeme89.memby.ui.toResumableMediaCardModel
|
||||
@@ -56,19 +53,6 @@ fun ContinueWatchingCard(
|
||||
onFocused = onFocused,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = modifier,
|
||||
titleSupplement = if (item.isMovie) {
|
||||
{ focused ->
|
||||
ItemRatingsStrip(
|
||||
item = item,
|
||||
load = focused,
|
||||
modifier = Modifier.padding(top = 2.dp),
|
||||
reserveSpace = false,
|
||||
compact = true,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@@ -184,7 +184,9 @@ internal fun MediaRow(
|
||||
val previousPageFocusRequester = remember { FocusRequester() }
|
||||
val nextPageFocusRequester = remember { FocusRequester() }
|
||||
val viewAllFocusRequester = remember { FocusRequester() }
|
||||
val headerReturnFocusRequester = remember { FocusRequester() }
|
||||
val pagedCardFocusRequester = remember { FocusRequester() }
|
||||
var headerReturnItemId by remember(row.id) { mutableStateOf<String?>(null) }
|
||||
var pagedCardIndex by remember(row.id) { mutableStateOf<Int?>(null) }
|
||||
var pageFocusRequestId by remember(row.id) { mutableIntStateOf(0) }
|
||||
val requestedEntryIndex = verticalFocusRequest
|
||||
@@ -207,7 +209,7 @@ internal fun MediaRow(
|
||||
// first time — the frame budget on a Chromecast is not the one a warm row gets.
|
||||
repeat(6) {
|
||||
delay(16.milliseconds)
|
||||
if (runCatching { verticalEntryFocusRequester.requestFocus() }.isSuccess) {
|
||||
if (verticalEntryFocusRequester.requestFocusIfAttached()) {
|
||||
currentOnVerticalFocusRequestConsumed(request.requestId)
|
||||
return@LaunchedEffect
|
||||
}
|
||||
@@ -221,7 +223,7 @@ internal fun MediaRow(
|
||||
// attach it, then keep the transfer bounded so a refreshed row can never trap focus.
|
||||
repeat(6) {
|
||||
delay(16.milliseconds)
|
||||
if (runCatching { pagedCardFocusRequester.requestFocus() }.isSuccess) {
|
||||
if (pagedCardFocusRequester.requestFocusIfAttached()) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
}
|
||||
@@ -237,9 +239,22 @@ internal fun MediaRow(
|
||||
val pageSize = if (
|
||||
row.items.firstOrNull()?.let { cardFormat(row.kind, it, artworkStyle) } == MediaCardFormat.PORTRAIT
|
||||
) 6 else 4
|
||||
val hasViewAll = viewAllLabel != null && onViewAll != null
|
||||
val headerReturnTargetId = headerReturnItemId
|
||||
?.takeIf { focusedId -> row.items.any { it.id == focusedId } }
|
||||
?: row.items.firstOrNull()?.id
|
||||
fun requestHeaderFocus(): Boolean = when {
|
||||
hasViewAll -> viewAllFocusRequester.requestFocusIfAttached()
|
||||
canScrollBack -> previousPageFocusRequester.requestFocusIfAttached()
|
||||
canScrollForward -> nextPageFocusRequester.requestFocusIfAttached()
|
||||
else -> false
|
||||
}
|
||||
Column(modifier, verticalArrangement = Arrangement.spacedBy(HomeRowHeaderSpacing)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = HomeContentHorizontalInset),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = HomeContentHorizontalInset)
|
||||
.focusGroup(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val visual = homeRowVisual(row)
|
||||
@@ -257,6 +272,7 @@ internal fun MediaRow(
|
||||
ViewAllButton(
|
||||
label = viewAllLabel,
|
||||
focusRequester = viewAllFocusRequester,
|
||||
downFocusRequester = headerReturnFocusRequester,
|
||||
onClick = onViewAll,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
@@ -265,6 +281,7 @@ internal fun MediaRow(
|
||||
forward = false,
|
||||
enabled = canScrollBack,
|
||||
focusRequester = previousPageFocusRequester,
|
||||
downFocusRequester = headerReturnFocusRequester,
|
||||
onClick = {
|
||||
val target = (rowState.firstVisibleItemIndex - pageSize).coerceAtLeast(0)
|
||||
pagedCardIndex = target
|
||||
@@ -276,6 +293,7 @@ internal fun MediaRow(
|
||||
forward = true,
|
||||
enabled = canScrollForward,
|
||||
focusRequester = nextPageFocusRequester,
|
||||
downFocusRequester = headerReturnFocusRequester,
|
||||
onClick = {
|
||||
val target = (rowState.firstVisibleItemIndex + pageSize)
|
||||
.coerceAtMost(row.items.lastIndex)
|
||||
@@ -351,6 +369,9 @@ internal fun MediaRow(
|
||||
if (item.id == returnFocusItemId) {
|
||||
cardModifier = cardModifier.focusRequester(returnFocusRequester)
|
||||
}
|
||||
if (item.id == headerReturnTargetId) {
|
||||
cardModifier = cardModifier.focusRequester(headerReturnFocusRequester)
|
||||
}
|
||||
if (index == requestedEntryIndex) {
|
||||
cardModifier = cardModifier.focusRequester(verticalEntryFocusRequester)
|
||||
}
|
||||
@@ -362,14 +383,16 @@ internal fun MediaRow(
|
||||
return@onPreviewKeyEvent false
|
||||
}
|
||||
when (event.key) {
|
||||
Key.DirectionUp -> onMoveVertical(index, RowFocusDirection.UP)
|
||||
Key.DirectionUp -> {
|
||||
requestHeaderFocus() ||
|
||||
onMoveVertical(index, RowFocusDirection.UP)
|
||||
}
|
||||
Key.DirectionDown -> onMoveVertical(index, RowFocusDirection.DOWN)
|
||||
Key.DirectionLeft -> {
|
||||
val firstVisible = rowState.layoutInfo.visibleItemsInfo
|
||||
.firstOrNull()?.index
|
||||
if (canScrollBack && index == firstVisible) {
|
||||
runCatching { previousPageFocusRequester.requestFocus() }
|
||||
.isSuccess
|
||||
previousPageFocusRequester.requestFocusIfAttached()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -380,9 +403,9 @@ internal fun MediaRow(
|
||||
if (index != lastVisible) {
|
||||
false
|
||||
} else if (viewAllLabel != null && onViewAll != null) {
|
||||
runCatching { viewAllFocusRequester.requestFocus() }.isSuccess
|
||||
viewAllFocusRequester.requestFocusIfAttached()
|
||||
} else if (canScrollForward) {
|
||||
runCatching { nextPageFocusRequester.requestFocus() }.isSuccess
|
||||
nextPageFocusRequester.requestFocusIfAttached()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -391,6 +414,7 @@ internal fun MediaRow(
|
||||
}
|
||||
}
|
||||
val focused: () -> Unit = {
|
||||
headerReturnItemId = item.id
|
||||
onContentFocused()
|
||||
// LazyRow already knows the semantic position. Passing it on
|
||||
// avoids searching the row again on every D-pad focus move.
|
||||
@@ -434,6 +458,7 @@ private fun GalleryJumpButton(
|
||||
forward: Boolean,
|
||||
enabled: Boolean,
|
||||
focusRequester: FocusRequester,
|
||||
downFocusRequester: FocusRequester,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
FocusScaleContainer(
|
||||
@@ -445,7 +470,10 @@ private fun GalleryJumpButton(
|
||||
.width(42.dp)
|
||||
.height(36.dp)
|
||||
.focusRequester(focusRequester)
|
||||
.focusProperties { canFocus = enabled },
|
||||
.focusProperties {
|
||||
canFocus = enabled
|
||||
down = downFocusRequester
|
||||
},
|
||||
) { focused ->
|
||||
Box(
|
||||
Modifier
|
||||
@@ -479,6 +507,7 @@ private fun GalleryJumpButton(
|
||||
private fun ViewAllButton(
|
||||
label: String,
|
||||
focusRequester: FocusRequester,
|
||||
downFocusRequester: FocusRequester,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
FocusScaleContainer(
|
||||
@@ -487,7 +516,8 @@ private fun ViewAllButton(
|
||||
contentDescription = label,
|
||||
modifier = Modifier
|
||||
.height(36.dp)
|
||||
.focusRequester(focusRequester),
|
||||
.focusRequester(focusRequester)
|
||||
.focusProperties { down = downFocusRequester },
|
||||
) { focused ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -522,6 +552,9 @@ private fun ViewAllButton(
|
||||
}
|
||||
}
|
||||
|
||||
private fun FocusRequester.requestFocusIfAttached(): Boolean =
|
||||
runCatching { requestFocus() }.getOrDefault(false)
|
||||
|
||||
private enum class MediaCardFormat { PORTRAIT, LANDSCAPE }
|
||||
|
||||
private fun cardFormat(
|
||||
|
||||
@@ -438,13 +438,11 @@ class SearchViewModel(private val repository: EmbyRepository) : ViewModel() {
|
||||
// both routes is a shape this search genuinely has. The result list is keyed by
|
||||
// item id, where that is a crash rather than a repeated poster.
|
||||
val items = found.distinctItems()
|
||||
// Gateway payloads carry the backend ranker's score. Preserve that
|
||||
// ordering exactly; direct-to-Emby mode keeps the local textual fallback.
|
||||
val ranked = if (items.any { it.membyRecommendationScore != null }) {
|
||||
items
|
||||
} else {
|
||||
rankSearchResults(term, items)
|
||||
}
|
||||
// Keep the backend's relevance and personalisation order within each
|
||||
// textual tier, but never let those softer signals bury the title the
|
||||
// viewer typed exactly. This matters especially in the one-column list,
|
||||
// where tenth place is ten rows away rather than the second grid row.
|
||||
val ranked = rankSearchResults(term, items)
|
||||
cache[term] = ranked
|
||||
viewModelScope.launch { repository.recordSearch(term) }
|
||||
_state.update {
|
||||
|
||||
Reference in New Issue
Block a user