diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 00000000..4c36a5e5 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,17 @@ +{ + "mcpServers": { + "compose-hot-reload": { + "command": "./gradlew", + "args": [ + "--no-daemon", + "--quiet", + "--console=plain", + "hotMcpServer" + ] + }, + "studio": { + "url": "http://127.0.0.1:64342/stream", + "type": "http" + } + } +} \ No newline at end of file diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index a8304a54..f733b628 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -13,19 +13,9 @@ "hotMcpServer" ] }, - "android-studio-mcp": { - "type": "local", - "environment": { - "IJ_MCP_SERVER_PORT": "64342" - }, - "enabled": true, - "disabled": false, - "command": [ - "/opt/android-studio/jbr/bin/java", - "-classpath", - "/home/krtirtho/.local/share/Google/AndroidStudio2026.1.4/mcpserver/lib/mcpserver.jar:/home/krtirtho/.local/share/Google/AndroidStudio2026.1.4/mcpserver/lib/io.modelcontextprotocol.kotlin.sdk.jar:/home/krtirtho/.local/share/Google/AndroidStudio2026.1.4/mcpserver/lib/io.github.oshai.kotlin.logging.jvm.jar:/opt/android-studio/lib/util-8.jar:/opt/android-studio/lib/intellij.libraries.kotlinx.coroutines.core.jar:/opt/android-studio/lib/intellij.libraries.ktor.client.cio.jar:/opt/android-studio/lib/intellij.libraries.ktor.client.jar:/opt/android-studio/lib/intellij.libraries.ktor.network.tls.jar:/opt/android-studio/lib/intellij.libraries.ktor.io.jar:/opt/android-studio/lib/intellij.libraries.ktor.utils.jar:/opt/android-studio/lib/intellij.libraries.kotlinx.io.jar:/opt/android-studio/lib/intellij.libraries.kotlinx.serialization.core.jar:/opt/android-studio/lib/intellij.libraries.kotlinx.serialization.json.jar", - "com.intellij.mcpserver.stdio.McpStdioRunnerKt" - ] + "studio": { + "url": "http://127.0.0.1:64342/stream", + "type": "remote" } } } \ 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 456de393..4eacf176 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 @@ -35,6 +35,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import dev.krtirtho.spotube.core.ui.theming.AppTheme import com.composeunstyled.Text +import com.composeunstyled.LocalTextStyle import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope @@ -158,7 +159,8 @@ fun LikedTracksCard( Iconsax.IconsaxPauseCircle } else Iconsax.Play, contentDescription = stringResource(Res.string.player_play_content_description), - modifier = Modifier.size(16.dp) + modifier = Modifier.size(16.dp), + tint = LocalTextStyle.current.color ) } } 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 05edce14..bd79c9e0 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 @@ -21,6 +21,7 @@ package dev.krtirtho.spotube.core.ui.component.cards import org.spotube.millennium.composables.MillenniumIcon import org.spotube.millennium.composables.MillenniumIconButton import org.spotube.millennium.composables.MillenniumIconButtonStyle +import com.composeunstyled.LocalTextStyle import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.fadeIn @@ -143,7 +144,7 @@ fun PlayableCard( imageVector = Iconsax.IconsaxAddSquare, contentDescription = stringResource( Res.string.player_add_to_queue_content_description - ) + ), ) } Spacer(modifier = Modifier.height(8.dp)) @@ -165,7 +166,8 @@ fun PlayableCard( } else { Res.string.player_play_content_description } - ) + ), + tint = LocalTextStyle.current.color ) } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/cards/PlayableListTile.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/cards/PlayableListTile.kt index 253f2155..cff5697b 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/cards/PlayableListTile.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/cards/PlayableListTile.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.composeunstyled.LocalTextStyle import coil3.compose.AsyncImage import dev.krtirtho.spotube.core.ui.component.LocalAnimatedVisibilityScope import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope @@ -130,7 +131,8 @@ fun PlayableListTile( ) { MillenniumIcon( if (isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay, - stringResource(if (isPlaying) Res.string.player_pause_content_description else Res.string.player_play_content_description) + stringResource(if (isPlaying) Res.string.player_pause_content_description else Res.string.player_play_content_description), + tint = LocalTextStyle.current.color ) } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistScreen.kt index a7ab61be..51934952 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistScreen.kt @@ -48,6 +48,7 @@ import androidx.compose.foundation.shape.CircleShape import dev.krtirtho.spotube.core.ui.theming.AppTheme import com.composeunstyled.Text +import com.composeunstyled.LocalTextStyle import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -117,87 +118,67 @@ fun ArtistScreen( Column( modifier = Modifier .fillMaxSize() - .background(MillenniumTheme.colors.background)) { + .background(MillenniumTheme.colors.background) + ) { ApplicationMainBar() Box(modifier = Modifier.fillMaxWidth().weight(1f)) { - when (val currentState = state) { - is ArtistScreenState.Loading -> { - ArtistLoadingContent() - } + when (val currentState = state) { + is ArtistScreenState.Loading -> { + ArtistLoadingContent() + } - is ArtistScreenState.Error -> { - ArtistErrorContent( - message = currentState.message, - onRetry = { viewModel.refresh() } - ) - } + is ArtistScreenState.Error -> { + ArtistErrorContent( + message = currentState.message, + onRetry = { viewModel.refresh() } + ) + } - is ArtistScreenState.Loaded -> { - val listState = rememberLazyListState() + is ArtistScreenState.Loaded -> { + val listState = rememberLazyListState() - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxSize() - .padding(bottom = LocalAppShellBottomInset.current + 24.dp), - verticalArrangement = Arrangement.spacedBy(24.dp)) { - item { - ArtistHeaderCard( - artist = currentState.artist, - isSaved = savedArtistIds.contains(currentState.artist.id), - onFollowClick = viewModel::toggleSavedArtist, - isBlacklisted = currentState.artist.id in blacklistedArtistIds, - onBlacklistClick = { viewModel.toggleArtistBlacklist(currentState.artist) }) - } - - item { - TopTracksHeader( - onPlay = viewModel::playTopTracks, - onAddToQueue = viewModel::addTopTracksToQueue) - } - - item { - TrackList( - tracks = currentState.topTracks, - error = null, - hasMore = false, - isLoading = false, - isLoadingNextPage = false, - currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id, - isCurrentTrackPlaying = playerState == PlayerState.PLAYING, - onTrackClick = viewModel::playTopTracksFromTrack, - onTrackOptionsAction = viewModel::handleTrackOptionsAction, - trackOptionsState = trackOptionsContext::stateFor, - onArtistClick = { trackArtist -> - navigationCommands.navigateTo( - Routes.Artist( - trackArtist.id - ) - ) - }, - onAlbumClick = { album -> - navigationCommands.navigateTo( - Routes.Album( - album.id - ) - ) - }, - onLoadNextPage = {}, - simplified = true, - scrollable = false, - onBulkDownload = viewModel::downloadTracks, - onBulkAddToQueue = viewModel::addTracksToQueue, - onBulkPlayNext = viewModel::playTracksNext, - onBulkAddToPlaylist = viewModel::showAddToPlaylistPicker, - onBulkAddToJam = viewModel::addTracksToJam, - isInJam = jamActive, - isJamGuest = isJamGuest) - } - - if (currentState.albums.isNotEmpty() || currentState.albumsNextPagination != null) { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .padding(bottom = LocalAppShellBottomInset.current + 24.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { item { - AlbumsSection( - albums = currentState.albums, + ArtistHeaderCard( + artist = currentState.artist, + isSaved = savedArtistIds.contains(currentState.artist.id), + onFollowClick = viewModel::toggleSavedArtist, + isBlacklisted = currentState.artist.id in blacklistedArtistIds, + onBlacklistClick = { viewModel.toggleArtistBlacklist(currentState.artist) }) + } + + item { + TopTracksHeader( + onPlay = viewModel::playTopTracks, + onAddToQueue = viewModel::addTopTracksToQueue + ) + } + + item { + TrackList( + tracks = currentState.topTracks, + error = null, + hasMore = false, + isLoading = false, + isLoadingNextPage = false, + currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id, + isCurrentTrackPlaying = playerState == PlayerState.PLAYING, + onTrackClick = viewModel::playTopTracksFromTrack, + onTrackOptionsAction = viewModel::handleTrackOptionsAction, + trackOptionsState = trackOptionsContext::stateFor, + onArtistClick = { trackArtist -> + navigationCommands.navigateTo( + Routes.Artist( + trackArtist.id + ) + ) + }, onAlbumClick = { album -> navigationCommands.navigateTo( Routes.Album( @@ -205,49 +186,74 @@ fun ArtistScreen( ) ) }, - onLoadMore = { viewModel.loadMoreAlbums() }) + onLoadNextPage = {}, + simplified = true, + scrollable = false, + onBulkDownload = viewModel::downloadTracks, + onBulkAddToQueue = viewModel::addTracksToQueue, + onBulkPlayNext = viewModel::playTracksNext, + onBulkAddToPlaylist = viewModel::showAddToPlaylistPicker, + onBulkAddToJam = viewModel::addTracksToJam, + isInJam = jamActive, + isJamGuest = isJamGuest + ) } - } - if (currentState.relatedArtists.isNotEmpty() || currentState.relatedArtistsNextPagination != null) { - item { - RelatedArtistsSection( - artists = currentState.relatedArtists, - onArtistClick = { artist -> - navigationCommands.navigateTo( - Routes.Artist( - artist.id + if (currentState.albums.isNotEmpty() || currentState.albumsNextPagination != null) { + item { + AlbumsSection( + albums = currentState.albums, + onAlbumClick = { album -> + navigationCommands.navigateTo( + Routes.Album( + album.id + ) ) - ) - }, - onLoadMore = { viewModel.loadMoreRelatedArtists() }) + }, + onLoadMore = { viewModel.loadMoreAlbums() }) + } } - } - if (currentState.featuredPlaylists.isNotEmpty() || currentState.featuredPlaylistsNextPagination != null) { - item { - FeaturedPlaylistsSection( - playlists = currentState.featuredPlaylists, - onPlaylistClick = { playlist -> - navigationCommands.navigateTo( - Routes.Playlist( - playlist.id + if (currentState.relatedArtists.isNotEmpty() || currentState.relatedArtistsNextPagination != null) { + item { + RelatedArtistsSection( + artists = currentState.relatedArtists, + onArtistClick = { artist -> + navigationCommands.navigateTo( + Routes.Artist( + artist.id + ) ) - ) - }, - onLoadMore = { viewModel.loadMoreFeaturedPlaylists() }) + }, + onLoadMore = { viewModel.loadMoreRelatedArtists() }) + } + } + + if (currentState.featuredPlaylists.isNotEmpty() || currentState.featuredPlaylistsNextPagination != null) { + item { + FeaturedPlaylistsSection( + playlists = currentState.featuredPlaylists, + onPlaylistClick = { playlist -> + navigationCommands.navigateTo( + Routes.Playlist( + playlist.id + ) + ) + }, + onLoadMore = { viewModel.loadMoreFeaturedPlaylists() }) + } } } } } } - } AddToPlaylistPicker( visible = showAddToPlaylistPicker, currentUserId = currentUserId, onDismiss = viewModel::dismissAddToPlaylistPicker, - onPlaylistSelected = viewModel::addTracksToPlaylist) + onPlaylistSelected = viewModel::addTracksToPlaylist + ) } } @@ -256,41 +262,50 @@ private fun ArtistLoadingContent() { Box( modifier = Modifier .fillMaxSize(), - contentAlignment = Alignment.Center) { + contentAlignment = Alignment.Center + ) { SkeletonTree(true) { Column( modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(24.dp)) { + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { Box( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp)) { + .padding(horizontal = 16.dp) + ) { Box( modifier = Modifier .size(200.dp) .clip(CircleShape) - .background(AppTheme.colors.surfaceContainerHighest)) + .background(AppTheme.colors.surfaceContainerHighest) + ) } Column( modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp)) { + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { Box( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp)) { + .padding(horizontal = 16.dp) + ) { Box( modifier = Modifier .fillMaxWidth() - .padding(vertical = 8.dp)) + .padding(vertical = 8.dp) + ) } LazyRow( horizontalArrangement = Arrangement.spacedBy(12.dp), - contentPadding = PaddingValues(horizontal = 16.dp)) { + contentPadding = PaddingValues(horizontal = 16.dp) + ) { items(4) { PlayableCard( title = "Item Title", subtitle = "Subtitle", - imageURL = "https://placehold.co/600x400") + imageURL = "https://placehold.co/600x400" + ) } } } @@ -302,18 +317,22 @@ private fun ArtistLoadingContent() { @Composable private fun ArtistErrorContent( message: String, - onRetry: () -> Unit) { + onRetry: () -> Unit +) { Box( modifier = Modifier .fillMaxSize(), - contentAlignment = Alignment.Center) { + contentAlignment = Alignment.Center + ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp)) { + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { Text( text = message, color = AppTheme.colors.error, - style = AppTheme.typography.bodyLarge) + style = AppTheme.typography.bodyLarge + ) MillenniumButton(onClick = onRetry) { Text("Retry") } @@ -327,34 +346,40 @@ private fun ArtistHeaderCard( isSaved: Boolean, onFollowClick: () -> Unit, isBlacklisted: Boolean, - onBlacklistClick: () -> Unit) { + onBlacklistClick: () -> Unit +) { Box(modifier = Modifier.fillMaxWidth()) { val isCompact = currentWindowWidthBreakpoint() isBelow WindowBreakpoints.Medium MillenniumCard( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 8.dp)) { + .padding(horizontal = 12.dp, vertical = 8.dp) + ) { if (isCompact) { Column( modifier = Modifier .fillMaxWidth() .padding(16.dp), verticalArrangement = Arrangement.spacedBy(14.dp), - horizontalAlignment = Alignment.CenterHorizontally) { + horizontalAlignment = Alignment.CenterHorizontally + ) { ArtistAvatar( artist = artist, - size = 180.dp) + size = 180.dp + ) ArtistMeta( artist = artist, - isCompact = true) + isCompact = true + ) ArtistHeaderActions( isSaved = isSaved, onFollowClick = onFollowClick, isBlacklisted = isBlacklisted, - onBlacklistClick = onBlacklistClick) + onBlacklistClick = onBlacklistClick + ) } } else { Row( @@ -362,23 +387,28 @@ private fun ArtistHeaderCard( .fillMaxWidth() .padding(20.dp), horizontalArrangement = Arrangement.spacedBy(20.dp), - verticalAlignment = Alignment.Top) { + verticalAlignment = Alignment.Top + ) { ArtistAvatar( artist = artist, - size = 220.dp) + size = 220.dp + ) Column( modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(14.dp)) { + verticalArrangement = Arrangement.spacedBy(14.dp) + ) { ArtistMeta( artist = artist, - isCompact = false) + isCompact = false + ) ArtistHeaderActions( isSaved = isSaved, onFollowClick = onFollowClick, isBlacklisted = isBlacklisted, - onBlacklistClick = onBlacklistClick) + onBlacklistClick = onBlacklistClick + ) } } } @@ -389,26 +419,30 @@ private fun ArtistHeaderCard( @Composable private fun ArtistAvatar( artist: MetadataArtist.Detailed, - size: androidx.compose.ui.unit.Dp) { + size: androidx.compose.ui.unit.Dp +) { Box( modifier = Modifier .size(size) .clip(CircleShape) .background(AppTheme.colors.surfaceContainerHighest), - contentAlignment = Alignment.Center) { + contentAlignment = Alignment.Center + ) { val imageUrl = artist.thumbnails.firstOrNull()?.url.orEmpty() if (imageUrl.isNotBlank()) { AsyncImage( model = imageUrl, contentDescription = artist.name, modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop) + contentScale = ContentScale.Crop + ) } else { MillenniumIcon( imageVector = Iconsax.User, contentDescription = artist.name, tint = AppTheme.colors.onSurfaceVariant, - modifier = Modifier.size(size * 0.4f)) + modifier = Modifier.size(size * 0.4f) + ) } } } @@ -416,18 +450,21 @@ private fun ArtistAvatar( @Composable private fun ArtistMeta( artist: MetadataArtist.Detailed, - isCompact: Boolean) { + isCompact: Boolean +) { Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = if (isCompact) Alignment.CenterHorizontally else Alignment.Start, - verticalArrangement = Arrangement.spacedBy(10.dp)) { + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { Text( text = artist.name, style = if (isCompact) AppTheme.typography.headlineSmall else AppTheme.typography.headlineLarge, fontWeight = FontWeight.SemiBold, textAlign = if (isCompact) TextAlign.Center else TextAlign.Start, maxLines = 2, - overflow = TextOverflow.Ellipsis) + overflow = TextOverflow.Ellipsis + ) Text( text = buildString { @@ -439,7 +476,8 @@ private fun ArtistMeta( }, style = AppTheme.typography.bodyMedium, color = AppTheme.colors.onSurfaceVariant, - textAlign = if (isCompact) TextAlign.Center else TextAlign.Start) + textAlign = if (isCompact) TextAlign.Center else TextAlign.Start + ) artist.biography?.takeIf { it.isNotBlank() }?.let { Text( @@ -448,7 +486,8 @@ private fun ArtistMeta( color = AppTheme.colors.onSurfaceVariant, maxLines = if (isCompact) 4 else 6, overflow = TextOverflow.Ellipsis, - textAlign = if (isCompact) TextAlign.Center else TextAlign.Start) + textAlign = if (isCompact) TextAlign.Center else TextAlign.Start + ) } } } @@ -458,19 +497,23 @@ private fun ArtistHeaderActions( isSaved: Boolean, onFollowClick: () -> Unit, isBlacklisted: Boolean, - onBlacklistClick: () -> Unit) { + onBlacklistClick: () -> Unit +) { Row( horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalAlignment = Alignment.CenterVertically) { + verticalAlignment = Alignment.CenterVertically + ) { if (isSaved) { MillenniumButton( onClick = onFollowClick, - style = MillenniumButtonStyle.Secondary) { + style = MillenniumButtonStyle.Secondary + ) { Text( "Following", textAlign = TextAlign.Center, maxLines = 1, - overflow = TextOverflow.Ellipsis) + overflow = TextOverflow.Ellipsis + ) } } else { MillenniumButton(onClick = onFollowClick) { @@ -484,19 +527,23 @@ private fun ArtistHeaderActions( if (isBlacklisted) { MillenniumIconButton( onClick = onBlacklistClick, - style = MillenniumIconButtonStyle.Secondary) { + style = MillenniumIconButtonStyle.Secondary + ) { MillenniumIcon( imageVector = Iconsax.IconsaxUserRemove, contentDescription = "Remove from blacklist", - tint = AppTheme.colors.error) + tint = AppTheme.colors.error + ) } } else { MillenniumIconButton( onClick = onBlacklistClick, - style = MillenniumIconButtonStyle.Secondary) { + style = MillenniumIconButtonStyle.Secondary + ) { MillenniumIcon( imageVector = Iconsax.IconsaxUserRemove, - contentDescription = "Add to blacklist") + contentDescription = "Add to blacklist" + ) } } } @@ -506,32 +553,40 @@ private fun ArtistHeaderActions( @Composable private fun TopTracksHeader( onPlay: () -> Unit, - onAddToQueue: () -> Unit) { + onAddToQueue: () -> Unit +) { Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween) { + horizontalArrangement = Arrangement.SpaceBetween + ) { Text( text = "Top Tracks", style = AppTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold) + fontWeight = FontWeight.SemiBold + ) Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { MillenniumIconButton( onClick = onPlay, - style = MillenniumIconButtonStyle.Primary) { + style = MillenniumIconButtonStyle.Primary + ) { MillenniumIcon( imageVector = Iconsax.IconsaxPlay, - contentDescription = "Play top tracks") + contentDescription = "Play top tracks", + tint = LocalTextStyle.current.color + ) } MillenniumIconButton( onClick = onAddToQueue, - style = MillenniumIconButtonStyle.Secondary) { + style = MillenniumIconButtonStyle.Secondary + ) { MillenniumIcon( imageVector = Iconsax.IconsaxAddSquare, - contentDescription = "Add top tracks to queue") + contentDescription = "Add top tracks to queue" + ) } } } @@ -541,7 +596,8 @@ private fun TopTracksHeader( private fun AlbumsSection( albums: List, onAlbumClick: (MetadataAlbum.Detailed) -> Unit, - onLoadMore: () -> Unit) { + onLoadMore: () -> Unit +) { val rowState = rememberLazyListState() LaunchedEffect(rowState) { @@ -566,7 +622,8 @@ private fun AlbumsSection( modifier = Modifier.dragScrollable(rowState), contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.Top) { + verticalAlignment = Alignment.Top + ) { items(albums, key = { it.id }) { album -> AlbumCard(album = album) } @@ -577,7 +634,8 @@ private fun AlbumsSection( private fun RelatedArtistsSection( artists: List, onArtistClick: (MetadataArtist.Basic) -> Unit, - onLoadMore: () -> Unit) { + onLoadMore: () -> Unit +) { val rowState = rememberLazyListState() LaunchedEffect(rowState) { @@ -602,7 +660,8 @@ private fun RelatedArtistsSection( modifier = Modifier.dragScrollable(rowState), contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.Top) { + verticalAlignment = Alignment.Top + ) { items(artists, key = { it.id }) { artist -> ArtistCard(artist = artist) } @@ -613,7 +672,8 @@ private fun RelatedArtistsSection( private fun FeaturedPlaylistsSection( playlists: List, onPlaylistClick: (MetadataPlaylist) -> Unit, - onLoadMore: () -> Unit) { + onLoadMore: () -> Unit +) { val rowState = rememberLazyListState() LaunchedEffect(rowState) { @@ -638,7 +698,8 @@ private fun FeaturedPlaylistsSection( modifier = Modifier.dragScrollable(rowState), contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.Top) { + verticalAlignment = Alignment.Top + ) { items(playlists, key = { it.id }) { playlist -> PlaylistCard(playlist = playlist) } @@ -651,7 +712,8 @@ private fun SectionHeader(title: String) { text = title, style = AppTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold, - modifier = Modifier.padding(horizontal = 16.dp)) + modifier = Modifier.padding(horizontal = 16.dp) + ) } private fun formatFollowers(count: Int?): String { diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt index 30cb314d..7ce08e4c 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt @@ -50,6 +50,7 @@ import androidx.compose.foundation.verticalScroll import dev.krtirtho.spotube.core.ui.theming.AppTheme import com.composeunstyled.Text +import com.composeunstyled.LocalTextStyle import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -86,7 +87,8 @@ import kotlin.time.Duration.Companion.milliseconds @Composable fun RemoteControlScreen( - onDisconnect: () -> Unit) { + onDisconnect: () -> Unit +) { val viewModel = koinViewModel() val playerState by viewModel.playerState.collectAsStateWithLifecycle() val connectionState by viewModel.connectionState.collectAsStateWithLifecycle() @@ -98,18 +100,21 @@ fun RemoteControlScreen( // stretch awkwardly on large screens. Box( modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.TopCenter) { + contentAlignment = Alignment.TopCenter + ) { Column( modifier = Modifier .fillMaxHeight() - .background(MillenniumTheme.colors.background)) { + .background(MillenniumTheme.colors.background) + ) { ApplicationMainBar( title = { Text("Remote Control") }, backButton = true, actions = { MillenniumIconButton( onClick = viewModel::toggleQueueVisibility, - style = MillenniumIconButtonStyle.Quiet) { + style = MillenniumIconButtonStyle.Quiet + ) { MillenniumIcon( imageVector = Iconsax.IconsaxMusicFilter, contentDescription = "Queue", @@ -117,66 +122,72 @@ fun RemoteControlScreen( AppTheme.colors.primary } else { AppTheme.colors.onSurfaceVariant - }) + } + ) } MillenniumIconButton( onClick = { viewModel.disconnect() onDisconnect() }, - style = MillenniumIconButtonStyle.Quiet) { + style = MillenniumIconButtonStyle.Quiet + ) { MillenniumIcon( imageVector = Iconsax.IconsaxCloseSquare, - contentDescription = "Disconnect") + contentDescription = "Disconnect" + ) } } ) Box(modifier = Modifier.weight(1f)) { - when (connectionState) { - is ConnectionState.Connected -> { - RemoteControlContent( - playerState = playerState, - onTogglePlayPause = viewModel::togglePlayPause, - onSkipNext = viewModel::skipNext, - onSkipPrevious = viewModel::skipPrevious, - onSeek = viewModel::seek, - onSetVolume = viewModel::setVolume, - onToggleShuffle = viewModel::toggleShuffle, - onCycleLoopMode = viewModel::cycleLoopMode, - modifier = Modifier.padding(bottom = shellBottomInset) - ) - } - is ConnectionState.Connecting -> { - Box( - modifier = Modifier - .fillMaxSize() - .padding(bottom = shellBottomInset), - contentAlignment = Alignment.Center - ) { - Text("Connecting...") + when (connectionState) { + is ConnectionState.Connected -> { + RemoteControlContent( + playerState = playerState, + onTogglePlayPause = viewModel::togglePlayPause, + onSkipNext = viewModel::skipNext, + onSkipPrevious = viewModel::skipPrevious, + onSeek = viewModel::seek, + onSetVolume = viewModel::setVolume, + onToggleShuffle = viewModel::toggleShuffle, + onCycleLoopMode = viewModel::cycleLoopMode, + modifier = Modifier.padding(bottom = shellBottomInset) + ) + } + + is ConnectionState.Connecting -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(bottom = shellBottomInset), + contentAlignment = Alignment.Center + ) { + Text("Connecting...") + } + } + + is ConnectionState.Disconnected -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(bottom = shellBottomInset), + contentAlignment = Alignment.Center + ) { + Text("Disconnected") + } + } + + is ConnectionState.Error -> { + Box( + modifier = Modifier + .fillMaxSize() + .padding(bottom = shellBottomInset), + contentAlignment = Alignment.Center + ) { + Text("Connection error: ${(connectionState as ConnectionState.Error).message}") + } } } - is ConnectionState.Disconnected -> { - Box( - modifier = Modifier - .fillMaxSize() - .padding(bottom = shellBottomInset), - contentAlignment = Alignment.Center - ) { - Text("Disconnected") - } - } - is ConnectionState.Error -> { - Box( - modifier = Modifier - .fillMaxSize() - .padding(bottom = shellBottomInset), - contentAlignment = Alignment.Center - ) { - Text("Connection error: ${(connectionState as ConnectionState.Error).message}") - } - } - } } } } @@ -198,15 +209,18 @@ fun RemoteControlScreen( Box( modifier = Modifier .fillMaxSize() - .padding(bottom = shellBottomInset)) { + .padding(bottom = shellBottomInset) + ) { QueueSheet( isVisible = isQueueVisible, onDismiss = { viewModel.toggleQueueVisibility() }, - modifier = Modifier.fillMaxSize()) { + modifier = Modifier.fillMaxSize() + ) { RemoteQueueSection( queueState = queueState, onPlayQueueItem = viewModel::playQueueItem, - onRemoveQueueItem = viewModel::removeQueueItem) + onRemoveQueueItem = viewModel::removeQueueItem + ) } } } @@ -221,15 +235,17 @@ private fun RemoteControlContent( onSetVolume: (Float) -> Unit, onToggleShuffle: () -> Unit, onCycleLoopMode: () -> Unit, - modifier: Modifier = Modifier) { - Box (modifier = Modifier.fillMaxSize()) { + modifier: Modifier = Modifier +) { + Box(modifier = Modifier.fillMaxSize()) { Column( modifier = modifier .verticalScroll(rememberScrollState()) .padding(horizontal = 24.dp) .widthIn(max = 480.dp) .align(Alignment.TopCenter), - horizontalAlignment = Alignment.CenterHorizontally) { + horizontalAlignment = Alignment.CenterHorizontally + ) { Spacer(modifier = Modifier.height(32.dp)) // Album art @@ -238,22 +254,26 @@ private fun RemoteControlContent( .fillMaxWidth() .aspectRatio(1f) .clip(RoundedCornerShape(16.dp)) - .background(AppTheme.colors.surfaceVariant)) { + .background(AppTheme.colors.surfaceVariant) + ) { if (playerState.currentTrackCoverUrl != null) { AsyncImage( model = playerState.currentTrackCoverUrl, contentDescription = "Album cover", modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop) + contentScale = ContentScale.Crop + ) } else { Box( modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center) { + contentAlignment = Alignment.Center + ) { MillenniumIcon( imageVector = Iconsax.IconsaxMusicFilter, contentDescription = null, tint = AppTheme.colors.onSurfaceVariant, - modifier = Modifier.size(48.dp)) + modifier = Modifier.size(48.dp) + ) } } } @@ -263,14 +283,16 @@ private fun RemoteControlContent( // Track info Column( modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally) { + horizontalAlignment = Alignment.CenterHorizontally + ) { Text( text = playerState.currentTrackTitle ?: "Unknown Track", style = AppTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center, maxLines = 2, - overflow = TextOverflow.Ellipsis) + overflow = TextOverflow.Ellipsis + ) Spacer(modifier = Modifier.height(8.dp)) @@ -280,7 +302,8 @@ private fun RemoteControlContent( color = AppTheme.colors.onSurfaceVariant, textAlign = TextAlign.Center, maxLines = 1, - overflow = TextOverflow.Ellipsis) + overflow = TextOverflow.Ellipsis + ) if (playerState.currentTrackAlbum != null) { Spacer(modifier = Modifier.height(4.dp)) @@ -290,7 +313,8 @@ private fun RemoteControlContent( color = AppTheme.colors.onSurfaceVariant.copy(alpha = 0.7f), textAlign = TextAlign.Center, maxLines = 1, - overflow = TextOverflow.Ellipsis) + overflow = TextOverflow.Ellipsis + ) } } @@ -298,25 +322,30 @@ private fun RemoteControlContent( // Seek bar Column( - modifier = Modifier.fillMaxWidth()) { + modifier = Modifier.fillMaxWidth() + ) { MillenniumSlider( value = playerState.positionMs.toFloat(), onValueChange = { onSeek(it.toLong()) }, label = "Seek", valueRange = 0f..playerState.durationMs.toFloat().coerceAtLeast(1f), - modifier = Modifier.fillMaxWidth()) + modifier = Modifier.fillMaxWidth() + ) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween) { + horizontalArrangement = Arrangement.SpaceBetween + ) { Text( text = formatDuration(playerState.positionMs), style = AppTheme.typography.bodySmall, - color = AppTheme.colors.onSurfaceVariant) + color = AppTheme.colors.onSurfaceVariant + ) Text( text = formatDuration(playerState.durationMs), style = AppTheme.typography.bodySmall, - color = AppTheme.colors.onSurfaceVariant) + color = AppTheme.colors.onSurfaceVariant + ) } } @@ -326,11 +355,13 @@ private fun RemoteControlContent( Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically) { + verticalAlignment = Alignment.CenterVertically + ) { // Shuffle MillenniumIconButton( onClick = onToggleShuffle, - style = MillenniumIconButtonStyle.Quiet) { + style = MillenniumIconButtonStyle.Quiet + ) { MillenniumIcon( imageVector = Iconsax.IconsaxShuffle, contentDescription = "Shuffle", @@ -338,23 +369,27 @@ private fun RemoteControlContent( AppTheme.colors.primary } else { AppTheme.colors.onSurfaceVariant - }) + } + ) } // Skip previous MillenniumIconButton( onClick = onSkipPrevious, - style = MillenniumIconButtonStyle.Quiet) { + style = MillenniumIconButtonStyle.Quiet + ) { MillenniumIcon( imageVector = Iconsax.IconsaxPrevious, contentDescription = "Previous", - modifier = Modifier.size(32.dp)) + modifier = Modifier.size(32.dp) + ) } // Play/Pause MillenniumIconButton( onClick = onTogglePlayPause, - style = MillenniumIconButtonStyle.Primary) { + style = MillenniumIconButtonStyle.Primary + ) { MillenniumIcon( imageVector = if (playerState.isPlaying) { Iconsax.IconsaxPause @@ -362,23 +397,28 @@ private fun RemoteControlContent( Iconsax.IconsaxPlay }, contentDescription = if (playerState.isPlaying) "Pause" else "Play", - modifier = Modifier.size(40.dp)) + modifier = Modifier.size(40.dp), + tint = LocalTextStyle.current.color + ) } // Skip next MillenniumIconButton( onClick = onSkipNext, - style = MillenniumIconButtonStyle.Quiet) { + style = MillenniumIconButtonStyle.Quiet + ) { MillenniumIcon( imageVector = Iconsax.IconsaxNext, contentDescription = "Next", - modifier = Modifier.size(32.dp)) + modifier = Modifier.size(32.dp) + ) } // Loop mode MillenniumIconButton( onClick = onCycleLoopMode, - style = MillenniumIconButtonStyle.Quiet) { + style = MillenniumIconButtonStyle.Quiet + ) { MillenniumIcon( imageVector = Iconsax.IconsaxRepeateMusic, contentDescription = "Loop mode", @@ -386,7 +426,8 @@ private fun RemoteControlContent( AppTheme.colors.primary } else { AppTheme.colors.onSurfaceVariant - }) + } + ) } } @@ -396,19 +437,22 @@ private fun RemoteControlContent( Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp)) { + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { MillenniumIcon( imageVector = Iconsax.IconsaxVolumeHigh, contentDescription = "Volume", tint = AppTheme.colors.onSurfaceVariant, - modifier = Modifier.size(24.dp)) + modifier = Modifier.size(24.dp) + ) MillenniumSlider( value = playerState.volume, onValueChange = onSetVolume, label = "Volume", valueRange = 0f..1f, - modifier = Modifier.weight(1f)) + modifier = Modifier.weight(1f) + ) } Spacer(modifier = Modifier.height(32.dp)) @@ -420,26 +464,31 @@ private fun RemoteControlContent( private fun RemoteQueueSection( queueState: RemoteQueueState, onPlayQueueItem: (Int) -> Unit, - onRemoveQueueItem: (String) -> Unit) { + onRemoveQueueItem: (String) -> Unit +) { Column( modifier = Modifier .fillMaxSize() .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(10.dp)) { + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { Text( text = "Queue", - style = AppTheme.typography.titleLarge) + style = AppTheme.typography.titleLarge + ) if (queueState.entries.isEmpty()) { Text( text = "No queue entries", style = AppTheme.typography.bodyMedium, - color = AppTheme.colors.onSurfaceVariant) + color = AppTheme.colors.onSurfaceVariant + ) } else { LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(bottom = 8.dp), - verticalArrangement = Arrangement.spacedBy(4.dp)) { + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { itemsIndexed( items = queueState.entries, key = { index, entry -> "${entry.mediaUrl}@$index" }) { index, entry -> @@ -461,7 +510,8 @@ private fun RemoteQueueItemRow( index: Int, isCurrent: Boolean, onPlayClick: () -> Unit, - onRemoveClick: () -> Unit) { + onRemoveClick: () -> Unit +) { var showMenu by remember { mutableStateOf(false) } MillenniumListTile( @@ -474,18 +524,21 @@ private fun RemoteQueueItemRow( .size(48.dp) .clip(AppTheme.shapes.small) .background(AppTheme.colors.surfaceVariant), - contentAlignment = Alignment.Center) { + contentAlignment = Alignment.Center + ) { if (entry.coverUrl != null) { AsyncImage( model = entry.coverUrl, contentDescription = null, modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop) + contentScale = ContentScale.Crop + ) } else { Text( text = "${index + 1}", style = AppTheme.typography.labelMedium, - color = AppTheme.colors.onSurfaceVariant) + color = AppTheme.colors.onSurfaceVariant + ) } } }, @@ -499,7 +552,8 @@ private fun RemoteQueueItemRow( AppTheme.colors.onSecondaryContainer } else { AppTheme.colors.onSurface - }) + } + ) }, subtitle = { Text( @@ -507,13 +561,15 @@ private fun RemoteQueueItemRow( style = AppTheme.typography.bodySmall, color = AppTheme.colors.onSurfaceVariant, maxLines = 1, - overflow = TextOverflow.Ellipsis) + overflow = TextOverflow.Ellipsis + ) }, trailing = { Text( text = formatDuration(entry.durationMs), style = AppTheme.typography.bodySmall, - color = AppTheme.colors.onSurfaceVariant) + color = AppTheme.colors.onSurfaceVariant + ) MillenniumDropdownMenu( expanded = showMenu, @@ -526,11 +582,13 @@ private fun RemoteQueueItemRow( anchor = { MillenniumIconButton( onClick = { showMenu = true }, - style = MillenniumIconButtonStyle.Quiet) { + style = MillenniumIconButtonStyle.Quiet + ) { MillenniumIcon( Iconsax.Iconsax3DotsMore, contentDescription = "More options", - modifier = Modifier.size(18.dp)) + modifier = Modifier.size(18.dp) + ) } }) } @@ -542,4 +600,4 @@ private fun formatDuration(ms: Long): String { val minutes = duration.inWholeMinutes val seconds = duration.inWholeSeconds % 60 return "$minutes:${seconds.toString().padStart(2, '0')}" -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppExpandedPlayer.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppExpandedPlayer.kt index 228141aa..75de7d32 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppExpandedPlayer.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppExpandedPlayer.kt @@ -65,6 +65,7 @@ import com.composeunstyled.rememberModalBottomSheetState import androidx.compose.ui.graphics.SolidColor import com.composeunstyled.Text +import com.composeunstyled.LocalTextStyle import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -158,7 +159,8 @@ fun AppExpandedPlayer( savedTracksViewModel: SavedTracksViewModel = koinViewModel( key = SAVED_TRACKS_COLLECTION_ID, parameters = { parametersOf() } - )) { + ) +) { val playerUiState = rememberPlayerUiState(audioPlayer, audioPlayerQueue) val jamRoomService: JamRoomService = koinInject() val isJamGuest by jamRoomService.role @@ -185,7 +187,8 @@ fun AppExpandedPlayer( var isSeeking by remember { mutableStateOf(false) } var seekProgress by remember { mutableFloatStateOf(playerUiState.progress) } val moreOptionsSheetState = rememberModalBottomSheetState( - initialDetent = SheetDetent.FullyExpanded) + initialDetent = SheetDetent.FullyExpanded + ) PlayerOptionDialogs(viewModel = playerOptionsViewModel) @@ -262,7 +265,8 @@ fun AppExpandedPlayer( contentPadding = PaddingValues(16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp), - modifier = Modifier.fillMaxWidth()) { + modifier = Modifier.fillMaxWidth() + ) { item { OptionTile( icon = Iconsax.SwapHorizontal2, @@ -297,7 +301,8 @@ fun AppExpandedPlayer( shape = RoundedCornerShape(14.dp), borderColor = MillenniumTheme.colors.border, borderWidth = 1.dp, - shadowElevation = 2.dp) + shadowElevation = 2.dp + ) .clickable { playerOptionsViewModel.dismissMoreOptionsSheet() when (status) { @@ -337,14 +342,16 @@ fun AppExpandedPlayer( .fillMaxSize() .padding(12.dp), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center) { + verticalArrangement = Arrangement.Center + ) { if (status is DownloadStatus.Downloading) { DownloadProgressIcon( track = currentTrack, icon = Iconsax.IconsaxDirectboxReceive, contentDescription = "Download", modifier = Modifier.size(28.dp), - tint = AppTheme.colors.onSurface) + tint = AppTheme.colors.onSurface + ) } else { val icon = when (status) { is DownloadStatus.Completed -> Iconsax.IconsaxCheckCircle @@ -361,7 +368,8 @@ fun AppExpandedPlayer( is DownloadStatus.Completed -> AppTheme.colors.primary is DownloadStatus.Failed -> AppTheme.colors.error else -> AppTheme.colors.onSurface - }) + } + ) } Spacer(modifier = Modifier.height(8.dp)) Text( @@ -370,7 +378,8 @@ fun AppExpandedPlayer( textAlign = TextAlign.Center, maxLines = 2, overflow = TextOverflow.Ellipsis, - color = statusColor) + color = statusColor + ) } } } @@ -416,7 +425,8 @@ fun AppExpandedPlayer( .windowInsetsPadding(WindowInsets.safeDrawing) .verticalScroll(rememberScrollState()) .padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally) { + horizontalAlignment = Alignment.CenterHorizontally + ) { Spacer(modifier = Modifier.height(4.dp)) Row( modifier = Modifier @@ -425,18 +435,24 @@ fun AppExpandedPlayer( ) { MillenniumIconButton( onClick = onCollapse, - style = MillenniumIconButtonStyle.Quiet) { - MillenniumIcon(Iconsax.IconsaxArrowDown4, contentDescription = "Collapse player") + style = MillenniumIconButtonStyle.Quiet + ) { + MillenniumIcon( + Iconsax.IconsaxArrowDown4, + contentDescription = "Collapse player" + ) } Text( text = "Now Playing", style = AppTheme.typography.titleMedium, modifier = Modifier.weight(1f), textAlign = TextAlign.Center, - maxLines = 1) + maxLines = 1 + ) MillenniumIconButton( onClick = playerOptionsViewModel::showMoreOptionsSheet, - style = MillenniumIconButtonStyle.Quiet) { + style = MillenniumIconButtonStyle.Quiet + ) { MillenniumIcon(Iconsax.Iconsax3DotsMore, contentDescription = "Player options") } } @@ -445,7 +461,8 @@ fun AppExpandedPlayer( modifier = Modifier .fillMaxWidth() .padding(top = 20.dp, bottom = 24.dp), - contentAlignment = Alignment.Center) { + contentAlignment = Alignment.Center + ) { Box( modifier = Modifier .fillMaxWidth(0.84f) @@ -458,7 +475,8 @@ fun AppExpandedPlayer( model = coverModel, contentDescription = playerUiState.title, modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop) + contentScale = ContentScale.Crop + ) } } @@ -471,16 +489,19 @@ fun AppExpandedPlayer( ) { PlayerHeartButton( audioPlayerQueue = audioPlayerQueue, - savedTracksViewModel = savedTracksViewModel) + savedTracksViewModel = savedTracksViewModel + ) Column( modifier = Modifier.weight(1f), - horizontalAlignment = Alignment.CenterHorizontally) { + horizontalAlignment = Alignment.CenterHorizontally + ) { Text( text = playerUiState.title, style = AppTheme.typography.headlineSmall, maxLines = 1, overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center) + textAlign = TextAlign.Center + ) PlayerArtistsText( artists = playerUiState.artistList, onArtistClick = { artistId -> @@ -488,7 +509,8 @@ fun AppExpandedPlayer( }, style = AppTheme.typography.bodyLarge, color = AppTheme.colors.onSurfaceVariant, - textAlign = TextAlign.Center) + textAlign = TextAlign.Center + ) } MillenniumIconButton( onClick = onQueue, @@ -518,26 +540,31 @@ fun AppExpandedPlayer( .fillMaxWidth() .padding(top = 2.dp), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically) { + verticalAlignment = Alignment.CenterVertically + ) { Text( text = formatPlayerTime(playerUiState.position), style = AppTheme.typography.labelSmall, - color = AppTheme.colors.onSurfaceVariant) + color = AppTheme.colors.onSurfaceVariant + ) Text( text = formatPlayerTime(playerUiState.displayDuration), style = AppTheme.typography.labelSmall, - color = AppTheme.colors.onSurfaceVariant) + color = AppTheme.colors.onSurfaceVariant + ) } Row( modifier = Modifier .fillMaxWidth() .padding(top = 16.dp, bottom = 12.dp), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically) { + verticalAlignment = Alignment.CenterVertically + ) { MillenniumIconButton( onClick = ::onShuffleToggle, enabled = !isJamGuest, - style = MillenniumIconButtonStyle.Quiet) { + style = MillenniumIconButtonStyle.Quiet + ) { MillenniumIcon( Iconsax.IconsaxShuffle, contentDescription = if (playerUiState.isShuffling) "Disable shuffle" else "Enable shuffle", @@ -551,27 +578,33 @@ fun AppExpandedPlayer( MillenniumIconButton( onClick = ::onSkipPrevious, enabled = !isJamGuest, - style = MillenniumIconButtonStyle.Quiet) { + style = MillenniumIconButtonStyle.Quiet + ) { MillenniumIcon(Iconsax.IconsaxPrevious, contentDescription = "Previous") } MillenniumIconButton( onClick = ::onPlayPause, - style = MillenniumIconButtonStyle.Primary) { + style = MillenniumIconButtonStyle.Primary + ) { MillenniumIcon( if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay, contentDescription = if (playerUiState.isPlaying) "Pause" else "Play", - modifier = Modifier.size(30.dp)) + modifier = Modifier.size(30.dp), + tint = LocalTextStyle.current.color + ) } MillenniumIconButton( onClick = ::onSkipNext, enabled = !isJamGuest, - style = MillenniumIconButtonStyle.Quiet) { + style = MillenniumIconButtonStyle.Quiet + ) { MillenniumIcon(Iconsax.IconsaxNext, contentDescription = "Next") } MillenniumIconButton( onClick = ::onLoopToggle, enabled = !isJamGuest, - style = MillenniumIconButtonStyle.Quiet) { + style = MillenniumIconButtonStyle.Quiet + ) { MillenniumIcon( imageVector = when (playerUiState.loopState) { LoopState.NONE -> Iconsax.IconsaxRepeateMusic @@ -594,12 +627,14 @@ fun AppExpandedPlayer( .fillMaxWidth() .padding(bottom = 16.dp), audioPlayer = audioPlayer, - onExpand = onExpandLyrics) + onExpand = onExpandLyrics + ) } MillenniumSnackbarHost( hostState = snackbarHostState, - modifier = Modifier.align(Alignment.BottomCenter)) + modifier = Modifier.align(Alignment.BottomCenter) + ) } } @@ -607,13 +642,15 @@ fun AppExpandedPlayer( @Composable private fun rememberSharedAlbumArtModifier( sharedTransitionScope: SharedTransitionScope?, - animatedVisibilityScope: AnimatedVisibilityScope?): Modifier { + animatedVisibilityScope: AnimatedVisibilityScope? +): Modifier { if (sharedTransitionScope == null || animatedVisibilityScope == null) return Modifier with(sharedTransitionScope) { return Modifier.sharedElement( sharedContentState = rememberSharedContentState(key = "player_album_art"), - animatedVisibilityScope = animatedVisibilityScope) + animatedVisibilityScope = animatedVisibilityScope + ) } } @@ -660,24 +697,30 @@ private fun LyricsPreviewCard( .millenniumSurface( background = SolidColor(AppTheme.colors.surfaceContainerHighest), shape = RoundedCornerShape(16.dp), - shadowElevation = 2.dp) - .clickable(onClick = onExpand)) { + shadowElevation = 2.dp + ) + .clickable(onClick = onExpand) + ) { Column(modifier = Modifier.padding(12.dp)) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically) { + verticalAlignment = Alignment.CenterVertically + ) { Text( text = "Lyrics", style = AppTheme.typography.labelMedium, - color = AppTheme.colors.onSurfaceVariant) + color = AppTheme.colors.onSurfaceVariant + ) MillenniumIconButton( onClick = onExpand, - style = MillenniumIconButtonStyle.Quiet) { + style = MillenniumIconButtonStyle.Quiet + ) { MillenniumIcon( Iconsax.IconsaxArrowSquareUp, contentDescription = "Expand lyrics", - tint = AppTheme.colors.onSurfaceVariant) + tint = AppTheme.colors.onSurfaceVariant + ) } } @@ -687,7 +730,8 @@ private fun LyricsPreviewCard( .fillMaxWidth() .height(120.dp), contentPadding = PaddingValues(vertical = 4.dp), - verticalArrangement = Arrangement.spacedBy(2.dp)) { + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { val displayLines = syncedLyrics.take(6) itemsIndexed(displayLines) { index, line -> val isCurrent = @@ -707,7 +751,8 @@ private fun LyricsPreviewCard( ), maxLines = 1, overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center) + textAlign = TextAlign.Center + ) } } } @@ -719,21 +764,25 @@ private fun OptionTile( icon: ImageVector, label: String, onClick: () -> Unit, - modifier: Modifier = Modifier) { + modifier: Modifier = Modifier +) { MillenniumCard( modifier = modifier .aspectRatio(1f) .clickable(onClick = onClick), - contentPadding = PaddingValues(12.dp)) { + contentPadding = PaddingValues(12.dp) + ) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center) { + verticalArrangement = Arrangement.Center + ) { MillenniumIcon( imageVector = icon, contentDescription = label, modifier = Modifier.size(28.dp), - tint = AppTheme.colors.onSurface) + tint = AppTheme.colors.onSurface + ) Spacer(modifier = Modifier.height(8.dp)) Text( text = label, @@ -741,7 +790,8 @@ private fun OptionTile( textAlign = TextAlign.Center, maxLines = 2, overflow = TextOverflow.Ellipsis, - color = AppTheme.colors.onSurfaceVariant) + color = AppTheme.colors.onSurfaceVariant + ) } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppLargePlayer.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppLargePlayer.kt index e2a87728..0b034497 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppLargePlayer.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppLargePlayer.kt @@ -18,12 +18,6 @@ package dev.krtirtho.spotube.modules.shell -import org.spotube.millennium.composables.MillenniumIcon -import org.spotube.millennium.composables.MillenniumDropdownMenu -import org.spotube.millennium.composables.MillenniumIconButton -import org.spotube.millennium.composables.MillenniumIconButtonStyle -import org.spotube.millennium.composables.MillenniumSeparator -import org.spotube.millennium.composables.MillenniumSlider import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -38,12 +32,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape - -import dev.krtirtho.spotube.core.ui.theming.AppTheme - -import com.composeunstyled.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -55,13 +44,14 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.composeunstyled.AnchorSide +import com.composeunstyled.Text +import com.composeunstyled.LocalTextStyle import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.blur.HazeColorEffect import dev.chrisbanes.haze.blur.blurEffect @@ -74,15 +64,14 @@ import dev.krtirtho.spotube.core.jam.JamRole import dev.krtirtho.spotube.core.jam.JamRoomService import dev.krtirtho.spotube.core.navigation.NavigationCommands import dev.krtirtho.spotube.core.navigation.Routes +import dev.krtirtho.spotube.core.ui.theming.AppTheme import dev.krtirtho.spotube.modules.downloads.DownloadProgressIcon import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel import dev.krtirtho.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.Iconsax3DotsMore -import dev.krtirtho.spotube.resources.iconsax.IconsaxCd import dev.krtirtho.spotube.resources.iconsax.IconsaxDirectboxReceive -import dev.krtirtho.spotube.resources.iconsax.IconsaxInformation import dev.krtirtho.spotube.resources.iconsax.IconsaxMusic import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicFilter import dev.krtirtho.spotube.resources.iconsax.IconsaxNext @@ -96,7 +85,6 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxShuffle import dev.krtirtho.spotube.resources.iconsax.IconsaxVolumeCross import dev.krtirtho.spotube.resources.iconsax.IconsaxVolumeHigh import dev.krtirtho.spotube.resources.iconsax.IconsaxVolumeLow -import dev.krtirtho.spotube.resources.iconsax.InconsaxClock import dev.krtirtho.spotube.resources.iconsax.SwapHorizontal2 import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -104,6 +92,12 @@ import org.jetbrains.compose.resources.stringResource import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel import org.koin.core.parameter.parametersOf +import org.spotube.millennium.composables.MillenniumDropdownMenu +import org.spotube.millennium.composables.MillenniumIcon +import org.spotube.millennium.composables.MillenniumIconButton +import org.spotube.millennium.composables.MillenniumIconButtonStyle +import org.spotube.millennium.composables.MillenniumSeparator +import org.spotube.millennium.composables.MillenniumSlider import spotube.composeapp.generated.resources.Res import spotube.composeapp.generated.resources.player_go_to_album import spotube.composeapp.generated.resources.player_sleep_timer @@ -346,7 +340,8 @@ fun AppLargePlayer( ) { MillenniumIcon( if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay, - contentDescription = if (playerUiState.isPlaying) "Pause" else "Play or pause" + contentDescription = if (playerUiState.isPlaying) "Pause" else "Play or pause", + tint = LocalTextStyle.current.color ) } MillenniumIconButton(