diff --git a/.gitignore b/.gitignore index 7a3faf1f..cfc7da21 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,8 @@ tm.json android/build android/app/.cxx + +/node_modules +/package.json +/package-lock.json +/bun.lock \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/dev/krtirtho/spotube/media/PlaybackService.kt b/composeApp/src/androidMain/kotlin/dev/krtirtho/spotube/media/PlaybackService.kt index f16a1ffe..a19c40fc 100644 --- a/composeApp/src/androidMain/kotlin/dev/krtirtho/spotube/media/PlaybackService.kt +++ b/composeApp/src/androidMain/kotlin/dev/krtirtho/spotube/media/PlaybackService.kt @@ -25,6 +25,7 @@ import android.content.Intent import android.os.Build import android.util.Log import androidx.annotation.OptIn +import androidx.core.app.NotificationCompat import androidx.media3.common.MediaItem as Media3MediaItem import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi @@ -76,6 +77,16 @@ class PlaybackService : MediaLibraryService(), KoinComponent { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) + val notification = NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("Spotube") + .setContentText("Playback service") + .setSmallIcon(android.R.drawable.ic_media_play) + .setContentIntent(sessionActivity) + .setOngoing(true) + .build() + + startForeground(NOTIFICATION_ID, notification) + librarySession = MediaLibrarySession.Builder(this, audioPlayer.player, LibrarySessionCallback()) .setSessionActivity(sessionActivity) .build() @@ -388,5 +399,6 @@ class PlaybackService : MediaLibraryService(), KoinComponent { companion object { private const val TAG = "PlaybackService" private const val CHANNEL_ID = "spotube_playback" + private const val NOTIFICATION_ID = 1 } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt index f82904cf..cc578a25 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt @@ -17,6 +17,8 @@ package dev.krtirtho.spotube +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable @@ -34,6 +36,7 @@ import dev.krtirtho.spotube.core.navigation.Routes import dev.krtirtho.spotube.core.navigation.TOP_LEVEL_ROUTES import dev.krtirtho.spotube.core.navigation.rememberNavigationState import dev.krtirtho.spotube.core.navigation.toEntries +import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope import dev.krtirtho.spotube.core.ui.theming.SpotubeTheme import dev.krtirtho.spotube.modules.settings.SettingsRepository import dev.krtirtho.spotube.modules.settings.UserSettings @@ -86,7 +89,7 @@ val tabs = listOf( ) ) -@OptIn(KoinExperimentalAPI::class, ExperimentalCoroutinesApi::class) +@OptIn(KoinExperimentalAPI::class, ExperimentalCoroutinesApi::class, ExperimentalSharedTransitionApi::class) @Composable fun App( content: @Composable () -> Unit = {} @@ -106,12 +109,18 @@ fun App( val baseUITheme = rememberBaseUITheme() CompositionLocalProvider(LocalBaseUITheme provides baseUITheme) { AppShell(navigator, navigationState) { - Column { - NavDisplay( - modifier = Modifier.fillMaxSize(), - onBack = navigator::pop, - entries = navigationState.toEntries(koinEntryProvider()) - ) + SharedTransitionLayout { + CompositionLocalProvider( + LocalSharedTransitionScope provides this@SharedTransitionLayout, + ) { + Column { + NavDisplay( + modifier = Modifier.fillMaxSize(), + onBack = navigator::pop, + entries = navigationState.toEntries(koinEntryProvider()) + ) + } + } } } content() diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AlbumCard.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AlbumCard.kt index 3dfcd375..2e021980 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AlbumCard.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AlbumCard.kt @@ -60,5 +60,6 @@ fun AlbumCard( scope.launch { playbackHelper.addAlbumToQueue(album.id) } }, modifier = modifier.width(160.dp), + sharedElementKey = "album_art_${album.id}", ) } 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 0cbb24e4..7c0c73d8 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 @@ -17,6 +17,7 @@ package dev.krtirtho.spotube.core.ui.component +import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -30,9 +31,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults 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 @@ -41,16 +41,17 @@ 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.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import dev.krtirtho.spotube.core.ui.base.ButtonGroup import dev.krtirtho.spotube.core.ui.base.ButtonGroupDivider +import dev.krtirtho.spotube.core.ui.base.Card import dev.krtirtho.spotube.core.ui.base.GroupIconButton 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 @@ -64,6 +65,7 @@ import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.painterResource +@OptIn(ExperimentalSharedTransitionApi::class) @Composable fun CollectionDetails( modifier: Modifier = Modifier, @@ -82,12 +84,14 @@ fun CollectionDetails( onFollowClick: () -> Unit = { }, showFollowButton: Boolean = true, onEdit: (() -> Unit)? = null, + sharedElementKey: String? = null, ) { - - BoxWithConstraints(modifier = modifier.fillMaxWidth()) { val isCompact = maxWidth < 600.dp + val sharedTransitionScope = LocalSharedTransitionScope.current + val animatedVisibilityScope = LocalAnimatedVisibilityScope.current + val playPauseButton = @Composable { Row( @@ -207,6 +211,11 @@ fun CollectionDetails( modifier = Modifier .size(if (isCompact) 100.dp else 200.dp) .clip(RoundedCornerShape(12.dp)) + .sharedElementOrNone( + key = sharedElementKey, + sharedTransitionScope = sharedTransitionScope, + animatedVisibilityScope = animatedVisibilityScope, + ) .shimmerApply(), contentAlignment = Alignment.Center, ) { @@ -260,10 +269,6 @@ fun CollectionDetails( modifier = Modifier .fillMaxWidth() .padding(vertical = 8.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), - shape = RoundedCornerShape(20.dp), ) { if (isCompact) { Column( @@ -314,8 +319,104 @@ fun CollectionDetails( } } +@OptIn(ExperimentalSharedTransitionApi::class) @Composable -@Preview +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() { CollectionDetails( title = "My Playlist", @@ -328,4 +429,4 @@ fun CollectionDetailsPreview() { onShufflePlay = {}, onAddToQueue = {}, ) -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/LikedTracksCard.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/LikedTracksCard.kt index 635a10a5..eca9b2e2 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/LikedTracksCard.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/LikedTracksCard.kt @@ -18,6 +18,7 @@ package dev.krtirtho.spotube.core.ui.component import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.clickable @@ -55,6 +56,9 @@ import dev.krtirtho.spotube.core.audioplayer.QueueCollectionEntry import dev.krtirtho.spotube.core.navigation.NavigationCommands import dev.krtirtho.spotube.core.navigation.Routes import dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper +import dev.krtirtho.spotube.core.ui.component.LocalAnimatedVisibilityScope +import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope +import dev.krtirtho.spotube.core.ui.component.sharedElementOrNone import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare import dev.krtirtho.spotube.resources.iconsax.IconsaxPauseCircle @@ -66,6 +70,7 @@ import org.koin.compose.koinInject import spotube.composeapp.generated.resources.Res import spotube.composeapp.generated.resources.liked_tracks +@OptIn(ExperimentalSharedTransitionApi::class) @Composable fun LikedTracksCard( modifier: Modifier = Modifier, @@ -79,6 +84,9 @@ fun LikedTracksCard( val currentCollectionEntry by audioPlayerQueue.currentCollectionEntryFlow.collectAsStateWithLifecycle() val isPlaying = currentCollectionEntry is QueueCollectionEntry.SavedTracks + val sharedTransitionScope = LocalSharedTransitionScope.current + val animatedVisibilityScope = LocalAnimatedVisibilityScope.current + Box( modifier = modifier .clip(RoundedCornerShape(8.dp)) @@ -91,8 +99,15 @@ fun LikedTracksCard( androidx.compose.foundation.Image( painter = painterResource(Res.drawable.liked_tracks), contentDescription = "Liked Tracks", - modifier = Modifier.fillMaxWidth().aspectRatio(1f) - .clip(RoundedCornerShape(8.dp)), + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(8.dp)) + .sharedElementOrNone( + key = "saved_tracks_art", + sharedTransitionScope = sharedTransitionScope, + animatedVisibilityScope = animatedVisibilityScope, + ), contentScale = ContentScale.Crop, ) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/PlaylistCard.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/PlaylistCard.kt index d604f23e..1bca2308 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/PlaylistCard.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/PlaylistCard.kt @@ -57,6 +57,7 @@ fun PlaylistCard( onAddToQueue = { scope.launch { playbackHelper.addPlaylistToQueue(playlist.id) } }, - modifier = modifier + modifier = modifier, + sharedElementKey = "playlist_art_${playlist.id}", ) } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/SharedTransitionLocals.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/SharedTransitionLocals.kt new file mode 100644 index 00000000..64c05db0 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/SharedTransitionLocals.kt @@ -0,0 +1,47 @@ +/* + * 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.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalSharedTransitionApi::class) +val LocalSharedTransitionScope = compositionLocalOf { null } + +@OptIn(ExperimentalSharedTransitionApi::class) +val LocalAnimatedVisibilityScope = compositionLocalOf { null } + +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +fun Modifier.sharedElementOrNone( + key: String?, + sharedTransitionScope: SharedTransitionScope?, + animatedVisibilityScope: AnimatedVisibilityScope?, +): Modifier { + if (sharedTransitionScope == null || animatedVisibilityScope == null || key == null) return this + with(sharedTransitionScope) { + return this@sharedElementOrNone.sharedElement( + sharedContentState = rememberSharedContentState(key = key), + animatedVisibilityScope = animatedVisibilityScope, + ) + } +} 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 a0ff05dc..ebfc537c 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,8 +18,11 @@ 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 @@ -42,6 +45,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider @@ -149,6 +153,9 @@ fun TrackList( contentPadding: PaddingValues = PaddingValues(bottom = LocalAppShellBottomInset.current), 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) } @@ -183,7 +190,7 @@ fun TrackList( } } - val listState = rememberLazyListState() + val listState = state ?: rememberLazyListState() val density = LocalDensity.current val shouldLoadMore = remember(density) { derivedStateOf { @@ -484,6 +491,17 @@ 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/core/ui/component/cards/PlayableCard.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/cards/PlayableCard.kt index dd1f0f36..14aee52b 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/cards/PlayableCard.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/cards/PlayableCard.kt @@ -18,6 +18,7 @@ package dev.krtirtho.spotube.core.ui.component.cards import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.clickable @@ -53,6 +54,9 @@ import dev.krtirtho.spotube.core.ui.base.BaseUITheme import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme import dev.krtirtho.spotube.core.ui.base.PrimaryIconButton import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton +import dev.krtirtho.spotube.core.ui.component.LocalAnimatedVisibilityScope +import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope +import dev.krtirtho.spotube.core.ui.component.sharedElementOrNone import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer import dev.krtirtho.spotube.core.ui.misc.shimmerApply import dev.krtirtho.spotube.resources.iconsax.Iconsax @@ -60,6 +64,7 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare import dev.krtirtho.spotube.resources.iconsax.IconsaxPause import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay +@OptIn(ExperimentalSharedTransitionApi::class) @Composable fun PlayableCard( title: String, @@ -70,8 +75,11 @@ fun PlayableCard( onPlay: (() -> Unit)? = null, onAddToQueue: (() -> Unit)? = null, modifier: Modifier = Modifier, + sharedElementKey: String? = null, ) { val interactionSource = remember { MutableInteractionSource() } + val sharedTransitionScope = LocalSharedTransitionScope.current + val animatedVisibilityScope = LocalAnimatedVisibilityScope.current Box( modifier = modifier @@ -85,8 +93,15 @@ fun PlayableCard( AsyncImage( model = imageURL, contentDescription = title, - modifier = Modifier.fillMaxWidth().aspectRatio(1f) - .clip(RoundedCornerShape(8.dp)), + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(8.dp)) + .sharedElementOrNone( + key = sharedElementKey, + sharedTransitionScope = sharedTransitionScope, + animatedVisibilityScope = animatedVisibilityScope, + ), contentScale = ContentScale.Crop, ) @@ -98,7 +113,6 @@ fun PlayableCard( val isHovered by interactionSource.collectIsHoveredAsState() this@Column.AnimatedVisibility( visible = isHovered, - //fade in/out animation when hovered enter = fadeIn(), exit = fadeOut(), ) { @@ -186,4 +200,4 @@ private fun PlayableCardPreview() { onAddToQueue = {} ) } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumRepository.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumRepository.kt index 23e8be24..a8d2e8ec 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumRepository.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/album/AlbumRepository.kt @@ -52,12 +52,17 @@ class AlbumRepository( .filterNotNull() .flatMapLatest { it.loggedInFlow } .distinctUntilChanged() - .collect { + .collect { loggedIn -> + isLoggedIn = loggedIn invalidateCaches() } } } + private var isLoggedIn: Boolean = false + + private val authPlugin get() = if (isLoggedIn) plugin else null + fun invalidateCaches() { albumInfoCache.invalidateAll() albumTracksCache.invalidateAll() @@ -68,17 +73,15 @@ class AlbumRepository( pluginManager.withScope { plugin.use { val album = metadataAlbumAPI.getAlbum(albumId) - val isSaved = metadataAlbumAPI.isSavedAlbums(listOf(albumId)).firstOrNull() ?: false + val isSaved = libraryRepository.isSavedAlbums(listOf(albumId)).firstOrNull() ?: false album to isSaved } } - }?.also { - libraryRepository.isSavedAlbums(listOf(albumId)) } } suspend fun getAlbumTracks(albumId: String, paginationStrategy: PaginationStrategy? = null) = - plugin?.let { plugin -> + authPlugin?.let { plugin -> albumTracksCache.get(albumId to (paginationStrategy ?: PaginationStrategy.Offset(0, 20))) { pluginManager.withScope { plugin.use { 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 a16c7b4f..c4074c61 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 @@ -18,9 +18,11 @@ 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 @@ -37,6 +39,7 @@ 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 @@ -75,6 +78,14 @@ 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 } @@ -110,7 +121,7 @@ fun AlbumScreen(albumId: String) { } Scaffold( - topBar = { ApplicationMainBar() } + topBar = { ApplicationMainBar(backButton = !isCollapsedHeaderShown) } ) { innerPadding -> when (state) { is AlbumScreenState.Loading -> { @@ -165,14 +176,28 @@ fun AlbumScreen(albumId: String) { 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() }, + ) + }, + isCollapsedHeaderShown = isCollapsedHeaderShown, headerContent = { CollectionDetails( title = album?.title ?: "Loading album...", description = album?.description ?: "${album?.albumType?.name.orEmpty()} • ${album?.releaseDate.orEmpty()}", - imageURL = album?.thumbnails?.firstOrNull()?.url.orEmpty(), + imageURL = artworkUrl, ownerName = ownerName, ownerImageURL = ownerImageURL, onOwnerClick = { @@ -185,9 +210,7 @@ fun AlbumScreen(albumId: String) { onPlay = viewModel::playAlbum, onShufflePlay = {}, onAddToQueue = viewModel::addAlbumToQueue, - isPlaying = - currentCollectionEntry?.id == albumId && - playerState == PlayerState.PLAYING, + isPlaying = isAlbumPlaying, isFollowing = savedAlbumIds.contains(albumId), onFollowClick = viewModel::toggleSavedAlbum, ) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/LibraryRepository.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/LibraryRepository.kt index dcf8a566..bc1129cf 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/LibraryRepository.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/LibraryRepository.kt @@ -68,12 +68,17 @@ class LibraryRepository( .filterNotNull() .flatMapLatest { it.loggedInFlow } .distinctUntilChanged() - .collect { + .collect { loggedIn -> + isLoggedIn = loggedIn invalidateCaches() } } } + private var isLoggedIn: Boolean = false + + private val authPlugin get() = if (isLoggedIn) plugin else null + fun invalidateCaches() { playlistCache.invalidateAll() albumCache.invalidateAll() @@ -84,7 +89,7 @@ class LibraryRepository( } suspend fun savedPlaylists(paginationStrategy: PaginationStrategy? = null) = - plugin?.let { plugin -> + authPlugin?.let { plugin -> playlistCache.get( key = paginationStrategy ?: PaginationStrategy.Offset(0, 20) ) { @@ -99,7 +104,7 @@ class LibraryRepository( } suspend fun savedAlbums(paginationStrategy: PaginationStrategy? = null) = - plugin?.let { plugin -> + authPlugin?.let { plugin -> albumCache.get( key = paginationStrategy ?: PaginationStrategy.Offset(0, 20) ) { @@ -114,7 +119,7 @@ class LibraryRepository( } suspend fun savedArtists(paginationStrategy: PaginationStrategy? = null) = - plugin?.let { plugin -> + authPlugin?.let { plugin -> artistCache.get( key = paginationStrategy ?: PaginationStrategy.Offset(0, 20) ) { @@ -131,11 +136,11 @@ class LibraryRepository( suspend fun isSavedPlaylists(ids: List): List { val unknownIds = ids.filterNot { savedPlaylistIds.value.contains(it) } - if(unknownIds.isEmpty()) { + if (unknownIds.isEmpty()) { return ids.map { true } } - val unknownStates = plugin?.let { plugin -> + val unknownStates = authPlugin?.let { plugin -> val savedStates = pluginManager.withScope { plugin.use { metadataPlaylistAPI.isSavedPlaylists(unknownIds) @@ -155,7 +160,7 @@ class LibraryRepository( } suspend fun savePlaylists(ids: List) { - plugin?.let { plugin -> + authPlugin?.let { plugin -> pluginManager.withScope { plugin.use { metadataPlaylistAPI.savePlaylists(ids) @@ -167,7 +172,7 @@ class LibraryRepository( } suspend fun removeSavedPlaylists(ids: List) { - plugin?.let { plugin -> + authPlugin?.let { plugin -> pluginManager.withScope { plugin.use { metadataPlaylistAPI.removeSavedPlaylists(ids) @@ -181,11 +186,11 @@ class LibraryRepository( suspend fun isSavedAlbums(ids: List): List { val unknownIds = ids.filterNot { savedAlbumIds.value.contains(it) } - if(unknownIds.isEmpty()) { + if (unknownIds.isEmpty()) { return ids.map { true } } - val unknownStates = plugin?.let { plugin -> + val unknownStates = authPlugin?.let { plugin -> val savedStates = pluginManager.withScope { plugin.use { metadataAlbumAPI.isSavedAlbums(unknownIds) @@ -205,7 +210,7 @@ class LibraryRepository( } suspend fun saveAlbums(ids: List) { - plugin?.let { plugin -> + authPlugin?.let { plugin -> pluginManager.withScope { plugin.use { metadataAlbumAPI.saveAlbums(ids) @@ -217,7 +222,7 @@ class LibraryRepository( } suspend fun removeSavedAlbums(ids: List) { - plugin?.let { plugin -> + authPlugin?.let { plugin -> pluginManager.withScope { plugin.use { metadataAlbumAPI.removeSavedAlbums(ids) @@ -231,11 +236,11 @@ class LibraryRepository( suspend fun isSavedArtists(ids: List): List { val unknownIds = ids.filterNot { savedArtistIds.value.contains(it) } - if(unknownIds.isEmpty()) { + if (unknownIds.isEmpty()) { return ids.map { true } } - val unknownStates = plugin?.let { plugin -> + val unknownStates = authPlugin?.let { plugin -> val savedStates = pluginManager.withScope { plugin.use { metadataArtistAPI.isSavedArtists(unknownIds) @@ -255,7 +260,7 @@ class LibraryRepository( } suspend fun saveArtists(ids: List) { - plugin?.let { plugin -> + authPlugin?.let { plugin -> pluginManager.withScope { plugin.use { metadataArtistAPI.saveArtists(ids) @@ -267,7 +272,7 @@ class LibraryRepository( } suspend fun removeSavedArtists(ids: List) { - plugin?.let { plugin -> + authPlugin?.let { plugin -> pluginManager.withScope { plugin.use { metadataArtistAPI.removeSavedArtists(ids) @@ -279,7 +284,7 @@ class LibraryRepository( } suspend fun currentUser(): MetadataUser? { - return plugin?.let { plugin -> + return authPlugin?.let { plugin -> pluginManager.withScope { plugin.use { metadataUserAPI.getUser("") @@ -296,7 +301,7 @@ class LibraryRepository( imageBase64: String, trackIds: List, ): MetadataPlaylist? { - return plugin?.let { plugin -> + return authPlugin?.let { plugin -> pluginManager.withScope { plugin.use { val result = metadataPlaylistAPI.createPlaylist( @@ -324,7 +329,7 @@ class LibraryRepository( imageBase64: String?, trackIds: List?, ): MetadataPlaylist? { - return plugin?.let { plugin -> + return authPlugin?.let { plugin -> pluginManager.withScope { plugin.use { val result = metadataPlaylistAPI.updatePlaylist( @@ -344,7 +349,7 @@ class LibraryRepository( } suspend fun addTracksToPlaylist(playlistId: String, trackIds: List) { - plugin?.let { plugin -> + authPlugin?.let { plugin -> pluginManager.withScope { plugin.use { metadataPlaylistAPI.addTracksToPlaylist(playlistId, trackIds) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistRepository.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistRepository.kt index bdaf3860..c7d444f6 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistRepository.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistRepository.kt @@ -53,12 +53,15 @@ class PlaylistRepository( .filterNotNull() .flatMapLatest { it.loggedInFlow } .distinctUntilChanged() - .collect { + .collect { loggedIn -> + isLoggedIn = loggedIn invalidateCaches() } } } + private var isLoggedIn: Boolean = false + fun invalidateCaches() { playlistInfoCache.invalidateAll() playlistTracksCache.invalidateAll() @@ -69,12 +72,10 @@ class PlaylistRepository( pluginManager.withScope { plugin.use { val playlist = metadataPlaylistAPI.getPlaylist(playlistId) - val isSaved = metadataPlaylistAPI.isSavedPlaylists(listOf(playlistId)).firstOrNull() ?: false + val isSaved = libraryRepository.isSavedPlaylists(listOf(playlistId)).firstOrNull() ?: false playlist to isSaved } } - }.also { - libraryRepository.isSavedPlaylists(listOf(playlistId)) } } 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 8cf52b9f..b0a6032d 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,10 +21,12 @@ 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 @@ -43,6 +45,7 @@ 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 @@ -87,6 +90,14 @@ 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) { @@ -105,7 +116,7 @@ fun PlaylistScreen(playlistId: String) { } Scaffold( - topBar = { ApplicationMainBar() } + topBar = { ApplicationMainBar(backButton = !isCollapsedHeaderShown) } ) { innerPadding -> when (state) { is PlaylistScreenState.Loading -> { @@ -164,6 +175,9 @@ fun PlaylistScreen(playlistId: String) { 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 @@ -180,20 +194,29 @@ fun PlaylistScreen(playlistId: String) { 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 = playlist?.thumbnails?.firstOrNull()?.url.orEmpty(), + imageURL = artworkUrl, ownerName = ownerName, ownerImageURL = ownerImageURL, onOwnerClick = {}, onPlay = viewModel::playPlaylist, onShufflePlay = {}, onAddToQueue = viewModel::addPlaylistToQueue, - isPlaying = - currentCollectionEntry?.id == playlistId && - playerState == PlayerState.PLAYING, + isPlaying = isPlaying, isFollowing = savedPlaylistIds.contains(playlistId), onFollowClick = viewModel::toggleSavedPlaylist, showFollowButton = playlistId != "saved_tracks", diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistViewModel.kt index dcedfdff..6fc31b94 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistViewModel.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/playlist/PlaylistViewModel.kt @@ -111,7 +111,11 @@ class PlaylistViewModel( } private suspend fun loadCurrentUser() { - _currentUserId.value = libraryRepository.currentUser()?.id + runCatching { + _currentUserId.value = libraryRepository.currentUser()?.id + }.onFailure { e -> + logger.e(e) { "Failed to load current user" } + } } private suspend fun loadInitialData() = runCatching { @@ -159,11 +163,15 @@ class PlaylistViewModel( fun toggleSavedPlaylist() { viewModelScope.launch { - val isLiked = libraryRepository.isSavedPlaylists(listOf(playlistId)).firstOrNull() ?: false - if (isLiked) { - libraryRepository.removeSavedPlaylists(listOf(playlistId)) - } else { - libraryRepository.savePlaylists(listOf(playlistId)) + runCatching { + val isLiked = libraryRepository.isSavedPlaylists(listOf(playlistId)).firstOrNull() ?: false + if (isLiked) { + libraryRepository.removeSavedPlaylists(listOf(playlistId)) + } else { + libraryRepository.savePlaylists(listOf(playlistId)) + } + }.onFailure { e -> + logger.e(e) { "Failed to toggle saved playlist" } } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksRepository.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksRepository.kt index c538d6df..e5a7a2e9 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksRepository.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/saved_tracks/SavedTracksRepository.kt @@ -56,12 +56,17 @@ class SavedTracksRepository( .filterNotNull() .flatMapLatest { it.loggedInFlow } .distinctUntilChanged() - .collect { + .collect { loggedIn -> + isLoggedIn = loggedIn invalidateCaches() } } } + private var isLoggedIn: Boolean = false + + private val authPlugin get() = if (isLoggedIn) plugin else null + fun invalidateCaches() { savedTracksCache.invalidateAll() totalCountCache.invalidateAll() @@ -69,7 +74,7 @@ class SavedTracksRepository( } suspend fun getSavedTracks(paginationStrategy: PaginationStrategy? = null) = - plugin?.let { plugin -> + authPlugin?.let { plugin -> val strategy = paginationStrategy ?: PaginationStrategy.Offset(0, 50) savedTracksCache.get(strategy) { val tracks = pluginManager.withScope { @@ -95,7 +100,7 @@ class SavedTracksRepository( } suspend fun saveTracks(ids: List) { - plugin?.let { plugin -> + authPlugin?.let { plugin -> pluginManager.withScope { plugin.use { metadataTrackAPI.saveTracks(ids) @@ -108,7 +113,7 @@ class SavedTracksRepository( } suspend fun removeSavedTracks(ids: List) { - plugin?.let { plugin -> + authPlugin?.let { plugin -> pluginManager.withScope { plugin.use { metadataTrackAPI.removeSavedTracks(ids) 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 168d5a61..9acd85bb 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 @@ -18,9 +18,11 @@ 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 @@ -38,6 +40,7 @@ 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 @@ -77,6 +80,14 @@ 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 } @@ -112,7 +123,7 @@ fun SavedTracksScreen() { } Scaffold( - topBar = { ApplicationMainBar() } + topBar = { ApplicationMainBar(backButton = !isCollapsedHeaderShown) } ) { innerPadding -> when (state) { is SavedTracksScreenState.Loading -> { @@ -165,8 +176,23 @@ fun SavedTracksScreen() { 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() }, + ) + }, + isCollapsedHeaderShown = isCollapsedHeaderShown, headerContent = { CollectionDetails( title = "Saved Tracks", @@ -179,8 +205,7 @@ fun SavedTracksScreen() { onPlay = viewModel::playSavedTracks, onShufflePlay = {}, onAddToQueue = viewModel::addSavedTracksToQueue, - isPlaying = currentCollectionEntry is QueueCollectionEntry.SavedTracks && - playerState == PlayerState.PLAYING, + isPlaying = isPlaying, isFollowing = false, onFollowClick = {}, showFollowButton = false, diff --git a/js_plugin_example/src/jsMain/kotlin/dev/krtirtho/js_plugin_example/plugin_apis/metadata/RealMetadataUserAPI.kt b/js_plugin_example/src/jsMain/kotlin/dev/krtirtho/js_plugin_example/plugin_apis/metadata/RealMetadataUserAPI.kt index 9fa844cf..cc86648d 100644 --- a/js_plugin_example/src/jsMain/kotlin/dev/krtirtho/js_plugin_example/plugin_apis/metadata/RealMetadataUserAPI.kt +++ b/js_plugin_example/src/jsMain/kotlin/dev/krtirtho/js_plugin_example/plugin_apis/metadata/RealMetadataUserAPI.kt @@ -23,8 +23,4 @@ class RealMetadataUserAPI : MetadataUserAPI { override suspend fun getUser(id: String): MetadataUser? { return FakeMetadataStore.getUser(id) } - - override suspend fun me(): MetadataUser? { - return FakeMetadataStore.getCurrentUser() - } } \ No newline at end of file