diff --git a/.gitignore b/.gitignore index 7072cb9..9a7f823 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ .externalNativeBuild .cxx local.properties +/app/release # Signing / secrets *.jks diff --git a/app/src/main/java/com/keylightpiano/pianotimeline/ui/RecordingItem.kt b/app/src/main/java/com/keylightpiano/pianotimeline/ui/RecordingItem.kt index 4c38e50..b2b000f 100644 --- a/app/src/main/java/com/keylightpiano/pianotimeline/ui/RecordingItem.kt +++ b/app/src/main/java/com/keylightpiano/pianotimeline/ui/RecordingItem.kt @@ -1,5 +1,8 @@ package com.keylightpiano.pianotimeline.ui +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.basicMarquee @@ -31,10 +34,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -42,6 +47,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.keylightpiano.pianotimeline.R import com.keylightpiano.pianotimeline.domain.Recording +import kotlinx.coroutines.launch private val NotesGreen = Color(0xFF25B300) private val NewBadgeRed = Color(0xFFE53935) @@ -145,15 +151,38 @@ fun RecordingItem( } } - // Favorite star - IconButton(onClick = onFavoriteClick, modifier = Modifier.size(36.dp)) { + // Favorite star with a spring "pop" on each tap. + val starScale = remember { Animatable(1f) } + val starScope = rememberCoroutineScope() + IconButton( + onClick = { + onFavoriteClick() + starScope.launch { + // Pop up then settle back with a springy overshoot. + starScale.snapTo(0.6f) + starScale.animateTo( + targetValue = 1f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium + ) + ) + } + }, + modifier = Modifier.size(36.dp) + ) { Icon( imageVector = if (recording.isFavorite) Icons.Filled.Star else Icons.Outlined.StarBorder, contentDescription = stringResource(if (recording.isFavorite) R.string.cd_unfavorite else R.string.cd_favorite), tint = if (recording.isFavorite) Color(0xFFFFC107) else MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(20.dp) + modifier = Modifier + .size(20.dp) + .graphicsLayer { + scaleX = starScale.value + scaleY = starScale.value + } ) } diff --git a/app/src/main/java/com/keylightpiano/pianotimeline/ui/RecordingListScreen.kt b/app/src/main/java/com/keylightpiano/pianotimeline/ui/RecordingListScreen.kt index 454c9e6..da885ba 100644 --- a/app/src/main/java/com/keylightpiano/pianotimeline/ui/RecordingListScreen.kt +++ b/app/src/main/java/com/keylightpiano/pianotimeline/ui/RecordingListScreen.kt @@ -51,7 +51,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -111,20 +110,13 @@ fun RecordingListScreen( wasRefreshing = isRefreshing } - // Preserve scroll position across a favorite toggle (which reorders the list). - // When the user stars/unstars an item, we snapshot the current top item's - // index+offset, then restore it after the list settles so the viewport - // doesn't jump to follow the moved item. - val coroutineScope = rememberCoroutineScope() + // Favoriting reorders the list (favorites float to the top). We let the + // LazyColumn's animateItem() handle the visual feedback — the toggled card + // visibly slides to its new position rather than vanishing under the finger. + // We intentionally do NOT force-restore scroll here; the viewport stays put + // while items animate to their new spots. val onFavoriteToggle: (Recording) -> Unit = { recording -> - val anchorIndex = listState.firstVisibleItemIndex - val anchorOffset = listState.firstVisibleItemScrollOffset vm.toggleFavorite(recording) - coroutineScope.launch { - // Wait one frame for the reordered list to be applied, then restore. - withFrameNanos { } - listState.scrollToItem(anchorIndex, anchorOffset) - } } // Deep link: when the VM requests scrolling to a specific entry, find its @@ -202,7 +194,7 @@ fun RecordingListScreen( Icon( imageVector = Icons.Default.Search, contentDescription = stringResource(R.string.cd_search_filter), - tint = if (hasActiveFilter) MaterialTheme.colorScheme.primary + tint = if (hasActiveFilter || showControls) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -418,7 +410,8 @@ fun RecordingListScreen( } else null, onCopyLink = { copyLink(context, vm.audioShareUrl(recording)) - } + }, + modifier = Modifier.animateItem() ) } }