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"
|
"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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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')}"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user