Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.theveloper.pixelplay.presentation.components

import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.runtime.*
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.ui.Modifier
Expand All @@ -21,8 +24,6 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.first

import com.theveloper.pixelplay.data.preferences.AlbumArtQuality
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MotionScheme

// ====== TIPOS/STATE DEL CARRUSEL (wrapper para mantener compatibilidad) ======

Expand All @@ -35,7 +36,7 @@ fun rememberRoundedParallaxCarouselState(

// ====== TU SECCIÓN: ACOPLADA AL NUEVO API ======

@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AlbumCarouselSection(
currentSong: Song?,
Expand Down Expand Up @@ -66,8 +67,17 @@ fun AlbumCarouselSection(
pageCount = { queue.size }
)

val motionScheme = remember { MotionScheme.expressive() }
val carouselAnimationSpec = remember { motionScheme.defaultSpatialSpec<Float>() }
// Snappy, critically-damped (no-overshoot) spring for programmatic skip scrolls.
// The previous MotionScheme.expressive().defaultSpatialSpec was a slow, bouncy spring whose
// long settle tail produced many frames of per-item measure/clip work, reading as "laggy".
// A no-bounce spring snaps in and settles fast, matching the native feel of the open/close
// animation while cutting the number of expensive carousel frames.
val carouselAnimationSpec = remember {
spring<Float>(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = Spring.StiffnessMediumLow
)
}

// Calculate target size based on quality
val targetSize = remember(albumArtQuality) {
Expand Down Expand Up @@ -99,12 +109,35 @@ fun AlbumCarouselSection(
targetSize = targetSize,
anchorIndex = effectiveTargetIndex
)
var ignoreNextSettledSelectionForPage by remember { mutableStateOf<Int?>(null) }
var programmaticScrollInProgress by remember { mutableStateOf(false) }
var lastSettledSongId by remember { mutableStateOf(currentSong?.id) }

// Track whether the *user* is actively dragging the carousel (via the pager's interaction
// source) so we can tell a real swipe apart from our own programmatic skip/seek scrolls.
var isUserDragging by remember { mutableStateOf(false) }
var userDragSettlePending by remember { mutableStateOf(false) }
LaunchedEffect(carouselState) {
carouselState.pagerState.interactionSource.interactions.collect { interaction ->
when (interaction) {
is DragInteraction.Start -> {
isUserDragging = true
userDragSettlePending = true
}
is DragInteraction.Stop, is DragInteraction.Cancel -> isUserDragging = false
}
}
}

// Player -> Carousel. Retargets continuously: each press cancels the in-flight animation and
// animates from the current position to the new target, so a rapid skip burst scrolls smoothly
// through the albums instead of freezing. We only ever defer to an active *user* drag — never
// to our own (cancelled) programmatic scroll, which is what previously caused the freeze: the
// effect restarted on every press and then blocked waiting for the just-cancelled scroll to go
// idle, so the carousel never advanced until the user stopped tapping.
LaunchedEffect(effectiveTargetIndex, requestedTargetIndex, queue) {
snapshotFlow { carouselState.pagerState.isScrollInProgress }
.first { !it }
if (isUserDragging) {
snapshotFlow { isUserDragging }.first { !it }
}

val currentPage = carouselState.pagerState.currentPage
if (currentPage != effectiveTargetIndex) {
Expand All @@ -117,9 +150,6 @@ fun AlbumCarouselSection(
// and avoid showing the wrong item for the duration of an animation.
carouselState.pagerState.scrollToPage(effectiveTargetIndex)
} else {
if (requestedTargetIndex != null) {
ignoreNextSettledSelectionForPage = effectiveTargetIndex
}
programmaticScrollInProgress = true
try {
carouselState.animateScrollToItem(effectiveTargetIndex, animationSpec = carouselAnimationSpec)
Expand All @@ -138,11 +168,13 @@ fun AlbumCarouselSection(
.distinctUntilChanged()
.filter { !it }
.collect {
// Only a settle that followed a real user drag changes the song. Programmatic
// skip/seek scrolls (and their mid-burst cancellations) must not feed back into
// the player — it already drove them — otherwise a rapid skip burst would fire
// spurious selections and fight the optimistic carousel index.
if (!userDragSettlePending) return@collect
userDragSettlePending = false
val settled = carouselState.pagerState.currentPage
if (ignoreNextSettledSelectionForPage == settled) {
ignoreNextSettledSelectionForPage = null
return@collect
}
if (settled != currentSongIndex) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
queue.getOrNull(settled)?.let { onSongSelected(it, settled) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import com.theveloper.pixelplay.ui.theme.GoogleSansRounded
import com.theveloper.pixelplay.ui.theme.LocalShowScrollbar
import androidx.compose.foundation.combinedClickable
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.distinctUntilChanged

@androidx.annotation.OptIn(UnstableApi::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
Expand Down Expand Up @@ -239,7 +240,11 @@ fun PlaylistItems(
onPlaylistLongPress: (Playlist) -> Unit = {},
onPlaylistSelectionToggle: (Playlist) -> Unit = {}
) {
val stablePlayerState by playerViewModel.stablePlayerState.collectAsStateWithLifecycle()
val hasCurrentSong by remember(playerViewModel) {
playerViewModel.stablePlayerState
.map { it.currentSong != null && it.currentSong != Song.emptySong() }
.distinctUntilChanged()
}.collectAsStateWithLifecycle(initialValue = false)
val listState = rememberLazyListState()
val playlistFastScrollLabelProvider = remember(filteredPlaylists, currentSortOption) {
{ index: Int ->
Expand Down Expand Up @@ -318,7 +323,7 @@ fun PlaylistItems(
}
}

val bottomPadding = if (stablePlayerState.currentSong != null && stablePlayerState.currentSong != Song.emptySong())
val bottomPadding = if (hasCurrentSong)
bottomBarHeight + MiniPlayerHeight + 16.dp
else
bottomBarHeight + 16.dp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,9 @@ private fun rememberBatchAnimatedColorScheme(target: ColorScheme): ColorScheme {
progress.snapTo(0f)
progress.animateTo(
targetValue = 1f,
animationSpec = spring(stiffness = Spring.StiffnessLow)
// Snappier than the old StiffnessLow so the palette resolves in step with the
// (now faster) carousel skip animation instead of trailing behind it.
animationSpec = spring(stiffness = Spring.StiffnessMediumLow)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import androidx.compose.ui.text.style.TextOverflow

@androidx.annotation.OptIn(UnstableApi::class)
Expand All @@ -93,6 +94,12 @@ fun LibraryAlbumsTab(
getSelectionIndex: (Long) -> Int? = { null },
storageFilter: StorageFilter = StorageFilter.ALL
) {
val hasCurrentSong by remember(playerViewModel) {
playerViewModel.stablePlayerState
.map { it.currentSong != null && it.currentSong != Song.emptySong() }
.distinctUntilChanged()
}.collectAsStateWithLifecycle(initialValue = false)

val gridState = rememberLazyGridState()
val listState = rememberLazyListState()
val dummyListState = rememberLazyListState()
Expand Down Expand Up @@ -372,8 +379,7 @@ fun LibraryAlbumsTab(
}
}
}
val stablePlayerState by playerViewModel.stablePlayerState.collectAsStateWithLifecycle()
val bottomPadding = if (stablePlayerState.currentSong != null && stablePlayerState.currentSong != Song.emptySong())
val bottomPadding = if (hasCurrentSong)
bottomBarHeight + MiniPlayerHeight + 16.dp
else
bottomBarHeight + 16.dp
Expand Down Expand Up @@ -444,8 +450,7 @@ fun LibraryAlbumsTab(
}
}

val stablePlayerState by playerViewModel.stablePlayerState.collectAsStateWithLifecycle()
val bottomPadding = if (stablePlayerState.currentSong != null && stablePlayerState.currentSong != Song.emptySong())
val bottomPadding = if (hasCurrentSong)
bottomBarHeight + MiniPlayerHeight + 16.dp
else
bottomBarHeight + 16.dp
Expand Down Expand Up @@ -478,6 +483,12 @@ fun LibraryArtistsTab(
onRefresh: () -> Unit,
storageFilter: StorageFilter = StorageFilter.ALL
) {
val hasCurrentSong by remember(playerViewModel) {
playerViewModel.stablePlayerState
.map { it.currentSong != null && it.currentSong != Song.emptySong() }
.distinctUntilChanged()
}.collectAsStateWithLifecycle(initialValue = false)

val listState = rememberLazyListState()
val dummyListState = rememberLazyListState()
val artistFastScrollLabelProvider = remember(artists, currentArtistSortOption) {
Expand Down Expand Up @@ -633,8 +644,7 @@ fun LibraryArtistsTab(
}
}

val stablePlayerState by playerViewModel.stablePlayerState.collectAsStateWithLifecycle()
val bottomPadding = if (stablePlayerState.currentSong != null && stablePlayerState.currentSong != Song.emptySong())
val bottomPadding = if (hasCurrentSong)
bottomBarHeight + MiniPlayerHeight + 16.dp
else
bottomBarHeight + 16.dp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1108,10 +1108,6 @@ fun LibraryScreen(
}
}
}
val allSongsLazyPagingItems = libraryViewModel.songsPagingFlow.collectAsLazyPagingItems()
val albumsLazyPagingItems = libraryViewModel.albumsPagingFlow.collectAsLazyPagingItems()
val artistsLazyPagingItems = libraryViewModel.artistsPagingFlow.collectAsLazyPagingItems()
val favoritePagingItems = libraryViewModel.favoritesPagingFlow.collectAsLazyPagingItems()
val isLibraryLoading by libraryViewModel.isLoadingLibrary.collectAsStateWithLifecycle()
val hasCurrentSong by remember(playerViewModel) {
playerViewModel.stablePlayerState
Expand Down Expand Up @@ -1230,7 +1226,11 @@ fun LibraryScreen(
onSelectAll = {
when (tabTitles.getOrNull(currentTabIndex)?.toLibraryTabIdOrNull()) {
LibraryTabId.LIKED -> {
multiSelectionState.selectAll(favoritePagingItems.itemSnapshotList.items)
scope.launch {
val songsToSelect =
playerViewModel.getSongsForCurrentFavoriteSelection()
multiSelectionState.selectAll(songsToSelect)
}
}
LibraryTabId.FOLDERS -> {
val songsToSelect =
Expand Down Expand Up @@ -1514,6 +1514,7 @@ fun LibraryScreen(
)
when (tabTitles.getOrNull(tabIndex)?.toLibraryTabIdOrNull()) {
LibraryTabId.SONGS -> {
val allSongsLazyPagingItems = libraryViewModel.songsPagingFlow.collectAsLazyPagingItems()
LibrarySongsTab(
songs = allSongsLazyPagingItems,
isLoading = isLibraryLoading,
Expand All @@ -1538,6 +1539,7 @@ fun LibraryScreen(
)
}
LibraryTabId.ALBUMS -> {
val albumsLazyPagingItems = libraryViewModel.albumsPagingFlow.collectAsLazyPagingItems()
val isLoading = playerUiState.isLoadingLibraryCategories

val stableOnAlbumClick: (Long) -> Unit = remember(navController) {
Expand Down Expand Up @@ -1568,6 +1570,7 @@ fun LibraryScreen(
}

LibraryTabId.ARTISTS -> {
val artistsLazyPagingItems = libraryViewModel.artistsPagingFlow.collectAsLazyPagingItems()
val isLoading = playerUiState.isLoadingLibraryCategories

LibraryArtistsTab(
Expand Down Expand Up @@ -1607,6 +1610,7 @@ fun LibraryScreen(
}

LibraryTabId.LIKED -> {
val favoritePagingItems = libraryViewModel.favoritesPagingFlow.collectAsLazyPagingItems()
LibraryFavoritesTab(
favoriteSongs = favoritePagingItems,
playerViewModel = playerViewModel,
Expand Down Expand Up @@ -1634,15 +1638,14 @@ fun LibraryScreen(
val folders = playerUiState.musicFolders
val currentFolder = playerUiState.currentFolder
val isLoading = playerUiState.isLoadingLibraryCategories
val stablePlayerState by playerViewModel.stablePlayerState.collectAsStateWithLifecycle()
val defaultFolderName = stringResource(R.string.presentation_batch_d_folder_name_fallback)

LibraryFoldersTab(
folders = folders,
currentFolder = currentFolder,
isLoading = isLoading,
bottomBarHeight = bottomBarHeightDp,
stablePlayerState = stablePlayerState,
playerViewModel = playerViewModel,
onNavigateBack = { playerViewModel.navigateBackFolder() },
onFolderClick = { folderPath -> playerViewModel.navigateToFolder(folderPath) },
onFolderAsPlaylistClick = { folder ->
Expand Down Expand Up @@ -2851,7 +2854,7 @@ fun LibraryFoldersTab(
onFolderClick: (String) -> Unit,
onFolderAsPlaylistClick: (MusicFolder) -> Unit,
onPlaySong: (Song, List<Song>) -> Unit,
stablePlayerState: StablePlayerState,
playerViewModel: PlayerViewModel,
bottomBarHeight: Dp,
onMoreOptionsClick: (Song) -> Unit,
isPlaylistView: Boolean = false,
Expand Down Expand Up @@ -2917,14 +2920,24 @@ fun LibraryFoldersTab(
val songsToShow = remember(activeFolder, currentSortOption) {
sortSongsForFolderView(activeFolder?.songs ?: emptyList(), currentSortOption)
}.toImmutableList()
val currentSong = stablePlayerState.currentSong
val currentSong by remember(playerViewModel) {
playerViewModel.stablePlayerState
.map { it.currentSong }
.distinctUntilChanged()
}.collectAsStateWithLifecycle(initialValue = null)

val currentSongId = currentSong?.id
val currentSongIndexInSongs = remember(songsToShow, currentSongId) {
currentSongId?.let { songId -> songsToShow.indexOfFirst { it.id == songId } } ?: -1
}
val currentSongListIndex = remember(itemsToShow.size, currentSongIndexInSongs) {
if (currentSongIndexInSongs < 0) -1 else itemsToShow.size + currentSongIndexInSongs
}
val hasCurrentSong by remember(playerViewModel) {
playerViewModel.stablePlayerState
.map { it.currentSong != null && it.currentSong != Song.emptySong() }
.distinctUntilChanged()
}.collectAsStateWithLifecycle(initialValue = false)
val songInCurrentFolder = currentSongIndexInSongs >= 0
val currentSongParentPath: String? = remember(currentSong?.path) {
currentSong?.path
Expand Down Expand Up @@ -3097,15 +3110,14 @@ fun LibraryFoldersTab(
}

items(songsToShow, key = { it.id }, contentType = { "song" }) { song ->
EnhancedSongListItem(
LibraryPlaybackAwareSongItem(
song = song,
isPlaying = stablePlayerState.currentSong?.id == song.id && stablePlayerState.isPlaying,
isCurrentSong = stablePlayerState.currentSong?.id == song.id,
onMoreOptionsClick = { onMoreOptionsClick(song) },
playerViewModel = playerViewModel,
isSelected = selectedSongIds.contains(song.id),
selectionIndex = if (isSelectionMode) getSelectionIndex(song.id) else null,
isSelectionMode = isSelectionMode,
onLongPress = { onSongLongPress(song) },
onMoreOptionsClick = { onMoreOptionsClick(song) },
onClick = {
if (isSelectionMode) {
onSongSelectionToggle(song)
Expand All @@ -3118,7 +3130,7 @@ fun LibraryFoldersTab(
}

// ScrollBar Overlay
val bottomPadding = if (stablePlayerState.currentSong != null && stablePlayerState.currentSong != Song.emptySong())
val bottomPadding = if (hasCurrentSong)
bottomBarHeight + MiniPlayerHeight + 16.dp
else
bottomBarHeight + 16.dp
Expand Down
Loading
Loading