From c10c9bb3fc4d9bf105bab6cd1913ac7e753f55ed Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 20 Jul 2026 17:40:30 +0600 Subject: [PATCH] feat: implement CollectionView component and refactor Album, Playlist, SavedTracks screens for improved UI consistency --- .../core/ui/component/CollectionDetails.kt | 98 ----- .../core/ui/component/CollectionView.kt | 257 +++++++++++++ .../spotube/core/ui/component/TrackList.kt | 16 - .../spotube/modules/album/AlbumScreen.kt | 228 ++++------- .../modules/playlist/PlaylistScreen.kt | 356 +++++++----------- .../modules/saved_tracks/SavedTracksScreen.kt | 213 ++++------- 6 files changed, 529 insertions(+), 639 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionView.kt diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionDetails.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionDetails.kt index 7c0c73d8..7f4335b5 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionDetails.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionDetails.kt @@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -51,7 +50,6 @@ import dev.krtirtho.spotube.core.ui.base.OutlineButton import dev.krtirtho.spotube.core.ui.base.PrimaryButton import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer import dev.krtirtho.spotube.core.ui.misc.shimmerApply -import dev.krtirtho.spotube.resources.iconsax.ArrowLeft3 import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare import dev.krtirtho.spotube.resources.iconsax.IconsaxEdit @@ -319,102 +317,6 @@ fun CollectionDetails( } } -@OptIn(ExperimentalSharedTransitionApi::class) -@Composable -fun CollapsedCollectionHeader( - title: String, - imageURL: String, - imageResource: DrawableResource? = null, - isPlaying: Boolean, - onPlay: () -> Unit, - modifier: Modifier = Modifier, - onBack: (() -> Unit)? = null, - sharedElementKey: String? = null, -) { - val sharedTransitionScope = LocalSharedTransitionScope.current - val animatedVisibilityScope = LocalAnimatedVisibilityScope.current - Card( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 4.dp), - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp), - ) { - if (onBack != null) { - IconButton( - onClick = onBack, - modifier = Modifier.size(36.dp), - ) { - Icon( - imageVector = Iconsax.ArrowLeft3, - contentDescription = "Back", - modifier = Modifier.size(20.dp), - ) - } - } - - Box( - modifier = Modifier - .size(36.dp) - .clip(RoundedCornerShape(6.dp)) - .sharedElementOrNone( - key = sharedElementKey, - sharedTransitionScope = sharedTransitionScope, - animatedVisibilityScope = animatedVisibilityScope, - ) - .shimmerApply(), - contentAlignment = Alignment.Center, - ) { - if (imageURL.isNotBlank()) { - AsyncImage( - model = imageURL, - contentDescription = title, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, - ) - } else if (imageResource != null) { - androidx.compose.foundation.Image( - painter = painterResource(imageResource), - contentDescription = title, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, - ) - } else { - Text( - text = title.take(1).ifBlank { "?" }.uppercase(), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - - TextWithShimmer( - text = title, - style = MaterialTheme.typography.titleSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f), - ) - - IconButton( - onClick = onPlay, - modifier = Modifier.size(36.dp), - ) { - Icon( - imageVector = if (isPlaying) Iconsax.IconsaxPauseCircle else Iconsax.IconsaxPlayCircle2, - contentDescription = if (isPlaying) "Pause" else "Play", - modifier = Modifier.size(24.dp), - ) - } - } - } -} - @Composable @androidx.compose.ui.tooling.preview.Preview fun CollectionDetailsPreview() { diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionView.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionView.kt new file mode 100644 index 00000000..02082d96 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionView.kt @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package dev.krtirtho.spotube.core.ui.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.album.MetadataAlbum +import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist +import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack +import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer +import dev.krtirtho.spotube.core.ui.misc.shimmerApply +import dev.krtirtho.spotube.resources.iconsax.Iconsax +import dev.krtirtho.spotube.resources.iconsax.IconsaxPauseCircle +import dev.krtirtho.spotube.resources.iconsax.IconsaxPlayCircle2 +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.painterResource + +@Composable +fun CollectionView( + title: String, + description: String, + imageURL: String, + imageResource: DrawableResource? = null, + ownerName: String, + ownerImageURL: String?, + onOwnerClick: () -> Unit, + onPlay: () -> Unit, + onShufflePlay: () -> Unit, + onAddToQueue: () -> Unit, + isPlaying: Boolean = false, + isFollowing: Boolean = false, + onFollowClick: () -> Unit = {}, + showFollowButton: Boolean = true, + onEdit: (() -> Unit)? = null, + sharedElementKey: String? = null, + isLoading: Boolean = false, + error: String? = null, + onRetry: (() -> Unit)? = null, + tracks: List = emptyList(), + hasMore: Boolean = false, + isLoadingNextPage: Boolean = false, + currentTrackId: String? = null, + isCurrentTrackPlaying: Boolean = false, + onTrackClick: (MetadataTrack) -> Unit = {}, + onLoadNextPage: () -> Unit = {}, + onTrackOptionsAction: (MetadataTrack, TrackOptionsAction) -> Unit = { _, _ -> }, + onArtistClick: (MetadataArtist.Basic) -> Unit = {}, + onAlbumClick: (MetadataAlbum.Detailed) -> Unit = {}, + onArtistsOverflowClick: (MetadataTrack) -> Unit = {}, + onBulkDownload: (List) -> Unit = {}, + onBulkAddToQueue: (List) -> Unit = {}, + onBulkPlayNext: (List) -> Unit = {}, + onBulkAddToPlaylist: (List) -> Unit = {}, + trackOptionsState: (MetadataTrack) -> TrackOptionsState = { TrackOptionsState() }, + footerContent: (@Composable () -> Unit)? = null, + trailingContent: @Composable () -> Unit = {}, +) { + val listState = rememberLazyListState() + val isCollapsed by remember { + derivedStateOf { + listState.firstVisibleItemIndex > 0 || + listState.firstVisibleItemScrollOffset > 400 + } + } + + Scaffold( + topBar = { + ApplicationMainBar( + backButton = true, + title = if (isCollapsed) { + { + CollapsedCollectionTitle( + title = title, + imageURL = imageURL, + imageResource = imageResource, + ) + } + } else { + {} + }, + actions = if (isCollapsed) { + { + IconButton(onClick = onPlay) { + Icon( + imageVector = if (isPlaying) Iconsax.IconsaxPauseCircle else Iconsax.IconsaxPlayCircle2, + contentDescription = if (isPlaying) "Pause" else "Play", + ) + } + } + } else { + {} + }, + ) + } + ) { innerPadding -> + if (error != null && tracks.isEmpty() && !isLoading) { + ErrorDisplay( + errorMessage = error, + onRetry = onRetry ?: {}, + modifier = Modifier.padding(innerPadding), + ) + } else { + TrackList( + modifier = Modifier.padding(innerPadding), + state = listState, + headerContent = { + if (isLoading && tracks.isEmpty()) { + dev.krtirtho.spotube.core.ui.misc.SkeletonTree(true) { + CollectionDetails( + title = title.ifBlank { "Loading..." }, + description = description, + imageURL = imageURL, + imageResource = imageResource, + ownerName = ownerName.ifBlank { "Unknown" }, + ownerImageURL = ownerImageURL, + onOwnerClick = onOwnerClick, + onPlay = onPlay, + onShufflePlay = onShufflePlay, + onAddToQueue = onAddToQueue, + isPlaying = isPlaying, + isFollowing = isFollowing, + onFollowClick = onFollowClick, + showFollowButton = showFollowButton, + onEdit = onEdit, + sharedElementKey = sharedElementKey, + ) + } + } else { + CollectionDetails( + title = title, + description = description, + imageURL = imageURL, + imageResource = imageResource, + ownerName = ownerName, + ownerImageURL = ownerImageURL, + onOwnerClick = onOwnerClick, + onPlay = onPlay, + onShufflePlay = onShufflePlay, + onAddToQueue = onAddToQueue, + isPlaying = isPlaying, + isFollowing = isFollowing, + onFollowClick = onFollowClick, + showFollowButton = showFollowButton, + onEdit = onEdit, + sharedElementKey = sharedElementKey, + ) + } + }, + footerContent = footerContent, + tracks = tracks, + error = error, + hasMore = hasMore, + isLoading = isLoading, + isLoadingNextPage = isLoadingNextPage, + currentTrackId = currentTrackId, + isCurrentTrackPlaying = isCurrentTrackPlaying, + onTrackClick = onTrackClick, + onLoadNextPage = onLoadNextPage, + onTrackOptionsAction = onTrackOptionsAction, + onArtistClick = onArtistClick, + onAlbumClick = onAlbumClick, + onArtistsOverflowClick = onArtistsOverflowClick, + onBulkDownload = onBulkDownload, + onBulkAddToQueue = onBulkAddToQueue, + onBulkPlayNext = onBulkPlayNext, + onBulkAddToPlaylist = onBulkAddToPlaylist, + trackOptionsState = trackOptionsState, + ) + } + + trailingContent() + } +} + +@Composable +private fun CollapsedCollectionTitle( + title: String, + imageURL: String, + imageResource: DrawableResource? = null, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(6.dp)) + .shimmerApply(), + contentAlignment = Alignment.Center, + ) { + if (imageURL.isNotBlank()) { + AsyncImage( + model = imageURL, + contentDescription = title, + modifier = Modifier.size(36.dp), + contentScale = ContentScale.Crop, + ) + } else if (imageResource != null) { + androidx.compose.foundation.Image( + painter = painterResource(imageResource), + contentDescription = title, + modifier = Modifier.size(36.dp), + contentScale = ContentScale.Crop, + ) + } else { + Text( + text = title.take(1).ifBlank { "?" }.uppercase(), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + TextWithShimmer( + text = title, + style = MaterialTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(start = 10.dp), + ) + } +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackList.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackList.kt index ebfc537c..b897e9b0 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackList.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackList.kt @@ -18,11 +18,8 @@ package dev.krtirtho.spotube.core.ui.component import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -154,8 +151,6 @@ fun TrackList( simplified: Boolean = false, scrollable: Boolean = true, state: LazyListState? = null, - collapsedHeader: (@Composable () -> Unit)? = null, - isCollapsedHeaderShown: Boolean = false, ) { var filterQuery by rememberSaveable { mutableStateOf("") } var sortBy by rememberSaveable { mutableStateOf(TrackSortOption.None) } @@ -491,17 +486,6 @@ fun TrackList( } } - if (collapsedHeader != null) { - AnimatedVisibility( - visible = isCollapsedHeaderShown, - enter = fadeIn() + slideInVertically { -it }, - exit = fadeOut() + slideOutVertically { -it }, - modifier = Modifier.align(Alignment.TopCenter).padding(horizontal = if (isCompact) 6.dp else 16.dp), - ) { - collapsedHeader() - } - } - if (!useDropdownForOptions) { selectedTrackForOptions?.let { track -> TrackOptionsBottomSheet( diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumScreen.kt index c4074c61..d7be2b4d 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumScreen.kt @@ -17,18 +17,13 @@ package dev.krtirtho.spotube.modules.album -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf 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.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack import dev.krtirtho.spotube.core.audioplayer.AudioPlayer @@ -38,14 +33,9 @@ import dev.krtirtho.spotube.core.audioplayer.QueueEntry import dev.krtirtho.spotube.core.navigation.NavigationCommands import dev.krtirtho.spotube.core.navigation.Routes import dev.krtirtho.spotube.core.share.ShareService -import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar -import dev.krtirtho.spotube.core.ui.component.CollapsedCollectionHeader -import dev.krtirtho.spotube.core.ui.component.CollectionDetails -import dev.krtirtho.spotube.core.ui.component.ErrorDisplay -import dev.krtirtho.spotube.core.ui.component.TrackList +import dev.krtirtho.spotube.core.ui.component.CollectionView import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction import dev.krtirtho.spotube.core.ui.component.TrackOptionsState -import dev.krtirtho.spotube.core.ui.misc.SkeletonTree import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel import dev.krtirtho.spotube.modules.library.LibraryRepository import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker @@ -78,14 +68,6 @@ fun AlbumScreen(albumId: String) { var tracksToAddToPlaylist by remember { mutableStateOf>(emptyList()) } var currentUserId by remember { mutableStateOf(null) } - val listState = rememberLazyListState() - val isCollapsedHeaderShown by remember { - derivedStateOf { - listState.firstVisibleItemIndex > 0 || - listState.firstVisibleItemScrollOffset > 400 - } - } - LaunchedEffect(Unit) { currentUserId = libraryRepository.currentUser()?.id } @@ -120,143 +102,77 @@ fun AlbumScreen(albumId: String) { } } - Scaffold( - topBar = { ApplicationMainBar(backButton = !isCollapsedHeaderShown) } - ) { innerPadding -> - when (state) { - is AlbumScreenState.Loading -> { - TrackList( - modifier = Modifier.padding(innerPadding), - headerContent = { - SkeletonTree(true) { - CollectionDetails( - title = "Loading album...", - description = "", - imageURL = "", - ownerName = "Unknown artist", - ownerImageURL = null, - onOwnerClick = {}, - onPlay = {}, - onShufflePlay = {}, - onAddToQueue = {}, - isPlaying = false, - isFollowing = false, - onFollowClick = {}, - ) - } - }, - tracks = emptyList(), - error = null, - hasMore = false, - isLoading = true, - isLoadingNextPage = false, - currentTrackId = null, - isCurrentTrackPlaying = false, - onTrackClick = {}, - onLoadNextPage = {}, - onArtistClick = { navigationCommands.navigateTo(Routes.Artist(it.id)) }, - onAlbumClick = { navigationCommands.navigateTo(Routes.Album(it.id)) }, - onTrackOptionsAction = { _, _ -> }, - trackOptionsState = { TrackOptionsState() }, - ) + val dataState = state as? AlbumScreenState.Data + val album = dataState?.album + val ownerName = + album?.artists?.joinToString { it.name }.orEmpty().ifBlank { "Unknown artist" } + val ownerImageURL = album?.artists?.firstOrNull()?.thumbnails?.firstOrNull()?.url + val firstArtistId = album?.artists?.firstOrNull()?.id + val artworkUrl = album?.thumbnails?.firstOrNull()?.url.orEmpty() + val isAlbumPlaying = currentCollectionEntry?.id == albumId && + playerState == PlayerState.PLAYING + + val errorMessage = (state as? AlbumScreenState.Error)?.message + val isLoading = state is AlbumScreenState.Loading && (dataState == null || dataState.tracks.isEmpty()) + + CollectionView( + title = album?.title ?: "Loading album...", + description = album?.description ?: "${album?.albumType?.name.orEmpty()} • ${album?.releaseDate.orEmpty()}", + imageURL = artworkUrl, + ownerName = ownerName, + ownerImageURL = ownerImageURL, + onOwnerClick = { + firstArtistId?.let { + navigationCommands.navigateTo(Routes.Artist(it)) } - - is AlbumScreenState.Error -> { - ErrorDisplay( - errorMessage = (state as AlbumScreenState.Error).message, - onRetry = { viewModel.refresh() }, - modifier = Modifier.padding(innerPadding), - ) - } - - is AlbumScreenState.Data -> { - val dataState = state as AlbumScreenState.Data - val album = dataState.album - val ownerName = - album?.artists?.joinToString { it.name }.orEmpty().ifBlank { "Unknown artist" } - val ownerImageURL = album?.artists?.firstOrNull()?.thumbnails?.firstOrNull()?.url - val firstArtistId = album?.artists?.firstOrNull()?.id - val artworkUrl = album?.thumbnails?.firstOrNull()?.url.orEmpty() - val isAlbumPlaying = currentCollectionEntry?.id == albumId && - playerState == PlayerState.PLAYING - - TrackList( - modifier = Modifier.padding(innerPadding), - state = listState, - collapsedHeader = { - CollapsedCollectionHeader( - title = album?.title ?: "Album", - imageURL = artworkUrl, - isPlaying = isAlbumPlaying, - onPlay = viewModel::playAlbum, - onBack = { navigationCommands.pop() }, + }, + onPlay = viewModel::playAlbum, + onShufflePlay = {}, + onAddToQueue = viewModel::addAlbumToQueue, + isPlaying = isAlbumPlaying, + isFollowing = savedAlbumIds.contains(albumId), + onFollowClick = viewModel::toggleSavedAlbum, + isLoading = isLoading, + error = errorMessage, + onRetry = { viewModel.refresh() }, + tracks = dataState?.tracks ?: emptyList(), + hasMore = dataState?.nextPagination != null, + isLoadingNextPage = state is AlbumScreenState.Data.LoadingMore, + currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id, + isCurrentTrackPlaying = playerState == PlayerState.PLAYING, + onTrackClick = viewModel::playAlbumFromTrack, + onLoadNextPage = viewModel::loadNextTracksPage, + onArtistClick = { navigationCommands.navigateTo(Routes.Artist(it.id)) }, + onAlbumClick = { navigationCommands.navigateTo(Routes.Album(it.id)) }, + onTrackOptionsAction = ::handleTrackOptionsAction, + trackOptionsState = ::getTrackOptionsState, + onBulkDownload = { tracks -> + downloadsViewModel.downloadTracks(tracks) + }, + onBulkAddToQueue = { tracks -> + viewModel.addTracksToQueue(tracks) + }, + onBulkPlayNext = { tracks -> + viewModel.playTracksNext(tracks) + }, + onBulkAddToPlaylist = { tracks -> + tracksToAddToPlaylist = tracks + showAddToPlaylistPicker = true + }, + trailingContent = { + AddToPlaylistPicker( + visible = showAddToPlaylistPicker, + currentUserId = currentUserId, + onDismiss = { showAddToPlaylistPicker = false }, + onPlaylistSelected = { playlistId -> + scope.launch { + libraryRepository.addTracksToPlaylist( + playlistId, + tracksToAddToPlaylist.map { it.id } ) - }, - isCollapsedHeaderShown = isCollapsedHeaderShown, - headerContent = { - CollectionDetails( - title = album?.title ?: "Loading album...", - description = album?.description ?: "${album?.albumType?.name.orEmpty()} • ${album?.releaseDate.orEmpty()}", - imageURL = artworkUrl, - ownerName = ownerName, - ownerImageURL = ownerImageURL, - onOwnerClick = { - firstArtistId?.let { - navigationCommands.navigateTo( - Routes.Artist(it) - ) - } - }, - onPlay = viewModel::playAlbum, - onShufflePlay = {}, - onAddToQueue = viewModel::addAlbumToQueue, - isPlaying = isAlbumPlaying, - isFollowing = savedAlbumIds.contains(albumId), - onFollowClick = viewModel::toggleSavedAlbum, - ) - }, - tracks = dataState.tracks, - error = null, - hasMore = dataState.nextPagination != null, - isLoading = state is AlbumScreenState.Loading && dataState.tracks.isEmpty(), - isLoadingNextPage = state is AlbumScreenState.Data.LoadingMore, - currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id, - isCurrentTrackPlaying = playerState == PlayerState.PLAYING, - onTrackClick = viewModel::playAlbumFromTrack, - onLoadNextPage = viewModel::loadNextTracksPage, - onArtistClick = { navigationCommands.navigateTo(Routes.Artist(it.id)) }, - onAlbumClick = { navigationCommands.navigateTo(Routes.Album(it.id)) }, - onTrackOptionsAction = ::handleTrackOptionsAction, - trackOptionsState = ::getTrackOptionsState, - onBulkDownload = { tracks -> - downloadsViewModel.downloadTracks(tracks) - }, - onBulkAddToQueue = { tracks -> - viewModel.addTracksToQueue(tracks) - }, - onBulkPlayNext = { tracks -> - viewModel.playTracksNext(tracks) - }, - onBulkAddToPlaylist = { tracks -> - tracksToAddToPlaylist = tracks - showAddToPlaylistPicker = true - }, - ) - } - } - - AddToPlaylistPicker( - visible = showAddToPlaylistPicker, - currentUserId = currentUserId, - onDismiss = { showAddToPlaylistPicker = false }, - onPlaylistSelected = { playlistId -> - scope.launch { - libraryRepository.addTracksToPlaylist( - playlistId, - tracksToAddToPlaylist.map { it.id } - ) - } - }, - ) - } + } + }, + ) + }, + ) } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistScreen.kt index b0a6032d..92f18783 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistScreen.kt @@ -21,12 +21,9 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Icon -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -44,20 +41,14 @@ import dev.krtirtho.spotube.core.navigation.NavigationCommands import dev.krtirtho.spotube.core.navigation.Routes import dev.krtirtho.spotube.core.share.ShareService import dev.krtirtho.spotube.core.ui.base.OutlineButton -import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar -import dev.krtirtho.spotube.core.ui.component.CollapsedCollectionHeader -import dev.krtirtho.spotube.core.ui.component.CollectionDetails -import dev.krtirtho.spotube.core.ui.component.ErrorDisplay -import dev.krtirtho.spotube.core.ui.component.TrackList +import dev.krtirtho.spotube.core.ui.component.CollectionView import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction import dev.krtirtho.spotube.core.ui.component.TrackOptionsState -import dev.krtirtho.spotube.core.ui.misc.SkeletonTree import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel import dev.krtirtho.spotube.modules.library.LibraryRepository import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker import dev.krtirtho.spotube.modules.library.playlist.PlaylistFormData import dev.krtirtho.spotube.modules.library.playlist.PlaylistFormSheet -import dev.krtirtho.spotube.modules.playlist.AddTracksToPlaylistDialog import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare import kotlinx.coroutines.launch @@ -90,14 +81,6 @@ fun PlaylistScreen(playlistId: String) { var showAddTracksDialog by remember { mutableStateOf(false) } var tracksToAddToPlaylist by remember { mutableStateOf>(emptyList()) } - val listState = rememberLazyListState() - val isCollapsedHeaderShown by remember { - derivedStateOf { - listState.firstVisibleItemIndex > 0 || - listState.firstVisibleItemScrollOffset > 400 - } - } - fun handleTrackOptionsAction(track: MetadataTrack, action: TrackOptionsAction) { viewModel.handleTrackOptionsAction(track, action) if (action is TrackOptionsAction.Share) { @@ -115,209 +98,144 @@ fun PlaylistScreen(playlistId: String) { } } - Scaffold( - topBar = { ApplicationMainBar(backButton = !isCollapsedHeaderShown) } - ) { innerPadding -> - when (state) { - is PlaylistScreenState.Loading -> { - TrackList( - modifier = Modifier.padding(innerPadding), - headerContent = { - SkeletonTree(true){ - CollectionDetails( - title = "Loading playlist...", - description = "", - imageURL = "", - ownerName = "Unknown", - ownerImageURL = null, - onOwnerClick = {}, - onPlay = {}, - onShufflePlay = {}, - onAddToQueue = {}, - isPlaying = false, - isFollowing = false, - onFollowClick = {}, - ) - } - }, - tracks = emptyList(), - error = null, - hasMore = false, - isLoading = true, - isLoadingNextPage = false, - currentTrackId = null, - isCurrentTrackPlaying = false, - onTrackClick = {}, - onLoadNextPage = {}, - onArtistClick = { navigationCommands.navigateTo(Routes.Artist(it.id)) }, - onAlbumClick = { navigationCommands.navigateTo(Routes.Album(it.id)) }, - onTrackOptionsAction = { _, _ -> }, - trackOptionsState = { TrackOptionsState() }, - ) - } + val dataState = state as? PlaylistScreenState.Data + val playlist = dataState?.playlist + val owner = playlist?.owner + val ownerName = owner?.displayName ?: owner?.username ?: "Unknown" + val ownerImageURL = owner?.thumbnails?.firstOrNull()?.url + val isOwner = currentUserId != null && playlist?.owner?.id == currentUserId + val artworkUrl = playlist?.thumbnails?.firstOrNull()?.url.orEmpty() + val isPlaying = currentCollectionEntry?.id == playlistId && + playerState == PlayerState.PLAYING - is PlaylistScreenState.Error -> { - ErrorDisplay( - errorMessage = (state as PlaylistScreenState.Error).message, - onRetry = { viewModel.refresh() }, - modifier = Modifier.padding(innerPadding), - ) - } + val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle() - is PlaylistScreenState.Data -> { - val dataState = state as PlaylistScreenState.Data - val playlist = dataState.playlist - val owner = playlist?.owner - val ownerName = owner?.displayName ?: owner?.username ?: "Unknown" - val ownerImageURL = owner?.thumbnails?.firstOrNull()?.url - - val isOwner = currentUserId != null && playlist?.owner?.id == currentUserId - - val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle() - - val artworkUrl = playlist?.thumbnails?.firstOrNull()?.url.orEmpty() - val isPlaying = currentCollectionEntry?.id == playlistId && - playerState == PlayerState.PLAYING - - fun getTrackOptionsState(track: MetadataTrack): TrackOptionsState { - val currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id - val queueTrackIds = queue.mapNotNull { entry -> - (entry as? QueueEntry.StreamingTrack)?.track?.id - }.toSet() - return TrackOptionsState( - isInQueue = queueTrackIds.contains(track.id), - isCurrentlyPlaying = track.id == currentTrackId, - isFavorite = savedTrackIds.contains(track.id), - isBlacklisted = false, - ) - } - - TrackList( - modifier = Modifier.padding(innerPadding), - state = listState, - collapsedHeader = { - CollapsedCollectionHeader( - title = playlist?.title ?: "Playlist", - imageURL = artworkUrl, - isPlaying = isPlaying, - onPlay = viewModel::playPlaylist, - onBack = { navigationCommands.pop() }, - ) - }, - isCollapsedHeaderShown = isCollapsedHeaderShown, - headerContent = { - CollectionDetails( - title = playlist?.title ?: "Loading playlist...", - description = playlist?.description.orEmpty(), - imageURL = artworkUrl, - ownerName = ownerName, - ownerImageURL = ownerImageURL, - onOwnerClick = {}, - onPlay = viewModel::playPlaylist, - onShufflePlay = {}, - onAddToQueue = viewModel::addPlaylistToQueue, - isPlaying = isPlaying, - isFollowing = savedPlaylistIds.contains(playlistId), - onFollowClick = viewModel::toggleSavedPlaylist, - showFollowButton = playlistId != "saved_tracks", - onEdit = if (isOwner) { { showEditPlaylist = true } } else null, - ) - }, - footerContent = if (isOwner) { - { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - horizontalArrangement = Arrangement.Center, - ) { - OutlineButton(onClick = { showAddTracksDialog = true }) { - Icon( - imageVector = Iconsax.IconsaxAddSquare, - contentDescription = null, - ) - Text("Add Tracks", modifier = Modifier.padding(start = 8.dp)) - } - } - } - } else null, - tracks = dataState.tracks, - error = null, - hasMore = dataState.nextPagination != null, - isLoading = state is PlaylistScreenState.Loading && dataState.tracks.isEmpty(), - isLoadingNextPage = state is PlaylistScreenState.Data.LoadingMore, - currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id, - isCurrentTrackPlaying = playerState == PlayerState.PLAYING, - onTrackClick = viewModel::playPlaylistFromTrack, - onLoadNextPage = viewModel::loadNextTracksPage, - onArtistClick = { navigationCommands.navigateTo(Routes.Artist(it.id)) }, - onAlbumClick = { navigationCommands.navigateTo(Routes.Album(it.id)) }, - onTrackOptionsAction = ::handleTrackOptionsAction, - trackOptionsState = ::getTrackOptionsState, - onBulkDownload = { tracks -> - downloadsViewModel.downloadTracks(tracks) - }, - onBulkAddToQueue = { tracks -> - viewModel.addTracksToQueue(tracks) - }, - onBulkPlayNext = { tracks -> - viewModel.playTracksNext(tracks) - }, - onBulkAddToPlaylist = { tracks -> - tracksToAddToPlaylist = tracks - showAddToPlaylistPicker = true - }, - ) - } - } - - val dataState = state as? PlaylistScreenState.Data - val playlist = (dataState as? PlaylistScreenState.Data.Loaded)?.playlist - PlaylistFormSheet( - visible = showEditPlaylist, - onDismiss = { showEditPlaylist = false }, - isEditing = true, - initialData = playlist?.let { - PlaylistFormData( - name = it.title, - description = it.description.orEmpty(), - isPublic = true, - isCollaborating = false, - imagePreviewUrl = it.thumbnails.firstOrNull()?.url, - ) - }, - onSubmit = { data -> - viewModel.updatePlaylist( - name = data.name, - description = data.description.ifBlank { null }, - isPublic = data.isPublic, - isCollaborating = data.isCollaborating, - imageBase64 = data.imageBase64.ifBlank { null }, - ) - }, - ) - - AddToPlaylistPicker( - visible = showAddToPlaylistPicker, - currentUserId = currentUserId, - onDismiss = { showAddToPlaylistPicker = false }, - onPlaylistSelected = { selectedPlaylistId -> - scope.launch { - libraryRepository.addTracksToPlaylist( - selectedPlaylistId, - tracksToAddToPlaylist.map { it.id } - ) - } - }, - ) - - AddTracksToPlaylistDialog( - visible = showAddTracksDialog, - playlistId = playlistId, - onDismiss = { - showAddTracksDialog = false - viewModel.refresh() - }, + fun getTrackOptionsState(track: MetadataTrack): TrackOptionsState { + val currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id + val queueTrackIds = queue.mapNotNull { entry -> + (entry as? QueueEntry.StreamingTrack)?.track?.id + }.toSet() + return TrackOptionsState( + isInQueue = queueTrackIds.contains(track.id), + isCurrentlyPlaying = track.id == currentTrackId, + isFavorite = savedTrackIds.contains(track.id), + isBlacklisted = false, ) } + + val errorMessage = (state as? PlaylistScreenState.Error)?.message + val isLoading = state is PlaylistScreenState.Loading && (dataState == null || dataState.tracks.isEmpty()) + + val footerContent: (@Composable () -> Unit)? = if (isOwner && dataState != null) { + { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.Center, + ) { + OutlineButton(onClick = { showAddTracksDialog = true }) { + Icon( + imageVector = Iconsax.IconsaxAddSquare, + contentDescription = null, + ) + Text("Add Tracks", modifier = Modifier.padding(start = 8.dp)) + } + } + } + } else null + + CollectionView( + title = playlist?.title ?: "Loading playlist...", + description = playlist?.description.orEmpty(), + imageURL = artworkUrl, + ownerName = ownerName, + ownerImageURL = ownerImageURL, + onOwnerClick = {}, + onPlay = viewModel::playPlaylist, + onShufflePlay = {}, + onAddToQueue = viewModel::addPlaylistToQueue, + isPlaying = isPlaying, + isFollowing = savedPlaylistIds.contains(playlistId), + onFollowClick = viewModel::toggleSavedPlaylist, + showFollowButton = playlistId != "saved_tracks", + onEdit = if (isOwner) { { showEditPlaylist = true } } else null, + isLoading = isLoading, + error = errorMessage, + onRetry = { viewModel.refresh() }, + tracks = dataState?.tracks ?: emptyList(), + hasMore = dataState?.nextPagination != null, + isLoadingNextPage = state is PlaylistScreenState.Data.LoadingMore, + currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id, + isCurrentTrackPlaying = playerState == PlayerState.PLAYING, + onTrackClick = viewModel::playPlaylistFromTrack, + onLoadNextPage = viewModel::loadNextTracksPage, + onArtistClick = { navigationCommands.navigateTo(Routes.Artist(it.id)) }, + onAlbumClick = { navigationCommands.navigateTo(Routes.Album(it.id)) }, + onTrackOptionsAction = ::handleTrackOptionsAction, + trackOptionsState = ::getTrackOptionsState, + onBulkDownload = { tracks -> + downloadsViewModel.downloadTracks(tracks) + }, + onBulkAddToQueue = { tracks -> + viewModel.addTracksToQueue(tracks) + }, + onBulkPlayNext = { tracks -> + viewModel.playTracksNext(tracks) + }, + onBulkAddToPlaylist = { tracks -> + tracksToAddToPlaylist = tracks + showAddToPlaylistPicker = true + }, + footerContent = footerContent, + trailingContent = { + val loadedPlaylist = (dataState as? PlaylistScreenState.Data.Loaded)?.playlist + PlaylistFormSheet( + visible = showEditPlaylist, + onDismiss = { showEditPlaylist = false }, + isEditing = true, + initialData = loadedPlaylist?.let { + PlaylistFormData( + name = it.title, + description = it.description.orEmpty(), + isPublic = true, + isCollaborating = false, + imagePreviewUrl = it.thumbnails.firstOrNull()?.url, + ) + }, + onSubmit = { data -> + viewModel.updatePlaylist( + name = data.name, + description = data.description.ifBlank { null }, + isPublic = data.isPublic, + isCollaborating = data.isCollaborating, + imageBase64 = data.imageBase64.ifBlank { null }, + ) + }, + ) + + AddToPlaylistPicker( + visible = showAddToPlaylistPicker, + currentUserId = currentUserId, + onDismiss = { showAddToPlaylistPicker = false }, + onPlaylistSelected = { selectedPlaylistId -> + scope.launch { + libraryRepository.addTracksToPlaylist( + selectedPlaylistId, + tracksToAddToPlaylist.map { it.id } + ) + } + }, + ) + + AddTracksToPlaylistDialog( + visible = showAddTracksDialog, + playlistId = playlistId, + onDismiss = { + showAddTracksDialog = false + viewModel.refresh() + }, + ) + }, + ) } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksScreen.kt index 9acd85bb..954755a2 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksScreen.kt @@ -17,18 +17,13 @@ package dev.krtirtho.spotube.modules.saved_tracks -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf 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.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack import dev.krtirtho.spotube.core.audioplayer.AudioPlayer @@ -39,19 +34,13 @@ import dev.krtirtho.spotube.core.audioplayer.QueueEntry import dev.krtirtho.spotube.core.navigation.NavigationCommands import dev.krtirtho.spotube.core.navigation.Routes import dev.krtirtho.spotube.core.share.ShareService -import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar -import dev.krtirtho.spotube.core.ui.component.CollapsedCollectionHeader -import dev.krtirtho.spotube.core.ui.component.CollectionDetails -import dev.krtirtho.spotube.core.ui.component.ErrorDisplay -import dev.krtirtho.spotube.core.ui.component.TrackList +import dev.krtirtho.spotube.core.ui.component.CollectionView import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction import dev.krtirtho.spotube.core.ui.component.TrackOptionsState -import dev.krtirtho.spotube.core.ui.misc.SkeletonTree import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel import dev.krtirtho.spotube.modules.library.LibraryRepository import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker import kotlinx.coroutines.launch -import org.jetbrains.compose.resources.DrawableResource import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel import org.koin.core.parameter.parametersOf @@ -80,14 +69,6 @@ fun SavedTracksScreen() { var tracksToAddToPlaylist by remember { mutableStateOf>(emptyList()) } var currentUserId by remember { mutableStateOf(null) } - val listState = rememberLazyListState() - val isCollapsedHeaderShown by remember { - derivedStateOf { - listState.firstVisibleItemIndex > 0 || - listState.firstVisibleItemScrollOffset > 400 - } - } - LaunchedEffect(Unit) { currentUserId = libraryRepository.currentUser()?.id } @@ -122,137 +103,69 @@ fun SavedTracksScreen() { } } - Scaffold( - topBar = { ApplicationMainBar(backButton = !isCollapsedHeaderShown) } - ) { innerPadding -> - when (state) { - is SavedTracksScreenState.Loading -> { - TrackList( - modifier = Modifier.padding(innerPadding), - headerContent = { - SkeletonTree(true) { - CollectionDetails( - title = "Loading saved tracks...", - description = "", - imageURL = "", - imageResource = Res.drawable.liked_tracks, - ownerName = "You", - ownerImageURL = null, - onOwnerClick = {}, - onPlay = {}, - onShufflePlay = {}, - onAddToQueue = {}, - isPlaying = false, - isFollowing = false, - onFollowClick = {}, - showFollowButton = false, - ) - } - }, - tracks = emptyList(), - error = null, - hasMore = false, - isLoading = true, - isLoadingNextPage = false, - currentTrackId = null, - isCurrentTrackPlaying = false, - onTrackClick = {}, - onLoadNextPage = {}, - onArtistClick = { navigationCommands.navigateTo(Routes.Artist(it.id)) }, - onAlbumClick = { navigationCommands.navigateTo(Routes.Album(it.id)) }, - onTrackOptionsAction = { _, _ -> }, - trackOptionsState = { TrackOptionsState() }, - ) - } + val dataState = state as? SavedTracksScreenState.Data + val isPlaying = currentCollectionEntry is QueueCollectionEntry.SavedTracks && + playerState == PlayerState.PLAYING - is SavedTracksScreenState.Error -> { - ErrorDisplay( - errorMessage = (state as SavedTracksScreenState.Error).message, - onRetry = { viewModel.refresh() }, - modifier = Modifier.padding(innerPadding), - ) - } + val errorMessage = (state as? SavedTracksScreenState.Error)?.message + val isLoading = state is SavedTracksScreenState.Loading && (dataState == null || dataState.tracks.isEmpty()) - is SavedTracksScreenState.Data -> { - val dataState = state as SavedTracksScreenState.Data - - val isPlaying = currentCollectionEntry is QueueCollectionEntry.SavedTracks && - playerState == PlayerState.PLAYING - - TrackList( - modifier = Modifier.padding(innerPadding), - state = listState, - collapsedHeader = { - CollapsedCollectionHeader( - title = "Saved Tracks", - imageURL = "", - imageResource = Res.drawable.liked_tracks, - isPlaying = isPlaying, - onPlay = viewModel::playSavedTracks, - onBack = { navigationCommands.pop() }, + CollectionView( + title = "Saved Tracks", + description = dataState?.let { "${it.totalCount} tracks" } ?: "", + imageURL = "", + imageResource = Res.drawable.liked_tracks, + ownerName = "You", + ownerImageURL = null, + onOwnerClick = {}, + onPlay = viewModel::playSavedTracks, + onShufflePlay = {}, + onAddToQueue = viewModel::addSavedTracksToQueue, + isPlaying = isPlaying, + isFollowing = false, + onFollowClick = {}, + showFollowButton = false, + isLoading = isLoading, + error = errorMessage, + onRetry = { viewModel.refresh() }, + tracks = dataState?.tracks ?: emptyList(), + hasMore = dataState?.nextPagination != null, + isLoadingNextPage = state is SavedTracksScreenState.Data.LoadingMore, + currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id, + isCurrentTrackPlaying = playerState == PlayerState.PLAYING, + onTrackClick = viewModel::playSavedTracksFromTrack, + onLoadNextPage = viewModel::loadNextTracksPage, + onArtistClick = { navigationCommands.navigateTo(Routes.Artist(it.id)) }, + onAlbumClick = { navigationCommands.navigateTo(Routes.Album(it.id)) }, + onTrackOptionsAction = ::handleTrackOptionsAction, + trackOptionsState = ::getTrackOptionsState, + onBulkDownload = { tracks -> + downloadsViewModel.downloadTracks(tracks) + }, + onBulkAddToQueue = { tracks -> + viewModel.addTracksToQueue(tracks) + }, + onBulkPlayNext = { tracks -> + viewModel.playTracksNext(tracks) + }, + onBulkAddToPlaylist = { tracks -> + tracksToAddToPlaylist = tracks + showAddToPlaylistPicker = true + }, + trailingContent = { + AddToPlaylistPicker( + visible = showAddToPlaylistPicker, + currentUserId = currentUserId, + onDismiss = { showAddToPlaylistPicker = false }, + onPlaylistSelected = { playlistId -> + scope.launch { + libraryRepository.addTracksToPlaylist( + playlistId, + tracksToAddToPlaylist.map { it.id } ) - }, - isCollapsedHeaderShown = isCollapsedHeaderShown, - headerContent = { - CollectionDetails( - title = "Saved Tracks", - description = "${dataState.totalCount} tracks", - imageURL = "", - imageResource = Res.drawable.liked_tracks, - ownerName = "You", - ownerImageURL = null, - onOwnerClick = {}, - onPlay = viewModel::playSavedTracks, - onShufflePlay = {}, - onAddToQueue = viewModel::addSavedTracksToQueue, - isPlaying = isPlaying, - isFollowing = false, - onFollowClick = {}, - showFollowButton = false, - ) - }, - tracks = dataState.tracks, - error = null, - hasMore = dataState.nextPagination != null, - isLoading = state is SavedTracksScreenState.Loading && dataState.tracks.isEmpty(), - isLoadingNextPage = state is SavedTracksScreenState.Data.LoadingMore, - currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id, - isCurrentTrackPlaying = playerState == PlayerState.PLAYING, - onTrackClick = viewModel::playSavedTracksFromTrack, - onLoadNextPage = viewModel::loadNextTracksPage, - onArtistClick = { navigationCommands.navigateTo(Routes.Artist(it.id)) }, - onAlbumClick = { navigationCommands.navigateTo(Routes.Album(it.id)) }, - onTrackOptionsAction = ::handleTrackOptionsAction, - trackOptionsState = ::getTrackOptionsState, - onBulkDownload = { tracks -> - downloadsViewModel.downloadTracks(tracks) - }, - onBulkAddToQueue = { tracks -> - viewModel.addTracksToQueue(tracks) - }, - onBulkPlayNext = { tracks -> - viewModel.playTracksNext(tracks) - }, - onBulkAddToPlaylist = { tracks -> - tracksToAddToPlaylist = tracks - showAddToPlaylistPicker = true - }, - ) - } - } - - AddToPlaylistPicker( - visible = showAddToPlaylistPicker, - currentUserId = currentUserId, - onDismiss = { showAddToPlaylistPicker = false }, - onPlaylistSelected = { playlistId -> - scope.launch { - libraryRepository.addTracksToPlaylist( - playlistId, - tracksToAddToPlaylist.map { it.id } - ) - } - }, - ) - } + } + }, + ) + }, + ) }