diff --git a/Satsamp Icon.png b/Satsamp Icon.png new file mode 100644 index 0000000..4237cb2 Binary files /dev/null and b/Satsamp Icon.png differ diff --git a/app/src/main/java/com/rada/ui/library/LibraryScreen.kt b/app/src/main/java/com/rada/ui/library/LibraryScreen.kt index 55ccca1..3c0ab77 100644 --- a/app/src/main/java/com/rada/ui/library/LibraryScreen.kt +++ b/app/src/main/java/com/rada/ui/library/LibraryScreen.kt @@ -1,5 +1,6 @@ package com.rada.ui.library +import androidx.compose.animation.Crossfade import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background @@ -284,14 +285,18 @@ fun LibraryScreen( .padding(horizontal = 16.dp) .padding(top = 4.dp, bottom = 2.dp), ) { - Text( - text = headerTitle, - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - color = LocalTokens.current.text, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + // Crossfade the header when the selected folder changes — a + // hard text swap reads as a glitch when tabs are tapped. + Crossfade(targetState = headerTitle, label = "libraryHeader") { t -> + Text( + text = t, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = LocalTokens.current.text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } Text( text = "${uiState.songs.size} songs", style = MaterialTheme.typography.bodySmall, @@ -477,8 +482,12 @@ fun LibraryScreen( modifier = Modifier.fillMaxSize(), ) { items(uiState.songs, key = { it.id }) { song -> + // Keyed rows animate in/out when the folder tab or + // search filter changes instead of popping. + val rowMod = if (LocalReduceMotion.current) Modifier else Modifier.animateItem() SongListItem( song = song, + modifier = rowMod, isPlaying = song.id == uiState.currentPlayingId, onClick = { viewModel.playSong(song) }, onToggleFavorite = { viewModel.toggleFavorite(song.id) }, @@ -768,10 +777,11 @@ private fun SongListItem( onToggleFavorite: () -> Unit, onAddToPlaylist: () -> Unit, onDelete: () -> Unit, + modifier: Modifier = Modifier, ) { val interaction = remember { MutableInteractionSource() } Row( - modifier = Modifier + modifier = modifier .fillMaxWidth() .pressDip(interaction, LocalReduceMotion.current) .clickable(interactionSource = interaction, indication = androidx.compose.foundation.LocalIndication.current, onClick = onClick) diff --git a/app/src/main/java/com/rada/ui/navigation/TabsScreen.kt b/app/src/main/java/com/rada/ui/navigation/TabsScreen.kt index feb5d55..3f7414a 100644 --- a/app/src/main/java/com/rada/ui/navigation/TabsScreen.kt +++ b/app/src/main/java/com/rada/ui/navigation/TabsScreen.kt @@ -30,6 +30,7 @@ import com.rada.ui.player.NowPlayingBar import com.rada.ui.playlists.PlaylistManagerScreen import com.rada.ui.stations.StationBrowserScreen import com.rada.ui.streams.StreamsScreen +import com.rada.ui.theme.LocalReduceMotion import com.rada.ui.theme.LocalTokens import kotlinx.coroutines.launch @@ -49,14 +50,26 @@ fun TabsScreen( ) { val pagerState = rememberPagerState(initialPage = initialIndex.coerceIn(0, 4)) { 5 } val scope = rememberCoroutineScope() + val reduceMotion = LocalReduceMotion.current // Deep-link target for Library (playlist picker → open that playlist). var libraryPlaylistId by remember { mutableStateOf(initialPlaylistId) } - // Tab taps land immediately. Swiping remains animated, but animating a - // tap composes and disposes heavy pages while the gesture is in flight. + // Adjacent tabs glide (neighbors stay composed, so it's smooth); far + // taps jump instantly — animating a multi-page hop composes heavy pages + // mid-flight and reintroduces the jank the jump was there to avoid. fun goToPage(page: Int) { if (pagerState.currentPage == page) return scope.launch { - pagerState.scrollToPage(page) + if (kotlin.math.abs(pagerState.currentPage - page) == 1 && !reduceMotion) { + pagerState.animateScrollToPage( + page, + animationSpec = androidx.compose.animation.core.tween( + durationMillis = 260, + easing = com.rada.ui.theme.MotionArrival, + ), + ) + } else { + pagerState.scrollToPage(page) + } } } diff --git a/app/src/main/java/com/rada/ui/theme/Motion.kt b/app/src/main/java/com/rada/ui/theme/Motion.kt index 709aec3..28df9ed 100644 --- a/app/src/main/java/com/rada/ui/theme/Motion.kt +++ b/app/src/main/java/com/rada/ui/theme/Motion.kt @@ -1,6 +1,7 @@ package com.rada.ui.theme import androidx.compose.ui.unit.dp +import androidx.compose.runtime.Composable import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState @@ -130,12 +131,15 @@ fun feedbackDuration(reduce: Boolean): Int = if (reduce) 0 else FeedbackMs /** * Press-dip for list rows: scales to 0.97 while pressed. Use on rows that * already have a clickable/combinedClickable modifier earlier in the chain. + * (A plain @Composable extension — the old composed{} form spun up an extra + * subcomposition per row, wasted recomposition churn in long lists.) */ +@Composable fun Modifier.pressDip( interactionSource: MutableInteractionSource, reduce: Boolean, -): Modifier = composed { - if (reduce) return@composed this +): Modifier { + if (reduce) return this val pressed by interactionSource.collectIsPressedAsState() val scale by animateFloatAsState( targetValue = if (pressed) 0.97f else 1f, @@ -145,7 +149,7 @@ fun Modifier.pressDip( ), label = "pressDip", ) - this.graphicsLayer { scaleX = scale; scaleY = scale } + return this.graphicsLayer { scaleX = scale; scaleY = scale } } // ── Dismiss-by-drag ─────────────────────────────────────────────────────── diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index 1c0f03c..7f63d63 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..7f63d63 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png index 1c3771c..5d2061f 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..5d2061f Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 930a353..5b9375b 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..5b9375b Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 07bd17b..dd56295 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..dd56295 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 21ea8c2..98c0ee5 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..98c0ee5 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ