feat(android): artist fast-scroll index bar with letter bubble
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
dd484df636
commit
9ffd071c58
1 file changed
+97
-2
@@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
@@ -20,9 +21,12 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.input.pointer.PointerEventType
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -37,6 +41,7 @@ import com.novasonic.app.ui.theme.*
|
|||||||
import com.novasonic.app.viewmodel.AlbumSort
|
import com.novasonic.app.viewmodel.AlbumSort
|
||||||
import com.novasonic.app.viewmodel.LibraryTab
|
import com.novasonic.app.viewmodel.LibraryTab
|
||||||
import com.novasonic.app.viewmodel.LibraryViewModel
|
import com.novasonic.app.viewmodel.LibraryViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryScreen(
|
fun LibraryScreen(
|
||||||
@@ -145,8 +150,26 @@ fun LibraryScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LibraryTab.ARTISTS -> LazyColumn(
|
LibraryTab.ARTISTS -> {
|
||||||
contentPadding = PaddingValues(vertical = 8.dp),
|
val listState = rememberLazyListState()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val letters = remember(state.artistIndexes) { state.artistIndexes.map { it.name } }
|
||||||
|
// Map letter → flat LazyColumn index of its header item
|
||||||
|
val letterToIndex = remember(state.artistIndexes) {
|
||||||
|
buildMap<String, Int> {
|
||||||
|
var flat = 0
|
||||||
|
state.artistIndexes.forEach { idx ->
|
||||||
|
put(idx.name, flat)
|
||||||
|
flat += 1 + idx.artist.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var activeLetter by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
contentPadding = PaddingValues(top = 8.dp, bottom = 8.dp, end = 28.dp),
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
state.artistIndexes.forEach { idx ->
|
state.artistIndexes.forEach { idx ->
|
||||||
@@ -186,6 +209,78 @@ fun LibraryScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (letters.isNotEmpty()) {
|
||||||
|
// Bubble showing active letter while dragging
|
||||||
|
activeLetter?.let { letter ->
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterEnd)
|
||||||
|
.offset(x = (-44).dp)
|
||||||
|
.size(52.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(NsAccent),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = letter,
|
||||||
|
fontSize = 22.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = NsBackground,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical letter strip
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterEnd)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.width(28.dp)
|
||||||
|
.pointerInput(letters) {
|
||||||
|
awaitPointerEventScope {
|
||||||
|
while (true) {
|
||||||
|
val event = awaitPointerEvent()
|
||||||
|
val y = event.changes.firstOrNull()?.position?.y ?: continue
|
||||||
|
val idx = ((y / size.height) * letters.size)
|
||||||
|
.toInt().coerceIn(0, letters.lastIndex)
|
||||||
|
val letter = letters[idx]
|
||||||
|
when (event.type) {
|
||||||
|
PointerEventType.Press,
|
||||||
|
PointerEventType.Move -> {
|
||||||
|
event.changes.forEach { it.consume() }
|
||||||
|
if (letter != activeLetter) {
|
||||||
|
activeLetter = letter
|
||||||
|
letterToIndex[letter]?.let { pos ->
|
||||||
|
scope.launch { listState.scrollToItem(pos) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> activeLetter = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
letters.forEach { letter ->
|
||||||
|
Text(
|
||||||
|
text = letter,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = if (letter == activeLetter) FontWeight.Bold else FontWeight.Normal,
|
||||||
|
color = if (letter == activeLetter) NsAccent else NsOnSurface3,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LibraryTab.PLAYLISTS -> LazyVerticalGrid(
|
LibraryTab.PLAYLISTS -> LazyVerticalGrid(
|
||||||
columns = GridCells.Adaptive(minSize = 150.dp),
|
columns = GridCells.Adaptive(minSize = 150.dp),
|
||||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 80.dp),
|
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 80.dp),
|
||||||
|
|||||||
Reference in new issue
Block a user