mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-11-04 10:13:59 +00:00
feat(ui): integrate LocalTextStyle for dynamic tinting in player controls and UI components
This commit is contained in:
parent
58dfa2de92
commit
fb96984d6e
17
.mcp.json
Normal file
17
.mcp.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,7 +118,8 @@ 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) {
|
||||
@ -140,7 +142,8 @@ fun ArtistScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(bottom = LocalAppShellBottomInset.current + 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)) {
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
item {
|
||||
ArtistHeaderCard(
|
||||
artist = currentState.artist,
|
||||
@ -153,7 +156,8 @@ fun ArtistScreen(
|
||||
item {
|
||||
TopTracksHeader(
|
||||
onPlay = viewModel::playTopTracks,
|
||||
onAddToQueue = viewModel::addTopTracksToQueue)
|
||||
onAddToQueue = viewModel::addTopTracksToQueue
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
@ -191,7 +195,8 @@ fun ArtistScreen(
|
||||
onBulkAddToPlaylist = viewModel::showAddToPlaylistPicker,
|
||||
onBulkAddToJam = viewModel::addTracksToJam,
|
||||
isInJam = jamActive,
|
||||
isJamGuest = isJamGuest)
|
||||
isJamGuest = isJamGuest
|
||||
)
|
||||
}
|
||||
|
||||
if (currentState.albums.isNotEmpty() || currentState.albumsNextPagination != null) {
|
||||
@ -247,7 +252,8 @@ fun ArtistScreen(
|
||||
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<MetadataAlbum.Detailed>,
|
||||
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<MetadataArtist.Basic>,
|
||||
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<MetadataPlaylist>,
|
||||
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 {
|
||||
|
||||
@ -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<RemoteControlViewModel>()
|
||||
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,17 +122,20 @@ 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"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -146,6 +154,7 @@ fun RemoteControlScreen(
|
||||
modifier = Modifier.padding(bottom = shellBottomInset)
|
||||
)
|
||||
}
|
||||
|
||||
is ConnectionState.Connecting -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@ -156,6 +165,7 @@ fun RemoteControlScreen(
|
||||
Text("Connecting...")
|
||||
}
|
||||
}
|
||||
|
||||
is ConnectionState.Disconnected -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@ -166,6 +176,7 @@ fun RemoteControlScreen(
|
||||
Text("Disconnected")
|
||||
}
|
||||
}
|
||||
|
||||
is ConnectionState.Error -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@ -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,7 +235,8 @@ private fun RemoteControlContent(
|
||||
onSetVolume: (Float) -> Unit,
|
||||
onToggleShuffle: () -> Unit,
|
||||
onCycleLoopMode: () -> Unit,
|
||||
modifier: Modifier = Modifier) {
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
@ -229,7 +244,8 @@ private fun RemoteControlContent(
|
||||
.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)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -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<SavedTracksViewModel>(
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user