feat(ui): integrate LocalTextStyle for dynamic tinting in player controls and UI components

This commit is contained in:
Kingkor Roy Tirtho 2026-10-15 13:12:39 +06:00
parent 58dfa2de92
commit fb96984d6e
9 changed files with 512 additions and 334 deletions

17
.mcp.json Normal file
View File

@ -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"
}
}
}

View File

@ -13,19 +13,9 @@
"hotMcpServer" "hotMcpServer"
] ]
}, },
"android-studio-mcp": { "studio": {
"type": "local", "url": "http://127.0.0.1:64342/stream",
"environment": { "type": "remote"
"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"
]
} }
} }
} }

View File

@ -35,6 +35,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import dev.krtirtho.spotube.core.ui.theming.AppTheme import dev.krtirtho.spotube.core.ui.theming.AppTheme
import com.composeunstyled.Text import com.composeunstyled.Text
import com.composeunstyled.LocalTextStyle
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@ -158,7 +159,8 @@ fun LikedTracksCard(
Iconsax.IconsaxPauseCircle Iconsax.IconsaxPauseCircle
} else Iconsax.Play, } else Iconsax.Play,
contentDescription = stringResource(Res.string.player_play_content_description), contentDescription = stringResource(Res.string.player_play_content_description),
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp),
tint = LocalTextStyle.current.color
) )
} }
} }

View File

@ -21,6 +21,7 @@ package dev.krtirtho.spotube.core.ui.component.cards
import org.spotube.millennium.composables.MillenniumIcon import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle import org.spotube.millennium.composables.MillenniumIconButtonStyle
import com.composeunstyled.LocalTextStyle
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@ -143,7 +144,7 @@ fun PlayableCard(
imageVector = Iconsax.IconsaxAddSquare, imageVector = Iconsax.IconsaxAddSquare,
contentDescription = stringResource( contentDescription = stringResource(
Res.string.player_add_to_queue_content_description Res.string.player_add_to_queue_content_description
) ),
) )
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@ -165,7 +166,8 @@ fun PlayableCard(
} else { } else {
Res.string.player_play_content_description Res.string.player_play_content_description
} }
) ),
tint = LocalTextStyle.current.color
) )
} }
} }

View File

@ -20,6 +20,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.composeunstyled.LocalTextStyle
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import dev.krtirtho.spotube.core.ui.component.LocalAnimatedVisibilityScope import dev.krtirtho.spotube.core.ui.component.LocalAnimatedVisibilityScope
import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope
@ -130,7 +131,8 @@ fun PlayableListTile(
) { ) {
MillenniumIcon( MillenniumIcon(
if (isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay, 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
) )
} }
} }

View File

@ -48,6 +48,7 @@ import androidx.compose.foundation.shape.CircleShape
import dev.krtirtho.spotube.core.ui.theming.AppTheme import dev.krtirtho.spotube.core.ui.theming.AppTheme
import com.composeunstyled.Text import com.composeunstyled.Text
import com.composeunstyled.LocalTextStyle
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -117,87 +118,67 @@ fun ArtistScreen(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(MillenniumTheme.colors.background)) { .background(MillenniumTheme.colors.background)
) {
ApplicationMainBar() ApplicationMainBar()
Box(modifier = Modifier.fillMaxWidth().weight(1f)) { Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
when (val currentState = state) { when (val currentState = state) {
is ArtistScreenState.Loading -> { is ArtistScreenState.Loading -> {
ArtistLoadingContent() ArtistLoadingContent()
} }
is ArtistScreenState.Error -> { is ArtistScreenState.Error -> {
ArtistErrorContent( ArtistErrorContent(
message = currentState.message, message = currentState.message,
onRetry = { viewModel.refresh() } onRetry = { viewModel.refresh() }
) )
} }
is ArtistScreenState.Loaded -> { is ArtistScreenState.Loaded -> {
val listState = rememberLazyListState() val listState = rememberLazyListState()
LazyColumn( LazyColumn(
state = listState, state = listState,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(bottom = LocalAppShellBottomInset.current + 24.dp), .padding(bottom = LocalAppShellBottomInset.current + 24.dp),
verticalArrangement = Arrangement.spacedBy(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) {
item { item {
AlbumsSection( ArtistHeaderCard(
albums = currentState.albums, 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 -> onAlbumClick = { album ->
navigationCommands.navigateTo( navigationCommands.navigateTo(
Routes.Album( 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) { if (currentState.albums.isNotEmpty() || currentState.albumsNextPagination != null) {
item { item {
RelatedArtistsSection( AlbumsSection(
artists = currentState.relatedArtists, albums = currentState.albums,
onArtistClick = { artist -> onAlbumClick = { album ->
navigationCommands.navigateTo( navigationCommands.navigateTo(
Routes.Artist( Routes.Album(
artist.id album.id
)
) )
) },
}, onLoadMore = { viewModel.loadMoreAlbums() })
onLoadMore = { viewModel.loadMoreRelatedArtists() }) }
} }
}
if (currentState.featuredPlaylists.isNotEmpty() || currentState.featuredPlaylistsNextPagination != null) { if (currentState.relatedArtists.isNotEmpty() || currentState.relatedArtistsNextPagination != null) {
item { item {
FeaturedPlaylistsSection( RelatedArtistsSection(
playlists = currentState.featuredPlaylists, artists = currentState.relatedArtists,
onPlaylistClick = { playlist -> onArtistClick = { artist ->
navigationCommands.navigateTo( navigationCommands.navigateTo(
Routes.Playlist( Routes.Artist(
playlist.id artist.id
)
) )
) },
}, onLoadMore = { viewModel.loadMoreRelatedArtists() })
onLoadMore = { viewModel.loadMoreFeaturedPlaylists() }) }
}
if (currentState.featuredPlaylists.isNotEmpty() || currentState.featuredPlaylistsNextPagination != null) {
item {
FeaturedPlaylistsSection(
playlists = currentState.featuredPlaylists,
onPlaylistClick = { playlist ->
navigationCommands.navigateTo(
Routes.Playlist(
playlist.id
)
)
},
onLoadMore = { viewModel.loadMoreFeaturedPlaylists() })
}
} }
} }
} }
} }
} }
}
AddToPlaylistPicker( AddToPlaylistPicker(
visible = showAddToPlaylistPicker, visible = showAddToPlaylistPicker,
currentUserId = currentUserId, currentUserId = currentUserId,
onDismiss = viewModel::dismissAddToPlaylistPicker, onDismiss = viewModel::dismissAddToPlaylistPicker,
onPlaylistSelected = viewModel::addTracksToPlaylist) onPlaylistSelected = viewModel::addTracksToPlaylist
)
} }
} }
@ -256,41 +262,50 @@ private fun ArtistLoadingContent() {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize(), .fillMaxSize(),
contentAlignment = Alignment.Center) { contentAlignment = Alignment.Center
) {
SkeletonTree(true) { SkeletonTree(true) {
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(24.dp)) { verticalArrangement = Arrangement.spacedBy(24.dp)
) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp)) { .padding(horizontal = 16.dp)
) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(200.dp) .size(200.dp)
.clip(CircleShape) .clip(CircleShape)
.background(AppTheme.colors.surfaceContainerHighest)) .background(AppTheme.colors.surfaceContainerHighest)
)
} }
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)) { verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp)) { .padding(horizontal = 16.dp)
) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp)) .padding(vertical = 8.dp)
)
} }
LazyRow( LazyRow(
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(horizontal = 16.dp)) { contentPadding = PaddingValues(horizontal = 16.dp)
) {
items(4) { items(4) {
PlayableCard( PlayableCard(
title = "Item Title", title = "Item Title",
subtitle = "Subtitle", subtitle = "Subtitle",
imageURL = "https://placehold.co/600x400") imageURL = "https://placehold.co/600x400"
)
} }
} }
} }
@ -302,18 +317,22 @@ private fun ArtistLoadingContent() {
@Composable @Composable
private fun ArtistErrorContent( private fun ArtistErrorContent(
message: String, message: String,
onRetry: () -> Unit) { onRetry: () -> Unit
) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize(), .fillMaxSize(),
contentAlignment = Alignment.Center) { contentAlignment = Alignment.Center
) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)) { verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text( Text(
text = message, text = message,
color = AppTheme.colors.error, color = AppTheme.colors.error,
style = AppTheme.typography.bodyLarge) style = AppTheme.typography.bodyLarge
)
MillenniumButton(onClick = onRetry) { MillenniumButton(onClick = onRetry) {
Text("Retry") Text("Retry")
} }
@ -327,34 +346,40 @@ private fun ArtistHeaderCard(
isSaved: Boolean, isSaved: Boolean,
onFollowClick: () -> Unit, onFollowClick: () -> Unit,
isBlacklisted: Boolean, isBlacklisted: Boolean,
onBlacklistClick: () -> Unit) { onBlacklistClick: () -> Unit
) {
Box(modifier = Modifier.fillMaxWidth()) { Box(modifier = Modifier.fillMaxWidth()) {
val isCompact = currentWindowWidthBreakpoint() isBelow WindowBreakpoints.Medium val isCompact = currentWindowWidthBreakpoint() isBelow WindowBreakpoints.Medium
MillenniumCard( MillenniumCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp)) { .padding(horizontal = 12.dp, vertical = 8.dp)
) {
if (isCompact) { if (isCompact) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(16.dp),
verticalArrangement = Arrangement.spacedBy(14.dp), verticalArrangement = Arrangement.spacedBy(14.dp),
horizontalAlignment = Alignment.CenterHorizontally) { horizontalAlignment = Alignment.CenterHorizontally
) {
ArtistAvatar( ArtistAvatar(
artist = artist, artist = artist,
size = 180.dp) size = 180.dp
)
ArtistMeta( ArtistMeta(
artist = artist, artist = artist,
isCompact = true) isCompact = true
)
ArtistHeaderActions( ArtistHeaderActions(
isSaved = isSaved, isSaved = isSaved,
onFollowClick = onFollowClick, onFollowClick = onFollowClick,
isBlacklisted = isBlacklisted, isBlacklisted = isBlacklisted,
onBlacklistClick = onBlacklistClick) onBlacklistClick = onBlacklistClick
)
} }
} else { } else {
Row( Row(
@ -362,23 +387,28 @@ private fun ArtistHeaderCard(
.fillMaxWidth() .fillMaxWidth()
.padding(20.dp), .padding(20.dp),
horizontalArrangement = Arrangement.spacedBy(20.dp), horizontalArrangement = Arrangement.spacedBy(20.dp),
verticalAlignment = Alignment.Top) { verticalAlignment = Alignment.Top
) {
ArtistAvatar( ArtistAvatar(
artist = artist, artist = artist,
size = 220.dp) size = 220.dp
)
Column( Column(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(14.dp)) { verticalArrangement = Arrangement.spacedBy(14.dp)
) {
ArtistMeta( ArtistMeta(
artist = artist, artist = artist,
isCompact = false) isCompact = false
)
ArtistHeaderActions( ArtistHeaderActions(
isSaved = isSaved, isSaved = isSaved,
onFollowClick = onFollowClick, onFollowClick = onFollowClick,
isBlacklisted = isBlacklisted, isBlacklisted = isBlacklisted,
onBlacklistClick = onBlacklistClick) onBlacklistClick = onBlacklistClick
)
} }
} }
} }
@ -389,26 +419,30 @@ private fun ArtistHeaderCard(
@Composable @Composable
private fun ArtistAvatar( private fun ArtistAvatar(
artist: MetadataArtist.Detailed, artist: MetadataArtist.Detailed,
size: androidx.compose.ui.unit.Dp) { size: androidx.compose.ui.unit.Dp
) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(size) .size(size)
.clip(CircleShape) .clip(CircleShape)
.background(AppTheme.colors.surfaceContainerHighest), .background(AppTheme.colors.surfaceContainerHighest),
contentAlignment = Alignment.Center) { contentAlignment = Alignment.Center
) {
val imageUrl = artist.thumbnails.firstOrNull()?.url.orEmpty() val imageUrl = artist.thumbnails.firstOrNull()?.url.orEmpty()
if (imageUrl.isNotBlank()) { if (imageUrl.isNotBlank()) {
AsyncImage( AsyncImage(
model = imageUrl, model = imageUrl,
contentDescription = artist.name, contentDescription = artist.name,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop) contentScale = ContentScale.Crop
)
} else { } else {
MillenniumIcon( MillenniumIcon(
imageVector = Iconsax.User, imageVector = Iconsax.User,
contentDescription = artist.name, contentDescription = artist.name,
tint = AppTheme.colors.onSurfaceVariant, tint = AppTheme.colors.onSurfaceVariant,
modifier = Modifier.size(size * 0.4f)) modifier = Modifier.size(size * 0.4f)
)
} }
} }
} }
@ -416,18 +450,21 @@ private fun ArtistAvatar(
@Composable @Composable
private fun ArtistMeta( private fun ArtistMeta(
artist: MetadataArtist.Detailed, artist: MetadataArtist.Detailed,
isCompact: Boolean) { isCompact: Boolean
) {
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalAlignment = if (isCompact) Alignment.CenterHorizontally else Alignment.Start, horizontalAlignment = if (isCompact) Alignment.CenterHorizontally else Alignment.Start,
verticalArrangement = Arrangement.spacedBy(10.dp)) { verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text( Text(
text = artist.name, text = artist.name,
style = if (isCompact) AppTheme.typography.headlineSmall else AppTheme.typography.headlineLarge, style = if (isCompact) AppTheme.typography.headlineSmall else AppTheme.typography.headlineLarge,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
textAlign = if (isCompact) TextAlign.Center else TextAlign.Start, textAlign = if (isCompact) TextAlign.Center else TextAlign.Start,
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis) overflow = TextOverflow.Ellipsis
)
Text( Text(
text = buildString { text = buildString {
@ -439,7 +476,8 @@ private fun ArtistMeta(
}, },
style = AppTheme.typography.bodyMedium, style = AppTheme.typography.bodyMedium,
color = AppTheme.colors.onSurfaceVariant, 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 { artist.biography?.takeIf { it.isNotBlank() }?.let {
Text( Text(
@ -448,7 +486,8 @@ private fun ArtistMeta(
color = AppTheme.colors.onSurfaceVariant, color = AppTheme.colors.onSurfaceVariant,
maxLines = if (isCompact) 4 else 6, maxLines = if (isCompact) 4 else 6,
overflow = TextOverflow.Ellipsis, 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, isSaved: Boolean,
onFollowClick: () -> Unit, onFollowClick: () -> Unit,
isBlacklisted: Boolean, isBlacklisted: Boolean,
onBlacklistClick: () -> Unit) { onBlacklistClick: () -> Unit
) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically) { verticalAlignment = Alignment.CenterVertically
) {
if (isSaved) { if (isSaved) {
MillenniumButton( MillenniumButton(
onClick = onFollowClick, onClick = onFollowClick,
style = MillenniumButtonStyle.Secondary) { style = MillenniumButtonStyle.Secondary
) {
Text( Text(
"Following", "Following",
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis) overflow = TextOverflow.Ellipsis
)
} }
} else { } else {
MillenniumButton(onClick = onFollowClick) { MillenniumButton(onClick = onFollowClick) {
@ -484,19 +527,23 @@ private fun ArtistHeaderActions(
if (isBlacklisted) { if (isBlacklisted) {
MillenniumIconButton( MillenniumIconButton(
onClick = onBlacklistClick, onClick = onBlacklistClick,
style = MillenniumIconButtonStyle.Secondary) { style = MillenniumIconButtonStyle.Secondary
) {
MillenniumIcon( MillenniumIcon(
imageVector = Iconsax.IconsaxUserRemove, imageVector = Iconsax.IconsaxUserRemove,
contentDescription = "Remove from blacklist", contentDescription = "Remove from blacklist",
tint = AppTheme.colors.error) tint = AppTheme.colors.error
)
} }
} else { } else {
MillenniumIconButton( MillenniumIconButton(
onClick = onBlacklistClick, onClick = onBlacklistClick,
style = MillenniumIconButtonStyle.Secondary) { style = MillenniumIconButtonStyle.Secondary
) {
MillenniumIcon( MillenniumIcon(
imageVector = Iconsax.IconsaxUserRemove, imageVector = Iconsax.IconsaxUserRemove,
contentDescription = "Add to blacklist") contentDescription = "Add to blacklist"
)
} }
} }
} }
@ -506,32 +553,40 @@ private fun ArtistHeaderActions(
@Composable @Composable
private fun TopTracksHeader( private fun TopTracksHeader(
onPlay: () -> Unit, onPlay: () -> Unit,
onAddToQueue: () -> Unit) { onAddToQueue: () -> Unit
) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween) { horizontalArrangement = Arrangement.SpaceBetween
) {
Text( Text(
text = "Top Tracks", text = "Top Tracks",
style = AppTheme.typography.titleLarge, style = AppTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold) fontWeight = FontWeight.SemiBold
)
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
MillenniumIconButton( MillenniumIconButton(
onClick = onPlay, onClick = onPlay,
style = MillenniumIconButtonStyle.Primary) { style = MillenniumIconButtonStyle.Primary
) {
MillenniumIcon( MillenniumIcon(
imageVector = Iconsax.IconsaxPlay, imageVector = Iconsax.IconsaxPlay,
contentDescription = "Play top tracks") contentDescription = "Play top tracks",
tint = LocalTextStyle.current.color
)
} }
MillenniumIconButton( MillenniumIconButton(
onClick = onAddToQueue, onClick = onAddToQueue,
style = MillenniumIconButtonStyle.Secondary) { style = MillenniumIconButtonStyle.Secondary
) {
MillenniumIcon( MillenniumIcon(
imageVector = Iconsax.IconsaxAddSquare, 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( private fun AlbumsSection(
albums: List<MetadataAlbum.Detailed>, albums: List<MetadataAlbum.Detailed>,
onAlbumClick: (MetadataAlbum.Detailed) -> Unit, onAlbumClick: (MetadataAlbum.Detailed) -> Unit,
onLoadMore: () -> Unit) { onLoadMore: () -> Unit
) {
val rowState = rememberLazyListState() val rowState = rememberLazyListState()
LaunchedEffect(rowState) { LaunchedEffect(rowState) {
@ -566,7 +622,8 @@ private fun AlbumsSection(
modifier = Modifier.dragScrollable(rowState), modifier = Modifier.dragScrollable(rowState),
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.Top) { verticalAlignment = Alignment.Top
) {
items(albums, key = { it.id }) { album -> items(albums, key = { it.id }) { album ->
AlbumCard(album = album) AlbumCard(album = album)
} }
@ -577,7 +634,8 @@ private fun AlbumsSection(
private fun RelatedArtistsSection( private fun RelatedArtistsSection(
artists: List<MetadataArtist.Basic>, artists: List<MetadataArtist.Basic>,
onArtistClick: (MetadataArtist.Basic) -> Unit, onArtistClick: (MetadataArtist.Basic) -> Unit,
onLoadMore: () -> Unit) { onLoadMore: () -> Unit
) {
val rowState = rememberLazyListState() val rowState = rememberLazyListState()
LaunchedEffect(rowState) { LaunchedEffect(rowState) {
@ -602,7 +660,8 @@ private fun RelatedArtistsSection(
modifier = Modifier.dragScrollable(rowState), modifier = Modifier.dragScrollable(rowState),
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.Top) { verticalAlignment = Alignment.Top
) {
items(artists, key = { it.id }) { artist -> items(artists, key = { it.id }) { artist ->
ArtistCard(artist = artist) ArtistCard(artist = artist)
} }
@ -613,7 +672,8 @@ private fun RelatedArtistsSection(
private fun FeaturedPlaylistsSection( private fun FeaturedPlaylistsSection(
playlists: List<MetadataPlaylist>, playlists: List<MetadataPlaylist>,
onPlaylistClick: (MetadataPlaylist) -> Unit, onPlaylistClick: (MetadataPlaylist) -> Unit,
onLoadMore: () -> Unit) { onLoadMore: () -> Unit
) {
val rowState = rememberLazyListState() val rowState = rememberLazyListState()
LaunchedEffect(rowState) { LaunchedEffect(rowState) {
@ -638,7 +698,8 @@ private fun FeaturedPlaylistsSection(
modifier = Modifier.dragScrollable(rowState), modifier = Modifier.dragScrollable(rowState),
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.Top) { verticalAlignment = Alignment.Top
) {
items(playlists, key = { it.id }) { playlist -> items(playlists, key = { it.id }) { playlist ->
PlaylistCard(playlist = playlist) PlaylistCard(playlist = playlist)
} }
@ -651,7 +712,8 @@ private fun SectionHeader(title: String) {
text = title, text = title,
style = AppTheme.typography.titleLarge, style = AppTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(horizontal = 16.dp)) modifier = Modifier.padding(horizontal = 16.dp)
)
} }
private fun formatFollowers(count: Int?): String { private fun formatFollowers(count: Int?): String {

View File

@ -50,6 +50,7 @@ import androidx.compose.foundation.verticalScroll
import dev.krtirtho.spotube.core.ui.theming.AppTheme import dev.krtirtho.spotube.core.ui.theming.AppTheme
import com.composeunstyled.Text import com.composeunstyled.Text
import com.composeunstyled.LocalTextStyle
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -86,7 +87,8 @@ import kotlin.time.Duration.Companion.milliseconds
@Composable @Composable
fun RemoteControlScreen( fun RemoteControlScreen(
onDisconnect: () -> Unit) { onDisconnect: () -> Unit
) {
val viewModel = koinViewModel<RemoteControlViewModel>() val viewModel = koinViewModel<RemoteControlViewModel>()
val playerState by viewModel.playerState.collectAsStateWithLifecycle() val playerState by viewModel.playerState.collectAsStateWithLifecycle()
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle() val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
@ -98,18 +100,21 @@ fun RemoteControlScreen(
// stretch awkwardly on large screens. // stretch awkwardly on large screens.
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.TopCenter) { contentAlignment = Alignment.TopCenter
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
.background(MillenniumTheme.colors.background)) { .background(MillenniumTheme.colors.background)
) {
ApplicationMainBar( ApplicationMainBar(
title = { Text("Remote Control") }, title = { Text("Remote Control") },
backButton = true, backButton = true,
actions = { actions = {
MillenniumIconButton( MillenniumIconButton(
onClick = viewModel::toggleQueueVisibility, onClick = viewModel::toggleQueueVisibility,
style = MillenniumIconButtonStyle.Quiet) { style = MillenniumIconButtonStyle.Quiet
) {
MillenniumIcon( MillenniumIcon(
imageVector = Iconsax.IconsaxMusicFilter, imageVector = Iconsax.IconsaxMusicFilter,
contentDescription = "Queue", contentDescription = "Queue",
@ -117,66 +122,72 @@ fun RemoteControlScreen(
AppTheme.colors.primary AppTheme.colors.primary
} else { } else {
AppTheme.colors.onSurfaceVariant AppTheme.colors.onSurfaceVariant
}) }
)
} }
MillenniumIconButton( MillenniumIconButton(
onClick = { onClick = {
viewModel.disconnect() viewModel.disconnect()
onDisconnect() onDisconnect()
}, },
style = MillenniumIconButtonStyle.Quiet) { style = MillenniumIconButtonStyle.Quiet
) {
MillenniumIcon( MillenniumIcon(
imageVector = Iconsax.IconsaxCloseSquare, imageVector = Iconsax.IconsaxCloseSquare,
contentDescription = "Disconnect") contentDescription = "Disconnect"
)
} }
} }
) )
Box(modifier = Modifier.weight(1f)) { Box(modifier = Modifier.weight(1f)) {
when (connectionState) { when (connectionState) {
is ConnectionState.Connected -> { is ConnectionState.Connected -> {
RemoteControlContent( RemoteControlContent(
playerState = playerState, playerState = playerState,
onTogglePlayPause = viewModel::togglePlayPause, onTogglePlayPause = viewModel::togglePlayPause,
onSkipNext = viewModel::skipNext, onSkipNext = viewModel::skipNext,
onSkipPrevious = viewModel::skipPrevious, onSkipPrevious = viewModel::skipPrevious,
onSeek = viewModel::seek, onSeek = viewModel::seek,
onSetVolume = viewModel::setVolume, onSetVolume = viewModel::setVolume,
onToggleShuffle = viewModel::toggleShuffle, onToggleShuffle = viewModel::toggleShuffle,
onCycleLoopMode = viewModel::cycleLoopMode, onCycleLoopMode = viewModel::cycleLoopMode,
modifier = Modifier.padding(bottom = shellBottomInset) modifier = Modifier.padding(bottom = shellBottomInset)
) )
} }
is ConnectionState.Connecting -> {
Box( is ConnectionState.Connecting -> {
modifier = Modifier Box(
.fillMaxSize() modifier = Modifier
.padding(bottom = shellBottomInset), .fillMaxSize()
contentAlignment = Alignment.Center .padding(bottom = shellBottomInset),
) { contentAlignment = Alignment.Center
Text("Connecting...") ) {
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( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(bottom = shellBottomInset)) { .padding(bottom = shellBottomInset)
) {
QueueSheet( QueueSheet(
isVisible = isQueueVisible, isVisible = isQueueVisible,
onDismiss = { viewModel.toggleQueueVisibility() }, onDismiss = { viewModel.toggleQueueVisibility() },
modifier = Modifier.fillMaxSize()) { modifier = Modifier.fillMaxSize()
) {
RemoteQueueSection( RemoteQueueSection(
queueState = queueState, queueState = queueState,
onPlayQueueItem = viewModel::playQueueItem, onPlayQueueItem = viewModel::playQueueItem,
onRemoveQueueItem = viewModel::removeQueueItem) onRemoveQueueItem = viewModel::removeQueueItem
)
} }
} }
} }
@ -221,15 +235,17 @@ private fun RemoteControlContent(
onSetVolume: (Float) -> Unit, onSetVolume: (Float) -> Unit,
onToggleShuffle: () -> Unit, onToggleShuffle: () -> Unit,
onCycleLoopMode: () -> Unit, onCycleLoopMode: () -> Unit,
modifier: Modifier = Modifier) { modifier: Modifier = Modifier
Box (modifier = Modifier.fillMaxSize()) { ) {
Box(modifier = Modifier.fillMaxSize()) {
Column( Column(
modifier = modifier modifier = modifier
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp) .padding(horizontal = 24.dp)
.widthIn(max = 480.dp) .widthIn(max = 480.dp)
.align(Alignment.TopCenter), .align(Alignment.TopCenter),
horizontalAlignment = Alignment.CenterHorizontally) { horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))
// Album art // Album art
@ -238,22 +254,26 @@ private fun RemoteControlContent(
.fillMaxWidth() .fillMaxWidth()
.aspectRatio(1f) .aspectRatio(1f)
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
.background(AppTheme.colors.surfaceVariant)) { .background(AppTheme.colors.surfaceVariant)
) {
if (playerState.currentTrackCoverUrl != null) { if (playerState.currentTrackCoverUrl != null) {
AsyncImage( AsyncImage(
model = playerState.currentTrackCoverUrl, model = playerState.currentTrackCoverUrl,
contentDescription = "Album cover", contentDescription = "Album cover",
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop) contentScale = ContentScale.Crop
)
} else { } else {
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center) { contentAlignment = Alignment.Center
) {
MillenniumIcon( MillenniumIcon(
imageVector = Iconsax.IconsaxMusicFilter, imageVector = Iconsax.IconsaxMusicFilter,
contentDescription = null, contentDescription = null,
tint = AppTheme.colors.onSurfaceVariant, tint = AppTheme.colors.onSurfaceVariant,
modifier = Modifier.size(48.dp)) modifier = Modifier.size(48.dp)
)
} }
} }
} }
@ -263,14 +283,16 @@ private fun RemoteControlContent(
// Track info // Track info
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally) { horizontalAlignment = Alignment.CenterHorizontally
) {
Text( Text(
text = playerState.currentTrackTitle ?: "Unknown Track", text = playerState.currentTrackTitle ?: "Unknown Track",
style = AppTheme.typography.headlineSmall, style = AppTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis) overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@ -280,7 +302,8 @@ private fun RemoteControlContent(
color = AppTheme.colors.onSurfaceVariant, color = AppTheme.colors.onSurfaceVariant,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis) overflow = TextOverflow.Ellipsis
)
if (playerState.currentTrackAlbum != null) { if (playerState.currentTrackAlbum != null) {
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
@ -290,7 +313,8 @@ private fun RemoteControlContent(
color = AppTheme.colors.onSurfaceVariant.copy(alpha = 0.7f), color = AppTheme.colors.onSurfaceVariant.copy(alpha = 0.7f),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis) overflow = TextOverflow.Ellipsis
)
} }
} }
@ -298,25 +322,30 @@ private fun RemoteControlContent(
// Seek bar // Seek bar
Column( Column(
modifier = Modifier.fillMaxWidth()) { modifier = Modifier.fillMaxWidth()
) {
MillenniumSlider( MillenniumSlider(
value = playerState.positionMs.toFloat(), value = playerState.positionMs.toFloat(),
onValueChange = { onSeek(it.toLong()) }, onValueChange = { onSeek(it.toLong()) },
label = "Seek", label = "Seek",
valueRange = 0f..playerState.durationMs.toFloat().coerceAtLeast(1f), valueRange = 0f..playerState.durationMs.toFloat().coerceAtLeast(1f),
modifier = Modifier.fillMaxWidth()) modifier = Modifier.fillMaxWidth()
)
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween) { horizontalArrangement = Arrangement.SpaceBetween
) {
Text( Text(
text = formatDuration(playerState.positionMs), text = formatDuration(playerState.positionMs),
style = AppTheme.typography.bodySmall, style = AppTheme.typography.bodySmall,
color = AppTheme.colors.onSurfaceVariant) color = AppTheme.colors.onSurfaceVariant
)
Text( Text(
text = formatDuration(playerState.durationMs), text = formatDuration(playerState.durationMs),
style = AppTheme.typography.bodySmall, style = AppTheme.typography.bodySmall,
color = AppTheme.colors.onSurfaceVariant) color = AppTheme.colors.onSurfaceVariant
)
} }
} }
@ -326,11 +355,13 @@ private fun RemoteControlContent(
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically) { verticalAlignment = Alignment.CenterVertically
) {
// Shuffle // Shuffle
MillenniumIconButton( MillenniumIconButton(
onClick = onToggleShuffle, onClick = onToggleShuffle,
style = MillenniumIconButtonStyle.Quiet) { style = MillenniumIconButtonStyle.Quiet
) {
MillenniumIcon( MillenniumIcon(
imageVector = Iconsax.IconsaxShuffle, imageVector = Iconsax.IconsaxShuffle,
contentDescription = "Shuffle", contentDescription = "Shuffle",
@ -338,23 +369,27 @@ private fun RemoteControlContent(
AppTheme.colors.primary AppTheme.colors.primary
} else { } else {
AppTheme.colors.onSurfaceVariant AppTheme.colors.onSurfaceVariant
}) }
)
} }
// Skip previous // Skip previous
MillenniumIconButton( MillenniumIconButton(
onClick = onSkipPrevious, onClick = onSkipPrevious,
style = MillenniumIconButtonStyle.Quiet) { style = MillenniumIconButtonStyle.Quiet
) {
MillenniumIcon( MillenniumIcon(
imageVector = Iconsax.IconsaxPrevious, imageVector = Iconsax.IconsaxPrevious,
contentDescription = "Previous", contentDescription = "Previous",
modifier = Modifier.size(32.dp)) modifier = Modifier.size(32.dp)
)
} }
// Play/Pause // Play/Pause
MillenniumIconButton( MillenniumIconButton(
onClick = onTogglePlayPause, onClick = onTogglePlayPause,
style = MillenniumIconButtonStyle.Primary) { style = MillenniumIconButtonStyle.Primary
) {
MillenniumIcon( MillenniumIcon(
imageVector = if (playerState.isPlaying) { imageVector = if (playerState.isPlaying) {
Iconsax.IconsaxPause Iconsax.IconsaxPause
@ -362,23 +397,28 @@ private fun RemoteControlContent(
Iconsax.IconsaxPlay Iconsax.IconsaxPlay
}, },
contentDescription = if (playerState.isPlaying) "Pause" else "Play", contentDescription = if (playerState.isPlaying) "Pause" else "Play",
modifier = Modifier.size(40.dp)) modifier = Modifier.size(40.dp),
tint = LocalTextStyle.current.color
)
} }
// Skip next // Skip next
MillenniumIconButton( MillenniumIconButton(
onClick = onSkipNext, onClick = onSkipNext,
style = MillenniumIconButtonStyle.Quiet) { style = MillenniumIconButtonStyle.Quiet
) {
MillenniumIcon( MillenniumIcon(
imageVector = Iconsax.IconsaxNext, imageVector = Iconsax.IconsaxNext,
contentDescription = "Next", contentDescription = "Next",
modifier = Modifier.size(32.dp)) modifier = Modifier.size(32.dp)
)
} }
// Loop mode // Loop mode
MillenniumIconButton( MillenniumIconButton(
onClick = onCycleLoopMode, onClick = onCycleLoopMode,
style = MillenniumIconButtonStyle.Quiet) { style = MillenniumIconButtonStyle.Quiet
) {
MillenniumIcon( MillenniumIcon(
imageVector = Iconsax.IconsaxRepeateMusic, imageVector = Iconsax.IconsaxRepeateMusic,
contentDescription = "Loop mode", contentDescription = "Loop mode",
@ -386,7 +426,8 @@ private fun RemoteControlContent(
AppTheme.colors.primary AppTheme.colors.primary
} else { } else {
AppTheme.colors.onSurfaceVariant AppTheme.colors.onSurfaceVariant
}) }
)
} }
} }
@ -396,19 +437,22 @@ private fun RemoteControlContent(
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)) { horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
MillenniumIcon( MillenniumIcon(
imageVector = Iconsax.IconsaxVolumeHigh, imageVector = Iconsax.IconsaxVolumeHigh,
contentDescription = "Volume", contentDescription = "Volume",
tint = AppTheme.colors.onSurfaceVariant, tint = AppTheme.colors.onSurfaceVariant,
modifier = Modifier.size(24.dp)) modifier = Modifier.size(24.dp)
)
MillenniumSlider( MillenniumSlider(
value = playerState.volume, value = playerState.volume,
onValueChange = onSetVolume, onValueChange = onSetVolume,
label = "Volume", label = "Volume",
valueRange = 0f..1f, valueRange = 0f..1f,
modifier = Modifier.weight(1f)) modifier = Modifier.weight(1f)
)
} }
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))
@ -420,26 +464,31 @@ private fun RemoteControlContent(
private fun RemoteQueueSection( private fun RemoteQueueSection(
queueState: RemoteQueueState, queueState: RemoteQueueState,
onPlayQueueItem: (Int) -> Unit, onPlayQueueItem: (Int) -> Unit,
onRemoveQueueItem: (String) -> Unit) { onRemoveQueueItem: (String) -> Unit
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp), .padding(16.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)) { verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text( Text(
text = "Queue", text = "Queue",
style = AppTheme.typography.titleLarge) style = AppTheme.typography.titleLarge
)
if (queueState.entries.isEmpty()) { if (queueState.entries.isEmpty()) {
Text( Text(
text = "No queue entries", text = "No queue entries",
style = AppTheme.typography.bodyMedium, style = AppTheme.typography.bodyMedium,
color = AppTheme.colors.onSurfaceVariant) color = AppTheme.colors.onSurfaceVariant
)
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 8.dp), contentPadding = PaddingValues(bottom = 8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)) { verticalArrangement = Arrangement.spacedBy(4.dp)
) {
itemsIndexed( itemsIndexed(
items = queueState.entries, items = queueState.entries,
key = { index, entry -> "${entry.mediaUrl}@$index" }) { index, entry -> key = { index, entry -> "${entry.mediaUrl}@$index" }) { index, entry ->
@ -461,7 +510,8 @@ private fun RemoteQueueItemRow(
index: Int, index: Int,
isCurrent: Boolean, isCurrent: Boolean,
onPlayClick: () -> Unit, onPlayClick: () -> Unit,
onRemoveClick: () -> Unit) { onRemoveClick: () -> Unit
) {
var showMenu by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) }
MillenniumListTile( MillenniumListTile(
@ -474,18 +524,21 @@ private fun RemoteQueueItemRow(
.size(48.dp) .size(48.dp)
.clip(AppTheme.shapes.small) .clip(AppTheme.shapes.small)
.background(AppTheme.colors.surfaceVariant), .background(AppTheme.colors.surfaceVariant),
contentAlignment = Alignment.Center) { contentAlignment = Alignment.Center
) {
if (entry.coverUrl != null) { if (entry.coverUrl != null) {
AsyncImage( AsyncImage(
model = entry.coverUrl, model = entry.coverUrl,
contentDescription = null, contentDescription = null,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop) contentScale = ContentScale.Crop
)
} else { } else {
Text( Text(
text = "${index + 1}", text = "${index + 1}",
style = AppTheme.typography.labelMedium, style = AppTheme.typography.labelMedium,
color = AppTheme.colors.onSurfaceVariant) color = AppTheme.colors.onSurfaceVariant
)
} }
} }
}, },
@ -499,7 +552,8 @@ private fun RemoteQueueItemRow(
AppTheme.colors.onSecondaryContainer AppTheme.colors.onSecondaryContainer
} else { } else {
AppTheme.colors.onSurface AppTheme.colors.onSurface
}) }
)
}, },
subtitle = { subtitle = {
Text( Text(
@ -507,13 +561,15 @@ private fun RemoteQueueItemRow(
style = AppTheme.typography.bodySmall, style = AppTheme.typography.bodySmall,
color = AppTheme.colors.onSurfaceVariant, color = AppTheme.colors.onSurfaceVariant,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis) overflow = TextOverflow.Ellipsis
)
}, },
trailing = { trailing = {
Text( Text(
text = formatDuration(entry.durationMs), text = formatDuration(entry.durationMs),
style = AppTheme.typography.bodySmall, style = AppTheme.typography.bodySmall,
color = AppTheme.colors.onSurfaceVariant) color = AppTheme.colors.onSurfaceVariant
)
MillenniumDropdownMenu( MillenniumDropdownMenu(
expanded = showMenu, expanded = showMenu,
@ -526,11 +582,13 @@ private fun RemoteQueueItemRow(
anchor = { anchor = {
MillenniumIconButton( MillenniumIconButton(
onClick = { showMenu = true }, onClick = { showMenu = true },
style = MillenniumIconButtonStyle.Quiet) { style = MillenniumIconButtonStyle.Quiet
) {
MillenniumIcon( MillenniumIcon(
Iconsax.Iconsax3DotsMore, Iconsax.Iconsax3DotsMore,
contentDescription = "More options", 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 minutes = duration.inWholeMinutes
val seconds = duration.inWholeSeconds % 60 val seconds = duration.inWholeSeconds % 60
return "$minutes:${seconds.toString().padStart(2, '0')}" return "$minutes:${seconds.toString().padStart(2, '0')}"
} }

View File

@ -65,6 +65,7 @@ import com.composeunstyled.rememberModalBottomSheetState
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import com.composeunstyled.Text import com.composeunstyled.Text
import com.composeunstyled.LocalTextStyle
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -158,7 +159,8 @@ fun AppExpandedPlayer(
savedTracksViewModel: SavedTracksViewModel = koinViewModel<SavedTracksViewModel>( savedTracksViewModel: SavedTracksViewModel = koinViewModel<SavedTracksViewModel>(
key = SAVED_TRACKS_COLLECTION_ID, key = SAVED_TRACKS_COLLECTION_ID,
parameters = { parametersOf() } parameters = { parametersOf() }
)) { )
) {
val playerUiState = rememberPlayerUiState(audioPlayer, audioPlayerQueue) val playerUiState = rememberPlayerUiState(audioPlayer, audioPlayerQueue)
val jamRoomService: JamRoomService = koinInject() val jamRoomService: JamRoomService = koinInject()
val isJamGuest by jamRoomService.role val isJamGuest by jamRoomService.role
@ -185,7 +187,8 @@ fun AppExpandedPlayer(
var isSeeking by remember { mutableStateOf(false) } var isSeeking by remember { mutableStateOf(false) }
var seekProgress by remember { mutableFloatStateOf(playerUiState.progress) } var seekProgress by remember { mutableFloatStateOf(playerUiState.progress) }
val moreOptionsSheetState = rememberModalBottomSheetState( val moreOptionsSheetState = rememberModalBottomSheetState(
initialDetent = SheetDetent.FullyExpanded) initialDetent = SheetDetent.FullyExpanded
)
PlayerOptionDialogs(viewModel = playerOptionsViewModel) PlayerOptionDialogs(viewModel = playerOptionsViewModel)
@ -262,7 +265,8 @@ fun AppExpandedPlayer(
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.fillMaxWidth()) { modifier = Modifier.fillMaxWidth()
) {
item { item {
OptionTile( OptionTile(
icon = Iconsax.SwapHorizontal2, icon = Iconsax.SwapHorizontal2,
@ -297,7 +301,8 @@ fun AppExpandedPlayer(
shape = RoundedCornerShape(14.dp), shape = RoundedCornerShape(14.dp),
borderColor = MillenniumTheme.colors.border, borderColor = MillenniumTheme.colors.border,
borderWidth = 1.dp, borderWidth = 1.dp,
shadowElevation = 2.dp) shadowElevation = 2.dp
)
.clickable { .clickable {
playerOptionsViewModel.dismissMoreOptionsSheet() playerOptionsViewModel.dismissMoreOptionsSheet()
when (status) { when (status) {
@ -337,14 +342,16 @@ fun AppExpandedPlayer(
.fillMaxSize() .fillMaxSize()
.padding(12.dp), .padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center) { verticalArrangement = Arrangement.Center
) {
if (status is DownloadStatus.Downloading) { if (status is DownloadStatus.Downloading) {
DownloadProgressIcon( DownloadProgressIcon(
track = currentTrack, track = currentTrack,
icon = Iconsax.IconsaxDirectboxReceive, icon = Iconsax.IconsaxDirectboxReceive,
contentDescription = "Download", contentDescription = "Download",
modifier = Modifier.size(28.dp), modifier = Modifier.size(28.dp),
tint = AppTheme.colors.onSurface) tint = AppTheme.colors.onSurface
)
} else { } else {
val icon = when (status) { val icon = when (status) {
is DownloadStatus.Completed -> Iconsax.IconsaxCheckCircle is DownloadStatus.Completed -> Iconsax.IconsaxCheckCircle
@ -361,7 +368,8 @@ fun AppExpandedPlayer(
is DownloadStatus.Completed -> AppTheme.colors.primary is DownloadStatus.Completed -> AppTheme.colors.primary
is DownloadStatus.Failed -> AppTheme.colors.error is DownloadStatus.Failed -> AppTheme.colors.error
else -> AppTheme.colors.onSurface else -> AppTheme.colors.onSurface
}) }
)
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
@ -370,7 +378,8 @@ fun AppExpandedPlayer(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = statusColor) color = statusColor
)
} }
} }
} }
@ -416,7 +425,8 @@ fun AppExpandedPlayer(
.windowInsetsPadding(WindowInsets.safeDrawing) .windowInsetsPadding(WindowInsets.safeDrawing)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally) { horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Row( Row(
modifier = Modifier modifier = Modifier
@ -425,18 +435,24 @@ fun AppExpandedPlayer(
) { ) {
MillenniumIconButton( MillenniumIconButton(
onClick = onCollapse, onClick = onCollapse,
style = MillenniumIconButtonStyle.Quiet) { style = MillenniumIconButtonStyle.Quiet
MillenniumIcon(Iconsax.IconsaxArrowDown4, contentDescription = "Collapse player") ) {
MillenniumIcon(
Iconsax.IconsaxArrowDown4,
contentDescription = "Collapse player"
)
} }
Text( Text(
text = "Now Playing", text = "Now Playing",
style = AppTheme.typography.titleMedium, style = AppTheme.typography.titleMedium,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 1) maxLines = 1
)
MillenniumIconButton( MillenniumIconButton(
onClick = playerOptionsViewModel::showMoreOptionsSheet, onClick = playerOptionsViewModel::showMoreOptionsSheet,
style = MillenniumIconButtonStyle.Quiet) { style = MillenniumIconButtonStyle.Quiet
) {
MillenniumIcon(Iconsax.Iconsax3DotsMore, contentDescription = "Player options") MillenniumIcon(Iconsax.Iconsax3DotsMore, contentDescription = "Player options")
} }
} }
@ -445,7 +461,8 @@ fun AppExpandedPlayer(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 20.dp, bottom = 24.dp), .padding(top = 20.dp, bottom = 24.dp),
contentAlignment = Alignment.Center) { contentAlignment = Alignment.Center
) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth(0.84f) .fillMaxWidth(0.84f)
@ -458,7 +475,8 @@ fun AppExpandedPlayer(
model = coverModel, model = coverModel,
contentDescription = playerUiState.title, contentDescription = playerUiState.title,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop) contentScale = ContentScale.Crop
)
} }
} }
@ -471,16 +489,19 @@ fun AppExpandedPlayer(
) { ) {
PlayerHeartButton( PlayerHeartButton(
audioPlayerQueue = audioPlayerQueue, audioPlayerQueue = audioPlayerQueue,
savedTracksViewModel = savedTracksViewModel) savedTracksViewModel = savedTracksViewModel
)
Column( Column(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally) { horizontalAlignment = Alignment.CenterHorizontally
) {
Text( Text(
text = playerUiState.title, text = playerUiState.title,
style = AppTheme.typography.headlineSmall, style = AppTheme.typography.headlineSmall,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center) textAlign = TextAlign.Center
)
PlayerArtistsText( PlayerArtistsText(
artists = playerUiState.artistList, artists = playerUiState.artistList,
onArtistClick = { artistId -> onArtistClick = { artistId ->
@ -488,7 +509,8 @@ fun AppExpandedPlayer(
}, },
style = AppTheme.typography.bodyLarge, style = AppTheme.typography.bodyLarge,
color = AppTheme.colors.onSurfaceVariant, color = AppTheme.colors.onSurfaceVariant,
textAlign = TextAlign.Center) textAlign = TextAlign.Center
)
} }
MillenniumIconButton( MillenniumIconButton(
onClick = onQueue, onClick = onQueue,
@ -518,26 +540,31 @@ fun AppExpandedPlayer(
.fillMaxWidth() .fillMaxWidth()
.padding(top = 2.dp), .padding(top = 2.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) { verticalAlignment = Alignment.CenterVertically
) {
Text( Text(
text = formatPlayerTime(playerUiState.position), text = formatPlayerTime(playerUiState.position),
style = AppTheme.typography.labelSmall, style = AppTheme.typography.labelSmall,
color = AppTheme.colors.onSurfaceVariant) color = AppTheme.colors.onSurfaceVariant
)
Text( Text(
text = formatPlayerTime(playerUiState.displayDuration), text = formatPlayerTime(playerUiState.displayDuration),
style = AppTheme.typography.labelSmall, style = AppTheme.typography.labelSmall,
color = AppTheme.colors.onSurfaceVariant) color = AppTheme.colors.onSurfaceVariant
)
} }
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 16.dp, bottom = 12.dp), .padding(top = 16.dp, bottom = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) { verticalAlignment = Alignment.CenterVertically
) {
MillenniumIconButton( MillenniumIconButton(
onClick = ::onShuffleToggle, onClick = ::onShuffleToggle,
enabled = !isJamGuest, enabled = !isJamGuest,
style = MillenniumIconButtonStyle.Quiet) { style = MillenniumIconButtonStyle.Quiet
) {
MillenniumIcon( MillenniumIcon(
Iconsax.IconsaxShuffle, Iconsax.IconsaxShuffle,
contentDescription = if (playerUiState.isShuffling) "Disable shuffle" else "Enable shuffle", contentDescription = if (playerUiState.isShuffling) "Disable shuffle" else "Enable shuffle",
@ -551,27 +578,33 @@ fun AppExpandedPlayer(
MillenniumIconButton( MillenniumIconButton(
onClick = ::onSkipPrevious, onClick = ::onSkipPrevious,
enabled = !isJamGuest, enabled = !isJamGuest,
style = MillenniumIconButtonStyle.Quiet) { style = MillenniumIconButtonStyle.Quiet
) {
MillenniumIcon(Iconsax.IconsaxPrevious, contentDescription = "Previous") MillenniumIcon(Iconsax.IconsaxPrevious, contentDescription = "Previous")
} }
MillenniumIconButton( MillenniumIconButton(
onClick = ::onPlayPause, onClick = ::onPlayPause,
style = MillenniumIconButtonStyle.Primary) { style = MillenniumIconButtonStyle.Primary
) {
MillenniumIcon( MillenniumIcon(
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay, if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play", contentDescription = if (playerUiState.isPlaying) "Pause" else "Play",
modifier = Modifier.size(30.dp)) modifier = Modifier.size(30.dp),
tint = LocalTextStyle.current.color
)
} }
MillenniumIconButton( MillenniumIconButton(
onClick = ::onSkipNext, onClick = ::onSkipNext,
enabled = !isJamGuest, enabled = !isJamGuest,
style = MillenniumIconButtonStyle.Quiet) { style = MillenniumIconButtonStyle.Quiet
) {
MillenniumIcon(Iconsax.IconsaxNext, contentDescription = "Next") MillenniumIcon(Iconsax.IconsaxNext, contentDescription = "Next")
} }
MillenniumIconButton( MillenniumIconButton(
onClick = ::onLoopToggle, onClick = ::onLoopToggle,
enabled = !isJamGuest, enabled = !isJamGuest,
style = MillenniumIconButtonStyle.Quiet) { style = MillenniumIconButtonStyle.Quiet
) {
MillenniumIcon( MillenniumIcon(
imageVector = when (playerUiState.loopState) { imageVector = when (playerUiState.loopState) {
LoopState.NONE -> Iconsax.IconsaxRepeateMusic LoopState.NONE -> Iconsax.IconsaxRepeateMusic
@ -594,12 +627,14 @@ fun AppExpandedPlayer(
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 16.dp), .padding(bottom = 16.dp),
audioPlayer = audioPlayer, audioPlayer = audioPlayer,
onExpand = onExpandLyrics) onExpand = onExpandLyrics
)
} }
MillenniumSnackbarHost( MillenniumSnackbarHost(
hostState = snackbarHostState, hostState = snackbarHostState,
modifier = Modifier.align(Alignment.BottomCenter)) modifier = Modifier.align(Alignment.BottomCenter)
)
} }
} }
@ -607,13 +642,15 @@ fun AppExpandedPlayer(
@Composable @Composable
private fun rememberSharedAlbumArtModifier( private fun rememberSharedAlbumArtModifier(
sharedTransitionScope: SharedTransitionScope?, sharedTransitionScope: SharedTransitionScope?,
animatedVisibilityScope: AnimatedVisibilityScope?): Modifier { animatedVisibilityScope: AnimatedVisibilityScope?
): Modifier {
if (sharedTransitionScope == null || animatedVisibilityScope == null) return Modifier if (sharedTransitionScope == null || animatedVisibilityScope == null) return Modifier
with(sharedTransitionScope) { with(sharedTransitionScope) {
return Modifier.sharedElement( return Modifier.sharedElement(
sharedContentState = rememberSharedContentState(key = "player_album_art"), sharedContentState = rememberSharedContentState(key = "player_album_art"),
animatedVisibilityScope = animatedVisibilityScope) animatedVisibilityScope = animatedVisibilityScope
)
} }
} }
@ -660,24 +697,30 @@ private fun LyricsPreviewCard(
.millenniumSurface( .millenniumSurface(
background = SolidColor(AppTheme.colors.surfaceContainerHighest), background = SolidColor(AppTheme.colors.surfaceContainerHighest),
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
shadowElevation = 2.dp) shadowElevation = 2.dp
.clickable(onClick = onExpand)) { )
.clickable(onClick = onExpand)
) {
Column(modifier = Modifier.padding(12.dp)) { Column(modifier = Modifier.padding(12.dp)) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) { verticalAlignment = Alignment.CenterVertically
) {
Text( Text(
text = "Lyrics", text = "Lyrics",
style = AppTheme.typography.labelMedium, style = AppTheme.typography.labelMedium,
color = AppTheme.colors.onSurfaceVariant) color = AppTheme.colors.onSurfaceVariant
)
MillenniumIconButton( MillenniumIconButton(
onClick = onExpand, onClick = onExpand,
style = MillenniumIconButtonStyle.Quiet) { style = MillenniumIconButtonStyle.Quiet
) {
MillenniumIcon( MillenniumIcon(
Iconsax.IconsaxArrowSquareUp, Iconsax.IconsaxArrowSquareUp,
contentDescription = "Expand lyrics", contentDescription = "Expand lyrics",
tint = AppTheme.colors.onSurfaceVariant) tint = AppTheme.colors.onSurfaceVariant
)
} }
} }
@ -687,7 +730,8 @@ private fun LyricsPreviewCard(
.fillMaxWidth() .fillMaxWidth()
.height(120.dp), .height(120.dp),
contentPadding = PaddingValues(vertical = 4.dp), contentPadding = PaddingValues(vertical = 4.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)) { verticalArrangement = Arrangement.spacedBy(2.dp)
) {
val displayLines = syncedLyrics.take(6) val displayLines = syncedLyrics.take(6)
itemsIndexed(displayLines) { index, line -> itemsIndexed(displayLines) { index, line ->
val isCurrent = val isCurrent =
@ -707,7 +751,8 @@ private fun LyricsPreviewCard(
), ),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center) textAlign = TextAlign.Center
)
} }
} }
} }
@ -719,21 +764,25 @@ private fun OptionTile(
icon: ImageVector, icon: ImageVector,
label: String, label: String,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier) { modifier: Modifier = Modifier
) {
MillenniumCard( MillenniumCard(
modifier = modifier modifier = modifier
.aspectRatio(1f) .aspectRatio(1f)
.clickable(onClick = onClick), .clickable(onClick = onClick),
contentPadding = PaddingValues(12.dp)) { contentPadding = PaddingValues(12.dp)
) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center) { verticalArrangement = Arrangement.Center
) {
MillenniumIcon( MillenniumIcon(
imageVector = icon, imageVector = icon,
contentDescription = label, contentDescription = label,
modifier = Modifier.size(28.dp), modifier = Modifier.size(28.dp),
tint = AppTheme.colors.onSurface) tint = AppTheme.colors.onSurface
)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = label, text = label,
@ -741,7 +790,8 @@ private fun OptionTile(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
color = AppTheme.colors.onSurfaceVariant) color = AppTheme.colors.onSurfaceVariant
)
} }
} }
} }

View File

@ -18,12 +18,6 @@
package dev.krtirtho.spotube.modules.shell 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.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -55,13 +44,14 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.composeunstyled.AnchorSide import com.composeunstyled.AnchorSide
import com.composeunstyled.Text
import com.composeunstyled.LocalTextStyle
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.blur.HazeColorEffect import dev.chrisbanes.haze.blur.HazeColorEffect
import dev.chrisbanes.haze.blur.blurEffect 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.jam.JamRoomService
import dev.krtirtho.spotube.core.navigation.NavigationCommands import dev.krtirtho.spotube.core.navigation.NavigationCommands
import dev.krtirtho.spotube.core.navigation.Routes import dev.krtirtho.spotube.core.navigation.Routes
import dev.krtirtho.spotube.core.ui.theming.AppTheme
import dev.krtirtho.spotube.modules.downloads.DownloadProgressIcon import dev.krtirtho.spotube.modules.downloads.DownloadProgressIcon
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel 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.SAVED_TRACKS_COLLECTION_ID
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel
import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.Iconsax3DotsMore 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.IconsaxDirectboxReceive
import dev.krtirtho.spotube.resources.iconsax.IconsaxInformation
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusic import dev.krtirtho.spotube.resources.iconsax.IconsaxMusic
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicFilter import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicFilter
import dev.krtirtho.spotube.resources.iconsax.IconsaxNext 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.IconsaxVolumeCross
import dev.krtirtho.spotube.resources.iconsax.IconsaxVolumeHigh import dev.krtirtho.spotube.resources.iconsax.IconsaxVolumeHigh
import dev.krtirtho.spotube.resources.iconsax.IconsaxVolumeLow import dev.krtirtho.spotube.resources.iconsax.IconsaxVolumeLow
import dev.krtirtho.spotube.resources.iconsax.InconsaxClock
import dev.krtirtho.spotube.resources.iconsax.SwapHorizontal2 import dev.krtirtho.spotube.resources.iconsax.SwapHorizontal2
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -104,6 +92,12 @@ import org.jetbrains.compose.resources.stringResource
import org.koin.compose.koinInject import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
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.Res
import spotube.composeapp.generated.resources.player_go_to_album import spotube.composeapp.generated.resources.player_go_to_album
import spotube.composeapp.generated.resources.player_sleep_timer import spotube.composeapp.generated.resources.player_sleep_timer
@ -346,7 +340,8 @@ fun AppLargePlayer(
) { ) {
MillenniumIcon( MillenniumIcon(
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay, 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( MillenniumIconButton(