M0: repo scaffolding, CI pipelines, and Go skeleton #1

Merged
bvandeusen merged 262 commits from dev into main 2026-04-18 18:00:46 -04:00
10 changed files with 147 additions and 37 deletions
Showing only changes of commit 208a7d056b - Show all commits
@@ -10,6 +10,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -17,43 +18,51 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.fabledsword.minstrel.auth.ui.AuthGateViewModel import com.fabledsword.minstrel.auth.ui.AuthGateViewModel
import com.fabledsword.minstrel.nav.DetailSeedCache
import com.fabledsword.minstrel.nav.LocalDetailSeedCache
import com.fabledsword.minstrel.nav.MinstrelNavGraph import com.fabledsword.minstrel.nav.MinstrelNavGraph
import com.fabledsword.minstrel.theme.MinstrelTheme import com.fabledsword.minstrel.theme.MinstrelTheme
import com.fabledsword.minstrel.theme.ThemePreferenceViewModel import com.fabledsword.minstrel.theme.ThemePreferenceViewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@Inject lateinit var seedCache: DetailSeedCache
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContent { App() } setContent { App(seedCache = seedCache) }
} }
} }
@Composable @Composable
private fun App( private fun App(
seedCache: DetailSeedCache,
themeVm: ThemePreferenceViewModel = hiltViewModel(), themeVm: ThemePreferenceViewModel = hiltViewModel(),
gate: AuthGateViewModel = hiltViewModel(), gate: AuthGateViewModel = hiltViewModel(),
) { ) {
val theme by themeVm.themeMode.collectAsStateWithLifecycle() val theme by themeVm.themeMode.collectAsStateWithLifecycle()
MinstrelTheme(darkOverride = theme.toDarkOverride()) { MinstrelTheme(darkOverride = theme.toDarkOverride()) {
val startDestination by gate.startDestination.collectAsStateWithLifecycle() CompositionLocalProvider(LocalDetailSeedCache provides seedCache) {
val resolved = startDestination val startDestination by gate.startDestination.collectAsStateWithLifecycle()
if (resolved == null) { val resolved = startDestination
BootSplash() if (resolved == null) {
} else { BootSplash()
// No bottom nav, no drawer — navigation is per-screen via } else {
// the AppBar actions row + kebab (`MainAppBarActions`). The // No bottom nav, no drawer — navigation is per-screen via
// MiniPlayer is included by each in-shell route's // the AppBar actions row + kebab (`MainAppBarActions`). The
// `ShellScaffold` wrap; full-screen routes (NowPlaying / // MiniPlayer is included by each in-shell route's
// Queue / unauthenticated) bypass the shell entirely. // `ShellScaffold` wrap; full-screen routes (NowPlaying /
val navController = rememberNavController() // Queue / unauthenticated) bypass the shell entirely.
MinstrelNavGraph( val navController = rememberNavController()
navController = navController, MinstrelNavGraph(
startDestination = resolved, navController = navController,
modifier = Modifier.fillMaxSize(), startDestination = resolved,
) modifier = Modifier.fillMaxSize(),
)
}
} }
} }
} }
@@ -95,7 +95,7 @@ fun AlbumDetailScreen(
modifier = Modifier.fillMaxSize().padding(inner), modifier = Modifier.fillMaxSize().padding(inner),
) { ) {
when (val s = state) { when (val s = state) {
AlbumDetailUiState.Loading -> LoadingCentered() is AlbumDetailUiState.Loading -> LoadingCentered()
is AlbumDetailUiState.Error -> EmptyState( is AlbumDetailUiState.Error -> EmptyState(
title = "Couldn't load album", title = "Couldn't load album",
body = s.message, body = s.message,
@@ -125,7 +125,8 @@ fun AlbumDetailScreen(
private fun titleFor(state: AlbumDetailUiState): String = when (state) { private fun titleFor(state: AlbumDetailUiState): String = when (state) {
is AlbumDetailUiState.Success -> state.detail.album.title is AlbumDetailUiState.Success -> state.detail.album.title
else -> "Album" is AlbumDetailUiState.Loading -> state.seed?.title ?: "Album"
is AlbumDetailUiState.Error -> "Album"
} }
@Composable @Composable
@@ -7,7 +7,9 @@ import androidx.navigation.toRoute
import com.fabledsword.minstrel.library.data.LibraryRepository import com.fabledsword.minstrel.library.data.LibraryRepository
import com.fabledsword.minstrel.likes.data.LikesRepository import com.fabledsword.minstrel.likes.data.LikesRepository
import com.fabledsword.minstrel.models.AlbumDetailRef import com.fabledsword.minstrel.models.AlbumDetailRef
import com.fabledsword.minstrel.models.AlbumRef
import com.fabledsword.minstrel.nav.AlbumDetail import com.fabledsword.minstrel.nav.AlbumDetail
import com.fabledsword.minstrel.nav.DetailSeedCache
import com.fabledsword.minstrel.player.PlayerController import com.fabledsword.minstrel.player.PlayerController
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -23,7 +25,7 @@ import javax.inject.Inject
private const val SHARE_STOP_TIMEOUT_MS = 5_000L private const val SHARE_STOP_TIMEOUT_MS = 5_000L
sealed interface AlbumDetailUiState { sealed interface AlbumDetailUiState {
data object Loading : AlbumDetailUiState data class Loading(val seed: AlbumRef? = null) : AlbumDetailUiState
data class Success(val detail: AlbumDetailRef) : AlbumDetailUiState data class Success(val detail: AlbumDetailRef) : AlbumDetailUiState
data class Error(val message: String) : AlbumDetailUiState data class Error(val message: String) : AlbumDetailUiState
} }
@@ -33,13 +35,15 @@ class AlbumDetailViewModel @Inject constructor(
private val repository: LibraryRepository, private val repository: LibraryRepository,
private val likes: LikesRepository, private val likes: LikesRepository,
private val player: PlayerController, private val player: PlayerController,
private val seedCache: DetailSeedCache,
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
) : ViewModel() { ) : ViewModel() {
private val route: AlbumDetail = savedStateHandle.toRoute() private val route: AlbumDetail = savedStateHandle.toRoute()
val albumId: String = route.id val albumId: String = route.id
private val internal = MutableStateFlow<AlbumDetailUiState>(AlbumDetailUiState.Loading) private val internal =
MutableStateFlow<AlbumDetailUiState>(AlbumDetailUiState.Loading())
val uiState: StateFlow<AlbumDetailUiState> = internal.asStateFlow() val uiState: StateFlow<AlbumDetailUiState> = internal.asStateFlow()
val albumLiked: StateFlow<Boolean> = val albumLiked: StateFlow<Boolean> =
@@ -64,7 +68,12 @@ class AlbumDetailViewModel @Inject constructor(
} }
fun refresh(): Job = viewModelScope.launch { fun refresh(): Job = viewModelScope.launch {
internal.value = AlbumDetailUiState.Loading val carried = when (val cur = internal.value) {
is AlbumDetailUiState.Loading -> cur.seed
is AlbumDetailUiState.Success -> cur.detail.album
is AlbumDetailUiState.Error -> null
}
internal.value = AlbumDetailUiState.Loading(carried ?: seedCache.peekAlbum(albumId))
try { try {
val detail = repository.refreshAlbumDetail(albumId) val detail = repository.refreshAlbumDetail(albumId)
internal.value = AlbumDetailUiState.Success(detail) internal.value = AlbumDetailUiState.Success(detail)
@@ -76,7 +76,7 @@ fun ArtistDetailScreen(
modifier = Modifier.fillMaxSize().padding(inner), modifier = Modifier.fillMaxSize().padding(inner),
) { ) {
when (val s = state) { when (val s = state) {
ArtistDetailUiState.Loading -> LoadingCentered() is ArtistDetailUiState.Loading -> LoadingCentered()
is ArtistDetailUiState.Error -> EmptyState( is ArtistDetailUiState.Error -> EmptyState(
title = "Couldn't load artist", title = "Couldn't load artist",
body = s.message, body = s.message,
@@ -98,7 +98,8 @@ fun ArtistDetailScreen(
private fun titleFor(state: ArtistDetailUiState): String = when (state) { private fun titleFor(state: ArtistDetailUiState): String = when (state) {
is ArtistDetailUiState.Success -> state.detail.artist.name is ArtistDetailUiState.Success -> state.detail.artist.name
else -> "Artist" is ArtistDetailUiState.Loading -> state.seed?.name ?: "Artist"
is ArtistDetailUiState.Error -> "Artist"
} }
@Composable @Composable
@@ -7,7 +7,9 @@ import androidx.navigation.toRoute
import com.fabledsword.minstrel.library.data.LibraryRepository import com.fabledsword.minstrel.library.data.LibraryRepository
import com.fabledsword.minstrel.likes.data.LikesRepository import com.fabledsword.minstrel.likes.data.LikesRepository
import com.fabledsword.minstrel.models.ArtistDetailRef import com.fabledsword.minstrel.models.ArtistDetailRef
import com.fabledsword.minstrel.models.ArtistRef
import com.fabledsword.minstrel.nav.ArtistDetail import com.fabledsword.minstrel.nav.ArtistDetail
import com.fabledsword.minstrel.nav.DetailSeedCache
import com.fabledsword.minstrel.player.PlayerController import com.fabledsword.minstrel.player.PlayerController
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -22,7 +24,7 @@ import javax.inject.Inject
private const val SHARE_STOP_TIMEOUT_MS = 5_000L private const val SHARE_STOP_TIMEOUT_MS = 5_000L
sealed interface ArtistDetailUiState { sealed interface ArtistDetailUiState {
data object Loading : ArtistDetailUiState data class Loading(val seed: ArtistRef? = null) : ArtistDetailUiState
data class Success(val detail: ArtistDetailRef) : ArtistDetailUiState data class Success(val detail: ArtistDetailRef) : ArtistDetailUiState
data class Error(val message: String) : ArtistDetailUiState data class Error(val message: String) : ArtistDetailUiState
} }
@@ -32,13 +34,15 @@ class ArtistDetailViewModel @Inject constructor(
private val repository: LibraryRepository, private val repository: LibraryRepository,
private val likes: LikesRepository, private val likes: LikesRepository,
private val player: PlayerController, private val player: PlayerController,
private val seedCache: DetailSeedCache,
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
) : ViewModel() { ) : ViewModel() {
private val route: ArtistDetail = savedStateHandle.toRoute() private val route: ArtistDetail = savedStateHandle.toRoute()
val artistId: String = route.id val artistId: String = route.id
private val internal = MutableStateFlow<ArtistDetailUiState>(ArtistDetailUiState.Loading) private val internal =
MutableStateFlow<ArtistDetailUiState>(ArtistDetailUiState.Loading())
val uiState: StateFlow<ArtistDetailUiState> = internal.asStateFlow() val uiState: StateFlow<ArtistDetailUiState> = internal.asStateFlow()
val artistLiked: StateFlow<Boolean> = val artistLiked: StateFlow<Boolean> =
@@ -61,7 +65,12 @@ class ArtistDetailViewModel @Inject constructor(
} }
fun refresh(): Job = viewModelScope.launch { fun refresh(): Job = viewModelScope.launch {
internal.value = ArtistDetailUiState.Loading val carried = when (val cur = internal.value) {
is ArtistDetailUiState.Loading -> cur.seed
is ArtistDetailUiState.Success -> cur.detail.artist
is ArtistDetailUiState.Error -> null
}
internal.value = ArtistDetailUiState.Loading(carried ?: seedCache.peekArtist(artistId))
try { try {
val detail = repository.refreshArtistDetail(artistId) val detail = repository.refreshArtistDetail(artistId)
internal.value = ArtistDetailUiState.Success(detail) internal.value = ArtistDetailUiState.Success(detail)
@@ -27,12 +27,15 @@ import com.composables.icons.lucide.Disc3
import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.Lucide
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import com.fabledsword.minstrel.models.AlbumRef import com.fabledsword.minstrel.models.AlbumRef
import com.fabledsword.minstrel.nav.LocalDetailSeedCache
import com.fabledsword.minstrel.theme.FabledSwordFlatTokens import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
/** /**
* One album tile. Mirrors the Flutter `AlbumCard` (~176dp wide, 144dp * One album tile. Mirrors the Flutter `AlbumCard` (~176dp wide, 144dp
* square cover). Tap fires `onClick` — wired to `AlbumDetail` nav in * square cover). Tap fires `onClick` — wired to `AlbumDetail` nav in
* Phase 5.5. * Phase 5.5. The tile stashes its `album` in [LocalDetailSeedCache]
* before firing onClick so the detail screen's AppBar renders title /
* cover / counts immediately without waiting for the fetch.
*/ */
@Composable @Composable
fun AlbumCard( fun AlbumCard(
@@ -40,10 +43,14 @@ fun AlbumCard(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val seedCache = LocalDetailSeedCache.current
Surface( Surface(
modifier = modifier modifier = modifier
.width(176.dp) .width(176.dp)
.clickable(onClick = onClick), .clickable {
seedCache.stashAlbum(album)
onClick()
},
color = Color.Transparent, color = Color.Transparent,
) { ) {
Column(modifier = Modifier.padding(horizontal = 8.dp)) { Column(modifier = Modifier.padding(horizontal = 8.dp)) {
@@ -27,10 +27,13 @@ import coil3.compose.AsyncImage
import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.User import com.composables.icons.lucide.User
import com.fabledsword.minstrel.models.ArtistRef import com.fabledsword.minstrel.models.ArtistRef
import com.fabledsword.minstrel.nav.LocalDetailSeedCache
/** /**
* One artist tile. Circular cover; name centered beneath. Tap fires * One artist tile. Circular cover; name centered beneath. Tap fires
* `onClick` — wired to `ArtistDetail` nav in Phase 5.5. * `onClick` — wired to `ArtistDetail` nav in Phase 5.5. Stashes the
* `artist` in [LocalDetailSeedCache] on click so the ArtistDetail
* AppBar renders the name before the fetch lands.
*/ */
@Composable @Composable
fun ArtistCard( fun ArtistCard(
@@ -38,10 +41,14 @@ fun ArtistCard(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val seedCache = LocalDetailSeedCache.current
Surface( Surface(
modifier = modifier modifier = modifier
.width(144.dp) .width(144.dp)
.clickable(onClick = onClick), .clickable {
seedCache.stashArtist(artist)
onClick()
},
color = Color.Transparent, color = Color.Transparent,
) { ) {
Column( Column(
@@ -0,0 +1,51 @@
package com.fabledsword.minstrel.nav
import androidx.compose.runtime.staticCompositionLocalOf
import com.fabledsword.minstrel.models.AlbumRef
import com.fabledsword.minstrel.models.ArtistRef
import com.fabledsword.minstrel.models.PlaylistRef
import javax.inject.Inject
import javax.inject.Singleton
/**
* In-memory hand-off for detail-screen "seed" data — the lightweight
* Ref the caller already has when navigating to AlbumDetail /
* ArtistDetail / PlaylistDetail. The detail VM peeks the seed on
* init so the AppBar header (title / cover / counts) renders
* immediately, before the network round-trip completes.
*
* Mirrors Flutter's go_router `extra:` parameter pattern. Bounded
* per-bucket via insertion-order LRU so back/forward navigation
* doesn't grow unbounded.
*/
@Singleton
class DetailSeedCache @Inject constructor() {
private val albums = lru<AlbumRef>(MAX_ENTRIES)
private val artists = lru<ArtistRef>(MAX_ENTRIES)
private val playlists = lru<PlaylistRef>(MAX_ENTRIES)
@Synchronized fun stashAlbum(ref: AlbumRef) { albums[ref.id] = ref }
@Synchronized fun stashArtist(ref: ArtistRef) { artists[ref.id] = ref }
@Synchronized fun stashPlaylist(ref: PlaylistRef) { playlists[ref.id] = ref }
@Synchronized fun peekAlbum(id: String): AlbumRef? = albums[id]
@Synchronized fun peekArtist(id: String): ArtistRef? = artists[id]
@Synchronized fun peekPlaylist(id: String): PlaylistRef? = playlists[id]
private companion object {
private const val MAX_ENTRIES = 32
private fun <V> lru(max: Int): MutableMap<String, V> =
object : LinkedHashMap<String, V>(max, 0.75f, true) {
override fun removeEldestEntry(eldest: Map.Entry<String, V>) = size > max
}
}
}
/**
* CompositionLocal carrying the app's [DetailSeedCache]. Provided
* at the [com.fabledsword.minstrel.MainActivity] root so any screen
* can stash a seed before calling `navController.navigate(...)`.
*/
val LocalDetailSeedCache = staticCompositionLocalOf<DetailSeedCache> {
error("DetailSeedCache not provided — wrap content in CompositionLocalProvider")
}
@@ -65,6 +65,7 @@ import com.fabledsword.minstrel.models.PlaylistTrackRef
import com.fabledsword.minstrel.models.TrackRef import com.fabledsword.minstrel.models.TrackRef
import com.fabledsword.minstrel.nav.AlbumDetail import com.fabledsword.minstrel.nav.AlbumDetail
import com.fabledsword.minstrel.nav.ArtistDetail import com.fabledsword.minstrel.nav.ArtistDetail
import com.fabledsword.minstrel.nav.DetailSeedCache
import com.fabledsword.minstrel.nav.PlaylistDetail import com.fabledsword.minstrel.nav.PlaylistDetail
import com.fabledsword.minstrel.player.PlayerController import com.fabledsword.minstrel.player.PlayerController
import com.fabledsword.minstrel.likes.data.LikesRepository import com.fabledsword.minstrel.likes.data.LikesRepository
@@ -98,7 +99,7 @@ private const val SHARE_STOP_TIMEOUT_MS = 5_000L
// ─── State ─────────────────────────────────────────────────────────── // ─── State ───────────────────────────────────────────────────────────
sealed interface PlaylistDetailUiState { sealed interface PlaylistDetailUiState {
data object Loading : PlaylistDetailUiState data class Loading(val seed: PlaylistRef? = null) : PlaylistDetailUiState
data class Success(val detail: PlaylistDetailRef) : PlaylistDetailUiState data class Success(val detail: PlaylistDetailRef) : PlaylistDetailUiState
data class Error(val message: String) : PlaylistDetailUiState data class Error(val message: String) : PlaylistDetailUiState
} }
@@ -111,13 +112,15 @@ class PlaylistDetailViewModel @Inject constructor(
private val likes: LikesRepository, private val likes: LikesRepository,
private val player: PlayerController, private val player: PlayerController,
private val eventsStream: EventsStream, private val eventsStream: EventsStream,
private val seedCache: DetailSeedCache,
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
) : ViewModel() { ) : ViewModel() {
private val route: PlaylistDetail = savedStateHandle.toRoute() private val route: PlaylistDetail = savedStateHandle.toRoute()
val playlistId: String = route.id val playlistId: String = route.id
private val internal = MutableStateFlow<PlaylistDetailUiState>(PlaylistDetailUiState.Loading) private val internal =
MutableStateFlow<PlaylistDetailUiState>(PlaylistDetailUiState.Loading())
val uiState: StateFlow<PlaylistDetailUiState> = internal.asStateFlow() val uiState: StateFlow<PlaylistDetailUiState> = internal.asStateFlow()
/** /**
@@ -178,7 +181,14 @@ class PlaylistDetailViewModel @Inject constructor(
fun refresh(): Job = viewModelScope.launch { fun refresh(): Job = viewModelScope.launch {
try { try {
internal.value = PlaylistDetailUiState.Loading val carried = when (val cur = internal.value) {
is PlaylistDetailUiState.Loading -> cur.seed
is PlaylistDetailUiState.Success -> cur.detail.playlist
is PlaylistDetailUiState.Error -> null
}
internal.value = PlaylistDetailUiState.Loading(
carried ?: seedCache.peekPlaylist(playlistId),
)
val detail = repository.refreshDetail(playlistId) val detail = repository.refreshDetail(playlistId)
internal.value = PlaylistDetailUiState.Success(detail) internal.value = PlaylistDetailUiState.Success(detail)
} catch ( } catch (
@@ -287,7 +297,7 @@ private fun PlaylistDetailContent(
navController: NavHostController, navController: NavHostController,
) { ) {
when (state) { when (state) {
PlaylistDetailUiState.Loading -> LoadingCentered() is PlaylistDetailUiState.Loading -> LoadingCentered()
is PlaylistDetailUiState.Error -> ErrorBlock(state.message, viewModel::refresh) is PlaylistDetailUiState.Error -> ErrorBlock(state.message, viewModel::refresh)
is PlaylistDetailUiState.Success -> { is PlaylistDetailUiState.Success -> {
val likedTrackIds by viewModel.likedTrackIds.collectAsState() val likedTrackIds by viewModel.likedTrackIds.collectAsState()
@@ -314,7 +324,8 @@ private fun PlaylistDetailContent(
private fun currentTitle(state: PlaylistDetailUiState): String = when (state) { private fun currentTitle(state: PlaylistDetailUiState): String = when (state) {
is PlaylistDetailUiState.Success -> state.detail.playlist.name is PlaylistDetailUiState.Success -> state.detail.playlist.name
else -> "Playlist" is PlaylistDetailUiState.Loading -> state.seed?.name ?: "Playlist"
is PlaylistDetailUiState.Error -> "Playlist"
} }
// ─── Body composables ──────────────────────────────────────────────── // ─── Body composables ────────────────────────────────────────────────
@@ -26,6 +26,7 @@ import coil3.compose.AsyncImage
import com.composables.icons.lucide.ListMusic import com.composables.icons.lucide.ListMusic
import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.Lucide
import com.fabledsword.minstrel.models.PlaylistRef import com.fabledsword.minstrel.models.PlaylistRef
import com.fabledsword.minstrel.nav.LocalDetailSeedCache
import com.fabledsword.minstrel.theme.FabledSwordFlatTokens import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
/** /**
@@ -43,10 +44,14 @@ fun PlaylistCard(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val seedCache = LocalDetailSeedCache.current
Surface( Surface(
modifier = modifier modifier = modifier
.width(176.dp) .width(176.dp)
.clickable(onClick = onClick), .clickable {
seedCache.stashPlaylist(playlist)
onClick()
},
color = Color.Transparent, color = Color.Transparent,
) { ) {
Column(modifier = Modifier.padding(horizontal = 8.dp)) { Column(modifier = Modifier.padding(horizontal = 8.dp)) {