Scroll to current chapter on playback overlay (#964)

## Description
This PR makes it so when you open the playback overlay and move down to
the "Chapters" row, the first focused chapter will be the current one
instead of always the first one.

### Related issues
Closes #695
Closes #951

### Testing
Emulator

## Screenshots
N/A

## AI or LLM usage
None
This commit is contained in:
Ray 2026-02-23 18:52:38 -05:00 committed by GitHub
parent 790bb4b535
commit 3e2a1869ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 51 additions and 8 deletions

View file

@ -31,6 +31,7 @@ data class Chapter(
)
},
)
}.orEmpty()
}?.sortedBy { it.position }
.orEmpty()
}
}

View file

@ -24,6 +24,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -35,6 +38,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged
@ -254,8 +258,34 @@ fun PlaybackOverlay(
exit = slideOutVertically { it / 2 } + fadeOut(),
) {
if (chapters.isNotEmpty()) {
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val chapterIndex =
remember {
val position = playerControls.currentPosition.milliseconds
val index =
chapters
.indexOfFirst { it.position > position }
.minus(1)
.let {
if (it < 0) {
// Didn't find a chapter, so it's either the first or last
if (position < chapters.first().position) {
0
} else {
chapters.lastIndex
}
} else {
it
}
}.coerceIn(0, chapters.lastIndex)
index
}
val listState = rememberLazyListState(chapterIndex)
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
LaunchedEffect(Unit) {
bringIntoViewRequester.bringIntoView()
focusRequester.tryRequestFocus()
}
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier =
@ -276,6 +306,7 @@ fun PlaybackOverlay(
style = MaterialTheme.typography.titleLarge,
)
LazyRow(
state = listState,
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier =
@ -305,9 +336,18 @@ fun PlaybackOverlay(
},
interactionSource = interactionSource,
modifier =
Modifier.ifElse(
Modifier
.ifElse(
index == chapterIndex,
Modifier
.focusRequester(focusRequester)
.bringIntoViewRequester(bringIntoViewRequester),
).ifElse(
index == 0,
Modifier.focusRequester(focusRequester),
Modifier.focusProperties {
// Prevent scrolling left on first card to prevent moving down
left = FocusRequester.Cancel
},
),
)
}
@ -448,8 +488,10 @@ fun PlaybackOverlay(
style = MaterialTheme.typography.labelLarge,
modifier =
Modifier
.background(Color.Black.copy(alpha = 0.6f), shape = RoundedCornerShape(4.dp))
.padding(horizontal = 8.dp, vertical = 4.dp),
.background(
Color.Black.copy(alpha = 0.6f),
shape = RoundedCornerShape(4.dp),
).padding(horizontal = 8.dp, vertical = 4.dp),
)
}
}