Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ class ItemDetailsTest {
DataState.NoData(),
),
geEditablePlaylists = suspend { emptyList() },
fetchColors = { null },
)
}

Expand All @@ -73,7 +72,6 @@ class ItemDetailsTest {
playableItemsState = DataState.Data(tracks),
),
geEditablePlaylists = suspend { emptyList() },
fetchColors = { null },
)
}

Expand All @@ -98,7 +96,6 @@ class ItemDetailsTest {
playableItemsState = DataState.Data(emptyList()),
),
geEditablePlaylists = suspend { emptyList() },
fetchColors = { null },
)
}

Expand All @@ -117,7 +114,6 @@ class ItemDetailsTest {
albumsState = DataState.NoData(),
playableItemsState = DataState.Data(emptyList()),
),
fetchColors = { null },
)
}

Expand All @@ -138,7 +134,6 @@ class ItemDetailsTest {
playableItemsState = DataState.Data(tracks),
),
geEditablePlaylists = suspend { emptyList() },
fetchColors = { null },
)
}

Expand All @@ -165,7 +160,6 @@ class ItemDetailsTest {
playableItemsState = DataState.Data(episodes),
),
geEditablePlaylists = suspend { emptyList() },
fetchColors = { null },
)
}

Expand All @@ -188,7 +182,6 @@ class ItemDetailsTest {
playableItemsState = DataState.NoData(),
),
geEditablePlaylists = suspend { emptyList() },
fetchColors = { null },
)
}

Expand Down Expand Up @@ -216,7 +209,6 @@ class ItemDetailsTest {
ItemDetails(
state = state.value,
geEditablePlaylists = suspend { emptyList() },
fetchColors = { null },
onPlayClick = onPlayClick,
)
}
Expand Down Expand Up @@ -258,7 +250,6 @@ class ItemDetailsTest {
state = state.value,
onBack = onBack,
geEditablePlaylists = suspend { emptyList() },
fetchColors = { null },
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
Expand All @@ -48,7 +47,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.music_assistant.client.data.model.client.AppMediaItemFixtures
import io.music_assistant.client.data.model.client.Chapter
import io.music_assistant.client.data.model.client.ClickContext
import io.music_assistant.client.data.model.client.ImageType
import io.music_assistant.client.data.model.client.MediaType
import io.music_assistant.client.data.model.client.QueueOption
import io.music_assistant.client.data.model.client.SortConfig
Expand All @@ -68,7 +66,6 @@ import io.music_assistant.client.data.model.client.stringResource
import io.music_assistant.client.data.model.client.toClickContext
import io.music_assistant.client.settings.ViewMode
import io.music_assistant.client.ui.compose.common.DataState
import io.music_assistant.client.ui.compose.common.ExtractedColorsFetcher
import io.music_assistant.client.ui.compose.common.SortChip
import io.music_assistant.client.ui.compose.common.ToastHost
import io.music_assistant.client.ui.compose.common.ToastState
Expand All @@ -83,8 +80,6 @@ import io.music_assistant.client.ui.compose.common.items.ProvideClickActions
import io.music_assistant.client.ui.compose.common.items.TrackWithMenu
import io.music_assistant.client.ui.compose.common.items.supportsAddToPlaylist
import io.music_assistant.client.ui.compose.common.providers.ProviderIcon
import io.music_assistant.client.ui.compose.common.rememberAnimatedPlayerColors
import io.music_assistant.client.ui.compose.common.rememberExtractedColorsFetcher
import io.music_assistant.client.ui.compose.common.rememberToastState
import io.music_assistant.client.ui.compose.common.viewmodel.ActionsViewModel
import io.music_assistant.client.ui.compose.nav.Screen
Expand Down Expand Up @@ -161,7 +156,6 @@ fun ItemDetails(
toastState: ToastState = rememberToastState(),
onNavigateToItem: (String, MediaType, String) -> Unit = { _, _, _ -> },
geEditablePlaylists: suspend () -> List<Playlist> = suspend { emptyList() },
fetchColors: ExtractedColorsFetcher? = null,
addToPlaylist: (String?, Playlist) -> Unit = { _, _ -> },
onLibraryClick: (AppMediaItem) -> Unit = {},
onFavoriteClick: (AppMediaItem) -> Unit = {},
Expand Down Expand Up @@ -236,7 +230,6 @@ fun ItemDetails(
onRemoveFromPlaylist = onRemoveFromPlaylist,
libraryActions = libraryActions,
providerIconFetcher = providerIconFetcher,
fetchColors = fetchColors,
onBack = onBack,
onToggleViewMode = onToggleViewMode,
onAlbumsSortChanged = onAlbumsSortChanged,
Expand Down Expand Up @@ -269,7 +262,6 @@ private fun ItemChildren(
onRemoveFromPlaylist: (String, Int) -> Unit,
libraryActions: LibraryActions,
providerIconFetcher: (@Composable (Modifier, String) -> Unit),
fetchColors: ExtractedColorsFetcher?,
onBack: () -> Unit,
onToggleViewMode: (MediaType) -> Unit,
onAlbumsSortChanged: (SubItemContext, SortOption) -> Unit,
Expand Down Expand Up @@ -308,7 +300,6 @@ private fun ItemChildren(
onRemoveFromPlaylist = onRemoveFromPlaylist,
libraryActions = libraryActions,
providerIconFetcher = providerIconFetcher,
fetchColors = fetchColors,
onBack = onBack,
onToggleViewMode = onToggleViewMode,
onAlbumsSortChanged = onAlbumsSortChanged,
Expand Down Expand Up @@ -343,7 +334,6 @@ private fun ItemContent(
onRemoveFromPlaylist: (String, Int) -> Unit,
libraryActions: LibraryActions,
providerIconFetcher: @Composable (Modifier, String) -> Unit,
fetchColors: ExtractedColorsFetcher?,
onBack: () -> Unit,
viewModeProvider: @Composable (MediaType) -> ViewMode,
onToggleViewMode: (MediaType) -> Unit,
Expand All @@ -355,28 +345,10 @@ private fun ItemContent(
// Tabs, the loading gate, and the selected tab are all derived in ItemDetailsViewModel.State.
val tabs = state.tabs

// Artwork-driven header colors. Library items carry no server palette, so colors are
// extracted locally from the thumbnail (cached by DominantColorViewModel) — same path
// as the player. The fetcher is Koin-backed, so fall back to a no-op when one isn't
// supplied and there's no Koin graph (under @Preview or in tests).
val resolvedFetchColors: ExtractedColorsFetcher = fetchColors
?: if (LocalInspectionMode.current) {
{ null }
} else {
rememberExtractedColorsFetcher()
}
val colors by rememberAnimatedPlayerColors(
imageUrl = item.image(ImageType.THUMB)?.url,
palette = null,
fallback = MaterialTheme.colorScheme.primaryContainer,
fetchColors = resolvedFetchColors,
)

val heroSlot: @Composable () -> Unit = {
ProvideClickActions(ClickContext.DETAIL) {
ItemHeader(
item = item,
colors = colors,
providerIconFetcher = providerIconFetcher,
onPlayClick = onPlayItemClick,
)
Expand All @@ -387,7 +359,6 @@ private fun ItemContent(
topBar = {
ItemTopBar(
item = item,
colors = colors,
onBack = onBack,
libraryActions = libraryActions,
playlistActions = playlistActions.takeIf { item.supportsAddToPlaylist },
Expand Down Expand Up @@ -420,7 +391,6 @@ private fun ItemContent(
TabsBar(
tabs = tabs,
selectedIndex = safeIndex,
controlTint = colors.controlTint,
onTabSelected = { onTabSelected(tabs[it]) },
albumsSortOption = state.albumsSortOption,
playableItemsSortOption = state.playableItemsSortOption,
Expand Down Expand Up @@ -463,7 +433,7 @@ private fun ItemContent(
private fun TabsBar(
tabs: List<ItemDetailsTab>,
selectedIndex: Int,
controlTint: Color,
controlTint: Color = MaterialTheme.colorScheme.primary,
onTabSelected: (Int) -> Unit,
albumsSortOption: SortOption?,
playableItemsSortOption: SortOption?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,7 @@ import org.jetbrains.compose.resources.stringResource
@Composable
fun ItemHeader(
item: AppMediaItem,
colors: PlayerColors = PlayerColors(
MaterialTheme.colorScheme.primaryContainer,
MaterialTheme.colorScheme.primary,
),
colors: PlayerColors? = null,
providerIconFetcher: (@Composable (Modifier, String) -> Unit)? = null,
onPlayClick: (QueueOption, Boolean) -> Unit = { _, _ -> },
) {
Expand All @@ -90,11 +87,17 @@ fun ItemHeader(
Column(
modifier = Modifier
.fillMaxWidth()
.background(
Brush.verticalGradient(
listOf(colors.dominant.inactive(), MaterialTheme.colorScheme.surface),
),
),
.let {
Comment thread
seadowg marked this conversation as resolved.
if (colors != null) {
it.background(
Brush.verticalGradient(
listOf(colors.dominant.inactive(), MaterialTheme.colorScheme.surface),
),
)
} else {
it
}
},
) {
val image = @Composable {
Image(
Expand All @@ -108,7 +111,7 @@ fun ItemHeader(
ItemPlayButton(
item,
onPlayClick = onPlayClick,
tint = colors.controlTint,
tint = colors?.controlTint,
modifier = Modifier.padding(top = 8.dp),
)
}
Expand Down Expand Up @@ -138,10 +141,7 @@ fun ItemHeader(
@Composable
internal fun ItemTopBar(
item: AppMediaItem,
colors: PlayerColors = PlayerColors(
MaterialTheme.colorScheme.primaryContainer,
MaterialTheme.colorScheme.primary,
),
colors: PlayerColors? = null,
onBack: () -> Unit,
libraryActions: LibraryActions?,
playlistActions: PlaylistActions?,
Expand All @@ -150,23 +150,37 @@ internal fun ItemTopBar(
// Flat fill equal to the header gradient's top color, so the bar reads as one
// continuous wash with the header below it. Back/overflow icons are NOT control-tinted
// — just black or white per the composited bar luminance, keeping them legible.
val barBg = colors.dominant.inactive()
val onBar = lerp(MaterialTheme.colorScheme.surface, colors.dominant, INACTIVE_ALPHA)
.contentColorByLuminance()
val topAppBarColors = if (colors != null) {
val onBar = lerp(MaterialTheme.colorScheme.surface, colors.dominant, INACTIVE_ALPHA)
.contentColorByLuminance()

TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent,
navigationIconContentColor = onBar,
actionIconContentColor = onBar,
titleContentColor = onBar,
)
} else {
TopAppBarDefaults.topAppBarColors()
}

// Paint barBg directly on the wrapping Box rather than via TopAppBar's containerColor:
// TopAppBar runs its own animateColorAsState on the container, which would stack a second
// tween on top of our color animation and visibly lag the header. Transparent container =
// single-pass color change, in lockstep with the header gradient.
Box(modifier = Modifier.background(barBg)) {
Box(
modifier = Modifier.let {
if (colors != null) {
it.background(colors.dominant.inactive())
} else {
it
}
},
) {
TopAppBar(
title = {},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent,
navigationIconContentColor = onBar,
actionIconContentColor = onBar,
titleContentColor = onBar,
),
colors = topAppBarColors,
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SplitButtonDefaults.LeadingButton
import androidx.compose.material3.SplitButtonDefaults.TrailingButton
import androidx.compose.material3.SplitButtonLayout
Expand Down Expand Up @@ -44,17 +43,21 @@ import org.jetbrains.compose.resources.stringResource
fun ItemPlayButton(
item: AppMediaItem,
onPlayClick: (QueueOption, Boolean) -> Unit,
tint: Color = MaterialTheme.colorScheme.primary,
tint: Color? = null,
modifier: Modifier = Modifier,
) {
if (!item.isPlayable) return

// Art-derived control tint as the button fill; black/white content per its luminance.
val onTint = tint.contentColorByLuminance()
val buttonColors = ButtonDefaults.buttonColors(
containerColor = tint,
contentColor = onTint,
)
val buttonColors = if (tint != null) {
// Art-derived control tint as the button fill; black/white content per its luminance.
val onTint = tint.contentColorByLuminance()
ButtonDefaults.buttonColors(
containerColor = tint,
contentColor = onTint,
)
} else {
ButtonDefaults.buttonColors()
}

// The detail header is wrapped in ProvideClickActions(DETAIL), so the config resolves
// this item's DETAIL default. effectiveActionFor is non-null here (item is playable).
Expand Down
Loading