mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-08-05 19:59:51 +00:00
feat: implement CollectionView component and refactor Album, Playlist, SavedTracks screens for improved UI consistency
This commit is contained in:
parent
b22ab3d43e
commit
c10c9bb3fc
@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.base.PrimaryButton
|
||||||
import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
|
import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
|
||||||
import dev.krtirtho.spotube.core.ui.misc.shimmerApply
|
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.Iconsax
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxEdit
|
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
|
@Composable
|
||||||
@androidx.compose.ui.tooling.preview.Preview
|
@androidx.compose.ui.tooling.preview.Preview
|
||||||
fun CollectionDetailsPreview() {
|
fun CollectionDetailsPreview() {
|
||||||
|
|||||||
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<MetadataTrack> = 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<MetadataTrack>) -> Unit = {},
|
||||||
|
onBulkAddToQueue: (List<MetadataTrack>) -> Unit = {},
|
||||||
|
onBulkPlayNext: (List<MetadataTrack>) -> Unit = {},
|
||||||
|
onBulkAddToPlaylist: (List<MetadataTrack>) -> 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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,11 +18,8 @@
|
|||||||
package dev.krtirtho.spotube.core.ui.component
|
package dev.krtirtho.spotube.core.ui.component
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.slideInVertically
|
|
||||||
import androidx.compose.animation.slideOutVertically
|
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@ -154,8 +151,6 @@ fun TrackList(
|
|||||||
simplified: Boolean = false,
|
simplified: Boolean = false,
|
||||||
scrollable: Boolean = true,
|
scrollable: Boolean = true,
|
||||||
state: LazyListState? = null,
|
state: LazyListState? = null,
|
||||||
collapsedHeader: (@Composable () -> Unit)? = null,
|
|
||||||
isCollapsedHeaderShown: Boolean = false,
|
|
||||||
) {
|
) {
|
||||||
var filterQuery by rememberSaveable { mutableStateOf("") }
|
var filterQuery by rememberSaveable { mutableStateOf("") }
|
||||||
var sortBy by rememberSaveable { mutableStateOf(TrackSortOption.None) }
|
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) {
|
if (!useDropdownForOptions) {
|
||||||
selectedTrackForOptions?.let { track ->
|
selectedTrackForOptions?.let { track ->
|
||||||
TrackOptionsBottomSheet(
|
TrackOptionsBottomSheet(
|
||||||
|
|||||||
@ -17,18 +17,13 @@
|
|||||||
|
|
||||||
package dev.krtirtho.spotube.modules.album
|
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.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayer
|
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.NavigationCommands
|
||||||
import dev.krtirtho.spotube.core.navigation.Routes
|
import dev.krtirtho.spotube.core.navigation.Routes
|
||||||
import dev.krtirtho.spotube.core.share.ShareService
|
import dev.krtirtho.spotube.core.share.ShareService
|
||||||
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
import dev.krtirtho.spotube.core.ui.component.CollectionView
|
||||||
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.TrackOptionsAction
|
import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsState
|
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.downloads.DownloadsViewModel
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
||||||
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
||||||
@ -78,14 +68,6 @@ fun AlbumScreen(albumId: String) {
|
|||||||
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
||||||
var currentUserId by remember { mutableStateOf<String?>(null) }
|
var currentUserId by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
|
||||||
val isCollapsedHeaderShown by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
listState.firstVisibleItemIndex > 0 ||
|
|
||||||
listState.firstVisibleItemScrollOffset > 400
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
currentUserId = libraryRepository.currentUser()?.id
|
currentUserId = libraryRepository.currentUser()?.id
|
||||||
}
|
}
|
||||||
@ -120,58 +102,8 @@ fun AlbumScreen(albumId: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
val dataState = state as? AlbumScreenState.Data
|
||||||
topBar = { ApplicationMainBar(backButton = !isCollapsedHeaderShown) }
|
val album = dataState?.album
|
||||||
) { 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() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 =
|
val ownerName =
|
||||||
album?.artists?.joinToString { it.name }.orEmpty().ifBlank { "Unknown artist" }
|
album?.artists?.joinToString { it.name }.orEmpty().ifBlank { "Unknown artist" }
|
||||||
val ownerImageURL = album?.artists?.firstOrNull()?.thumbnails?.firstOrNull()?.url
|
val ownerImageURL = album?.artists?.firstOrNull()?.thumbnails?.firstOrNull()?.url
|
||||||
@ -180,21 +112,10 @@ fun AlbumScreen(albumId: String) {
|
|||||||
val isAlbumPlaying = currentCollectionEntry?.id == albumId &&
|
val isAlbumPlaying = currentCollectionEntry?.id == albumId &&
|
||||||
playerState == PlayerState.PLAYING
|
playerState == PlayerState.PLAYING
|
||||||
|
|
||||||
TrackList(
|
val errorMessage = (state as? AlbumScreenState.Error)?.message
|
||||||
modifier = Modifier.padding(innerPadding),
|
val isLoading = state is AlbumScreenState.Loading && (dataState == null || dataState.tracks.isEmpty())
|
||||||
state = listState,
|
|
||||||
collapsedHeader = {
|
CollectionView(
|
||||||
CollapsedCollectionHeader(
|
|
||||||
title = album?.title ?: "Album",
|
|
||||||
imageURL = artworkUrl,
|
|
||||||
isPlaying = isAlbumPlaying,
|
|
||||||
onPlay = viewModel::playAlbum,
|
|
||||||
onBack = { navigationCommands.pop() },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
isCollapsedHeaderShown = isCollapsedHeaderShown,
|
|
||||||
headerContent = {
|
|
||||||
CollectionDetails(
|
|
||||||
title = album?.title ?: "Loading album...",
|
title = album?.title ?: "Loading album...",
|
||||||
description = album?.description ?: "${album?.albumType?.name.orEmpty()} • ${album?.releaseDate.orEmpty()}",
|
description = album?.description ?: "${album?.albumType?.name.orEmpty()} • ${album?.releaseDate.orEmpty()}",
|
||||||
imageURL = artworkUrl,
|
imageURL = artworkUrl,
|
||||||
@ -202,9 +123,7 @@ fun AlbumScreen(albumId: String) {
|
|||||||
ownerImageURL = ownerImageURL,
|
ownerImageURL = ownerImageURL,
|
||||||
onOwnerClick = {
|
onOwnerClick = {
|
||||||
firstArtistId?.let {
|
firstArtistId?.let {
|
||||||
navigationCommands.navigateTo(
|
navigationCommands.navigateTo(Routes.Artist(it))
|
||||||
Routes.Artist(it)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPlay = viewModel::playAlbum,
|
onPlay = viewModel::playAlbum,
|
||||||
@ -213,12 +132,11 @@ fun AlbumScreen(albumId: String) {
|
|||||||
isPlaying = isAlbumPlaying,
|
isPlaying = isAlbumPlaying,
|
||||||
isFollowing = savedAlbumIds.contains(albumId),
|
isFollowing = savedAlbumIds.contains(albumId),
|
||||||
onFollowClick = viewModel::toggleSavedAlbum,
|
onFollowClick = viewModel::toggleSavedAlbum,
|
||||||
)
|
isLoading = isLoading,
|
||||||
},
|
error = errorMessage,
|
||||||
tracks = dataState.tracks,
|
onRetry = { viewModel.refresh() },
|
||||||
error = null,
|
tracks = dataState?.tracks ?: emptyList(),
|
||||||
hasMore = dataState.nextPagination != null,
|
hasMore = dataState?.nextPagination != null,
|
||||||
isLoading = state is AlbumScreenState.Loading && dataState.tracks.isEmpty(),
|
|
||||||
isLoadingNextPage = state is AlbumScreenState.Data.LoadingMore,
|
isLoadingNextPage = state is AlbumScreenState.Data.LoadingMore,
|
||||||
currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id,
|
currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id,
|
||||||
isCurrentTrackPlaying = playerState == PlayerState.PLAYING,
|
isCurrentTrackPlaying = playerState == PlayerState.PLAYING,
|
||||||
@ -241,10 +159,7 @@ fun AlbumScreen(albumId: String) {
|
|||||||
tracksToAddToPlaylist = tracks
|
tracksToAddToPlaylist = tracks
|
||||||
showAddToPlaylistPicker = true
|
showAddToPlaylistPicker = true
|
||||||
},
|
},
|
||||||
)
|
trailingContent = {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddToPlaylistPicker(
|
AddToPlaylistPicker(
|
||||||
visible = showAddToPlaylistPicker,
|
visible = showAddToPlaylistPicker,
|
||||||
currentUserId = currentUserId,
|
currentUserId = currentUserId,
|
||||||
@ -258,5 +173,6 @@ fun AlbumScreen(albumId: String) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,12 +21,9 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
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.navigation.Routes
|
||||||
import dev.krtirtho.spotube.core.share.ShareService
|
import dev.krtirtho.spotube.core.share.ShareService
|
||||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
||||||
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
import dev.krtirtho.spotube.core.ui.component.CollectionView
|
||||||
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.TrackOptionsAction
|
import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsState
|
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.downloads.DownloadsViewModel
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
||||||
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
||||||
import dev.krtirtho.spotube.modules.library.playlist.PlaylistFormData
|
import dev.krtirtho.spotube.modules.library.playlist.PlaylistFormData
|
||||||
import dev.krtirtho.spotube.modules.library.playlist.PlaylistFormSheet
|
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.Iconsax
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -90,14 +81,6 @@ fun PlaylistScreen(playlistId: String) {
|
|||||||
var showAddTracksDialog by remember { mutableStateOf(false) }
|
var showAddTracksDialog by remember { mutableStateOf(false) }
|
||||||
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
|
||||||
val isCollapsedHeaderShown by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
listState.firstVisibleItemIndex > 0 ||
|
|
||||||
listState.firstVisibleItemScrollOffset > 400
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleTrackOptionsAction(track: MetadataTrack, action: TrackOptionsAction) {
|
fun handleTrackOptionsAction(track: MetadataTrack, action: TrackOptionsAction) {
|
||||||
viewModel.handleTrackOptionsAction(track, action)
|
viewModel.handleTrackOptionsAction(track, action)
|
||||||
if (action is TrackOptionsAction.Share) {
|
if (action is TrackOptionsAction.Share) {
|
||||||
@ -115,70 +98,18 @@ fun PlaylistScreen(playlistId: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
val dataState = state as? PlaylistScreenState.Data
|
||||||
topBar = { ApplicationMainBar(backButton = !isCollapsedHeaderShown) }
|
val playlist = dataState?.playlist
|
||||||
) { 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() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is PlaylistScreenState.Error -> {
|
|
||||||
ErrorDisplay(
|
|
||||||
errorMessage = (state as PlaylistScreenState.Error).message,
|
|
||||||
onRetry = { viewModel.refresh() },
|
|
||||||
modifier = Modifier.padding(innerPadding),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is PlaylistScreenState.Data -> {
|
|
||||||
val dataState = state as PlaylistScreenState.Data
|
|
||||||
val playlist = dataState.playlist
|
|
||||||
val owner = playlist?.owner
|
val owner = playlist?.owner
|
||||||
val ownerName = owner?.displayName ?: owner?.username ?: "Unknown"
|
val ownerName = owner?.displayName ?: owner?.username ?: "Unknown"
|
||||||
val ownerImageURL = owner?.thumbnails?.firstOrNull()?.url
|
val ownerImageURL = owner?.thumbnails?.firstOrNull()?.url
|
||||||
|
|
||||||
val isOwner = currentUserId != null && playlist?.owner?.id == currentUserId
|
val isOwner = currentUserId != null && playlist?.owner?.id == currentUserId
|
||||||
|
|
||||||
val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
val artworkUrl = playlist?.thumbnails?.firstOrNull()?.url.orEmpty()
|
val artworkUrl = playlist?.thumbnails?.firstOrNull()?.url.orEmpty()
|
||||||
val isPlaying = currentCollectionEntry?.id == playlistId &&
|
val isPlaying = currentCollectionEntry?.id == playlistId &&
|
||||||
playerState == PlayerState.PLAYING
|
playerState == PlayerState.PLAYING
|
||||||
|
|
||||||
|
val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
fun getTrackOptionsState(track: MetadataTrack): TrackOptionsState {
|
fun getTrackOptionsState(track: MetadataTrack): TrackOptionsState {
|
||||||
val currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id
|
val currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id
|
||||||
val queueTrackIds = queue.mapNotNull { entry ->
|
val queueTrackIds = queue.mapNotNull { entry ->
|
||||||
@ -192,38 +123,10 @@ fun PlaylistScreen(playlistId: String) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackList(
|
val errorMessage = (state as? PlaylistScreenState.Error)?.message
|
||||||
modifier = Modifier.padding(innerPadding),
|
val isLoading = state is PlaylistScreenState.Loading && (dataState == null || dataState.tracks.isEmpty())
|
||||||
state = listState,
|
|
||||||
collapsedHeader = {
|
val footerContent: (@Composable () -> Unit)? = if (isOwner && dataState != null) {
|
||||||
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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -240,11 +143,28 @@ fun PlaylistScreen(playlistId: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else null,
|
} else null
|
||||||
tracks = dataState.tracks,
|
|
||||||
error = null,
|
CollectionView(
|
||||||
hasMore = dataState.nextPagination != null,
|
title = playlist?.title ?: "Loading playlist...",
|
||||||
isLoading = state is PlaylistScreenState.Loading && dataState.tracks.isEmpty(),
|
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,
|
isLoadingNextPage = state is PlaylistScreenState.Data.LoadingMore,
|
||||||
currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id,
|
currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id,
|
||||||
isCurrentTrackPlaying = playerState == PlayerState.PLAYING,
|
isCurrentTrackPlaying = playerState == PlayerState.PLAYING,
|
||||||
@ -267,17 +187,14 @@ fun PlaylistScreen(playlistId: String) {
|
|||||||
tracksToAddToPlaylist = tracks
|
tracksToAddToPlaylist = tracks
|
||||||
showAddToPlaylistPicker = true
|
showAddToPlaylistPicker = true
|
||||||
},
|
},
|
||||||
)
|
footerContent = footerContent,
|
||||||
}
|
trailingContent = {
|
||||||
}
|
val loadedPlaylist = (dataState as? PlaylistScreenState.Data.Loaded)?.playlist
|
||||||
|
|
||||||
val dataState = state as? PlaylistScreenState.Data
|
|
||||||
val playlist = (dataState as? PlaylistScreenState.Data.Loaded)?.playlist
|
|
||||||
PlaylistFormSheet(
|
PlaylistFormSheet(
|
||||||
visible = showEditPlaylist,
|
visible = showEditPlaylist,
|
||||||
onDismiss = { showEditPlaylist = false },
|
onDismiss = { showEditPlaylist = false },
|
||||||
isEditing = true,
|
isEditing = true,
|
||||||
initialData = playlist?.let {
|
initialData = loadedPlaylist?.let {
|
||||||
PlaylistFormData(
|
PlaylistFormData(
|
||||||
name = it.title,
|
name = it.title,
|
||||||
description = it.description.orEmpty(),
|
description = it.description.orEmpty(),
|
||||||
@ -319,5 +236,6 @@ fun PlaylistScreen(playlistId: String) {
|
|||||||
viewModel.refresh()
|
viewModel.refresh()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,18 +17,13 @@
|
|||||||
|
|
||||||
package dev.krtirtho.spotube.modules.saved_tracks
|
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.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayer
|
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.NavigationCommands
|
||||||
import dev.krtirtho.spotube.core.navigation.Routes
|
import dev.krtirtho.spotube.core.navigation.Routes
|
||||||
import dev.krtirtho.spotube.core.share.ShareService
|
import dev.krtirtho.spotube.core.share.ShareService
|
||||||
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
import dev.krtirtho.spotube.core.ui.component.CollectionView
|
||||||
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.TrackOptionsAction
|
import dev.krtirtho.spotube.core.ui.component.TrackOptionsAction
|
||||||
import dev.krtirtho.spotube.core.ui.component.TrackOptionsState
|
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.downloads.DownloadsViewModel
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
||||||
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
import dev.krtirtho.spotube.modules.library.playlist.AddToPlaylistPicker
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.compose.resources.DrawableResource
|
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
@ -80,14 +69,6 @@ fun SavedTracksScreen() {
|
|||||||
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
||||||
var currentUserId by remember { mutableStateOf<String?>(null) }
|
var currentUserId by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
|
||||||
val isCollapsedHeaderShown by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
listState.firstVisibleItemIndex > 0 ||
|
|
||||||
listState.firstVisibleItemScrollOffset > 400
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
currentUserId = libraryRepository.currentUser()?.id
|
currentUserId = libraryRepository.currentUser()?.id
|
||||||
}
|
}
|
||||||
@ -122,81 +103,16 @@ fun SavedTracksScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
val dataState = state as? SavedTracksScreenState.Data
|
||||||
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() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is SavedTracksScreenState.Error -> {
|
|
||||||
ErrorDisplay(
|
|
||||||
errorMessage = (state as SavedTracksScreenState.Error).message,
|
|
||||||
onRetry = { viewModel.refresh() },
|
|
||||||
modifier = Modifier.padding(innerPadding),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is SavedTracksScreenState.Data -> {
|
|
||||||
val dataState = state as SavedTracksScreenState.Data
|
|
||||||
|
|
||||||
val isPlaying = currentCollectionEntry is QueueCollectionEntry.SavedTracks &&
|
val isPlaying = currentCollectionEntry is QueueCollectionEntry.SavedTracks &&
|
||||||
playerState == PlayerState.PLAYING
|
playerState == PlayerState.PLAYING
|
||||||
|
|
||||||
TrackList(
|
val errorMessage = (state as? SavedTracksScreenState.Error)?.message
|
||||||
modifier = Modifier.padding(innerPadding),
|
val isLoading = state is SavedTracksScreenState.Loading && (dataState == null || dataState.tracks.isEmpty())
|
||||||
state = listState,
|
|
||||||
collapsedHeader = {
|
CollectionView(
|
||||||
CollapsedCollectionHeader(
|
|
||||||
title = "Saved Tracks",
|
title = "Saved Tracks",
|
||||||
imageURL = "",
|
description = dataState?.let { "${it.totalCount} tracks" } ?: "",
|
||||||
imageResource = Res.drawable.liked_tracks,
|
|
||||||
isPlaying = isPlaying,
|
|
||||||
onPlay = viewModel::playSavedTracks,
|
|
||||||
onBack = { navigationCommands.pop() },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
isCollapsedHeaderShown = isCollapsedHeaderShown,
|
|
||||||
headerContent = {
|
|
||||||
CollectionDetails(
|
|
||||||
title = "Saved Tracks",
|
|
||||||
description = "${dataState.totalCount} tracks",
|
|
||||||
imageURL = "",
|
imageURL = "",
|
||||||
imageResource = Res.drawable.liked_tracks,
|
imageResource = Res.drawable.liked_tracks,
|
||||||
ownerName = "You",
|
ownerName = "You",
|
||||||
@ -209,12 +125,11 @@ fun SavedTracksScreen() {
|
|||||||
isFollowing = false,
|
isFollowing = false,
|
||||||
onFollowClick = {},
|
onFollowClick = {},
|
||||||
showFollowButton = false,
|
showFollowButton = false,
|
||||||
)
|
isLoading = isLoading,
|
||||||
},
|
error = errorMessage,
|
||||||
tracks = dataState.tracks,
|
onRetry = { viewModel.refresh() },
|
||||||
error = null,
|
tracks = dataState?.tracks ?: emptyList(),
|
||||||
hasMore = dataState.nextPagination != null,
|
hasMore = dataState?.nextPagination != null,
|
||||||
isLoading = state is SavedTracksScreenState.Loading && dataState.tracks.isEmpty(),
|
|
||||||
isLoadingNextPage = state is SavedTracksScreenState.Data.LoadingMore,
|
isLoadingNextPage = state is SavedTracksScreenState.Data.LoadingMore,
|
||||||
currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id,
|
currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id,
|
||||||
isCurrentTrackPlaying = playerState == PlayerState.PLAYING,
|
isCurrentTrackPlaying = playerState == PlayerState.PLAYING,
|
||||||
@ -237,10 +152,7 @@ fun SavedTracksScreen() {
|
|||||||
tracksToAddToPlaylist = tracks
|
tracksToAddToPlaylist = tracks
|
||||||
showAddToPlaylistPicker = true
|
showAddToPlaylistPicker = true
|
||||||
},
|
},
|
||||||
)
|
trailingContent = {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddToPlaylistPicker(
|
AddToPlaylistPicker(
|
||||||
visible = showAddToPlaylistPicker,
|
visible = showAddToPlaylistPicker,
|
||||||
currentUserId = currentUserId,
|
currentUserId = currentUserId,
|
||||||
@ -254,5 +166,6 @@ fun SavedTracksScreen() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user