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 }
- )
- }
- },
- )
- }
+ }
+ },
+ )
+ },
+ )
}