Created
March 5, 2026 16:55
-
-
Save Kyriakos-Georgiopoulos/3953cc13de46b38bec1054e578e69bfc to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * Copyright 2026 Kyriakos Georgiopoulos | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| import android.app.Activity | |
| import androidx.compose.animation.AnimatedContent | |
| import androidx.compose.animation.AnimatedVisibility | |
| import androidx.compose.animation.AnimatedVisibilityScope | |
| import androidx.compose.animation.Crossfade | |
| import androidx.compose.animation.ExperimentalSharedTransitionApi | |
| import androidx.compose.animation.SharedTransitionLayout | |
| import androidx.compose.animation.SharedTransitionScope | |
| import androidx.compose.animation.animateColorAsState | |
| import androidx.compose.animation.core.Animatable | |
| import androidx.compose.animation.core.FastOutLinearInEasing | |
| import androidx.compose.animation.core.FastOutSlowInEasing | |
| import androidx.compose.animation.core.RepeatMode | |
| import androidx.compose.animation.core.Spring | |
| import androidx.compose.animation.core.animateDp | |
| import androidx.compose.animation.core.animateFloat | |
| import androidx.compose.animation.core.animateFloatAsState | |
| import androidx.compose.animation.core.infiniteRepeatable | |
| import androidx.compose.animation.core.rememberInfiniteTransition | |
| import androidx.compose.animation.core.snap | |
| import androidx.compose.animation.core.spring | |
| import androidx.compose.animation.core.tween | |
| import androidx.compose.animation.core.updateTransition | |
| import androidx.compose.animation.fadeIn | |
| import androidx.compose.animation.fadeOut | |
| import androidx.compose.animation.slideInVertically | |
| import androidx.compose.animation.slideOutVertically | |
| import androidx.compose.animation.togetherWith | |
| import androidx.compose.foundation.Canvas | |
| import androidx.compose.foundation.background | |
| import androidx.compose.foundation.border | |
| import androidx.compose.foundation.clickable | |
| import androidx.compose.foundation.interaction.MutableInteractionSource | |
| import androidx.compose.foundation.interaction.collectIsPressedAsState | |
| import androidx.compose.foundation.layout.Arrangement | |
| import androidx.compose.foundation.layout.Box | |
| import androidx.compose.foundation.layout.Column | |
| import androidx.compose.foundation.layout.PaddingValues | |
| import androidx.compose.foundation.layout.Row | |
| import androidx.compose.foundation.layout.Spacer | |
| import androidx.compose.foundation.layout.WindowInsets | |
| import androidx.compose.foundation.layout.asPaddingValues | |
| import androidx.compose.foundation.layout.fillMaxHeight | |
| import androidx.compose.foundation.layout.fillMaxSize | |
| import androidx.compose.foundation.layout.fillMaxWidth | |
| import androidx.compose.foundation.layout.height | |
| import androidx.compose.foundation.layout.offset | |
| import androidx.compose.foundation.layout.padding | |
| import androidx.compose.foundation.layout.requiredSize | |
| import androidx.compose.foundation.layout.size | |
| import androidx.compose.foundation.layout.statusBars | |
| import androidx.compose.foundation.layout.statusBarsPadding | |
| import androidx.compose.foundation.layout.width | |
| import androidx.compose.foundation.layout.wrapContentSize | |
| import androidx.compose.foundation.lazy.LazyColumn | |
| import androidx.compose.foundation.lazy.itemsIndexed | |
| import androidx.compose.foundation.lazy.rememberLazyListState | |
| import androidx.compose.foundation.shape.CircleShape | |
| import androidx.compose.foundation.shape.RoundedCornerShape | |
| import androidx.compose.material3.Text | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.LaunchedEffect | |
| import androidx.compose.runtime.SideEffect | |
| import androidx.compose.runtime.getValue | |
| import androidx.compose.runtime.mutableFloatStateOf | |
| import androidx.compose.runtime.mutableIntStateOf | |
| import androidx.compose.runtime.mutableLongStateOf | |
| import androidx.compose.runtime.mutableStateOf | |
| import androidx.compose.runtime.remember | |
| import androidx.compose.runtime.rememberCoroutineScope | |
| import androidx.compose.runtime.setValue | |
| import androidx.compose.runtime.withFrameMillis | |
| import androidx.compose.ui.Alignment | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.draw.BlurredEdgeTreatment | |
| import androidx.compose.ui.draw.blur | |
| import androidx.compose.ui.draw.clip | |
| import androidx.compose.ui.draw.drawWithContent | |
| import androidx.compose.ui.draw.scale | |
| import androidx.compose.ui.draw.shadow | |
| import androidx.compose.ui.geometry.CornerRadius | |
| import androidx.compose.ui.geometry.Offset | |
| import androidx.compose.ui.geometry.Rect | |
| import androidx.compose.ui.geometry.Size | |
| import androidx.compose.ui.graphics.BlendMode | |
| import androidx.compose.ui.graphics.Brush | |
| import androidx.compose.ui.graphics.Color | |
| import androidx.compose.ui.graphics.CompositingStrategy | |
| import androidx.compose.ui.graphics.Path | |
| import androidx.compose.ui.graphics.PathEffect | |
| import androidx.compose.ui.graphics.SolidColor | |
| import androidx.compose.ui.graphics.StrokeCap | |
| import androidx.compose.ui.graphics.StrokeJoin | |
| import androidx.compose.ui.graphics.TransformOrigin | |
| import androidx.compose.ui.graphics.drawscope.Stroke | |
| import androidx.compose.ui.graphics.graphicsLayer | |
| import androidx.compose.ui.platform.LocalDensity | |
| import androidx.compose.ui.platform.LocalView | |
| import androidx.compose.ui.text.font.FontFamily | |
| import androidx.compose.ui.text.font.FontWeight | |
| import androidx.compose.ui.unit.Dp | |
| import androidx.compose.ui.unit.dp | |
| import androidx.compose.ui.unit.sp | |
| import androidx.compose.ui.zIndex | |
| import androidx.core.view.WindowCompat | |
| import kotlinx.coroutines.delay | |
| import kotlinx.coroutines.launch | |
| // ========================================== | |
| // THEME CONSTANTS | |
| // ========================================== | |
| val VoidBlack = Color(color = 0xFF070709) | |
| val GlassSurface = Color(color = 0xFFFFFFFF).copy(alpha = 0.05f) | |
| /** | |
| * Represents the core navigation state of the application. | |
| */ | |
| enum class AppScreen { LIST, PLAYER, PROFILE } | |
| /** | |
| * Represents a musical track with a bespoke "Harmonic Luminescence" color profile. | |
| * | |
| * @property title The name of the track. | |
| * @property artist The artist of the track. | |
| * @property durationMs Total duration of the track in milliseconds. | |
| * @property primaryColor The core neon color used for highlights and ambient background blur. | |
| * @property primaryGradient The deep analogous gradient used for the album sleeve. | |
| * @property accentGradient The contrasting analogous gradient used for playback controls. | |
| * @property logoStyle An integer mapping to a unique geometric vector logo for this track. | |
| */ | |
| data class Track( | |
| val title: String, | |
| val artist: String, | |
| val durationMs: Long, | |
| val primaryColor: Color, | |
| val primaryGradient: List<Color>, | |
| val accentGradient: List<Color>, | |
| val logoStyle: Int | |
| ) | |
| val Playlist = listOf( | |
| Track( | |
| "GLASS ECHOES", | |
| "Data Luv", | |
| 192000L, | |
| Color(0xFF00E5FF), | |
| listOf(Color(0xFF001122), Color(0xFF0088FF)), | |
| listOf(Color(0xFF00E5FF), Color(0xFF00FF9D)), | |
| 0 | |
| ), | |
| Track( | |
| "SYNTHETIC SOUL", | |
| "Analog Void", | |
| 198000L, | |
| Color(0xFFFF3366), | |
| listOf(Color(0xFF1A0011), Color(0xFFCC0044)), | |
| listOf(Color(0xFFFF3366), Color(0xFFFF7755)), | |
| 1 | |
| ), | |
| Track( | |
| "CHROME HORIZON", | |
| "Vector 9", | |
| 184000L, | |
| Color(0xFFFFB800), | |
| listOf(Color(0xFF2A0D00), Color(0xFFCC5500)), | |
| listOf(Color(0xFFFF8800), Color(0xFFFFD700)), | |
| 2 | |
| ), | |
| Track( | |
| "VOID DRIFTER", | |
| "Kyu", | |
| 240000L, | |
| Color(0xFF9900FF), | |
| listOf(Color(0xFF0D001A), Color(0xFF5500CC)), | |
| listOf(Color(0xFF9900FF), Color(0xFFFF00AA)), | |
| 3 | |
| ), | |
| Track( | |
| "SYSTEM REBOOT", | |
| "Analog Void", | |
| 205000L, | |
| Color(0xFF00FF66), | |
| listOf(Color(0xFF001A0D), Color(0xFF009944)), | |
| listOf(Color(0xFF00FF66), Color(0xFFCCFF00)), | |
| 4 | |
| ), | |
| Track( | |
| "QUANTUM LEAP", | |
| "Holo", | |
| 175000L, | |
| Color(0xFF6200EA), | |
| listOf(Color(0xFF1A004A), Color(0xFF6200EA)), | |
| listOf(Color(0xFF6200EA), Color(0xFFD500F9)), | |
| 5 | |
| ), | |
| Track( | |
| "NEON TEARS", | |
| "Data Luv", | |
| 215000L, | |
| Color(0xFF4455FF), | |
| listOf(Color(0xFF000511), Color(0xFF1122AA)), | |
| listOf(Color(0xFF4455FF), Color(0xFF00CCFF)), | |
| 0 | |
| ), | |
| Track( | |
| "MAGNETIC", | |
| "Vector 9", | |
| 220000L, | |
| Color(0xFFFF2200), | |
| listOf(Color(0xFF1A0500), Color(0xFFAA0000)), | |
| listOf(Color(0xFFFF2200), Color(0xFFFF007F)), | |
| 1 | |
| ), | |
| Track( | |
| "LUMINOUS", | |
| "Vector 9", | |
| 188000L, | |
| Color(0xFFFFFF00), | |
| listOf(Color(0xFF333300), Color(0xFFCCCC00)), | |
| listOf(Color(0xFFFFFF00), Color(0xFFFF9900)), | |
| 2 | |
| ), | |
| Track( | |
| "SILICON HEART", | |
| "Data Luv", | |
| 210000L, | |
| Color(0xFF00FA9A), | |
| listOf(Color(0xFF002211), Color(0xFF00AA66)), | |
| listOf(Color(0xFF00FA9A), Color(0xFF00E5FF)), | |
| 3 | |
| ), | |
| Track( | |
| "ASTRAL ECHO", | |
| "Kyu", | |
| 235000L, | |
| Color(0xFFB088FF), | |
| listOf(Color(0xFF1A0033), Color(0xFF7744CC)), | |
| listOf(Color(0xFFB088FF), Color(0xFFFF77FF)), | |
| 4 | |
| ), | |
| Track( | |
| "NEURAL LINK", | |
| "Analog Void", | |
| 195000L, | |
| Color(0xFFFF5500), | |
| listOf(Color(0xFF330000), Color(0xFFAA2200)), | |
| listOf(Color(0xFFFF5500), Color(0xFFFFCC00)), | |
| 5 | |
| ), | |
| Track( | |
| "ZERO GRAVITY", | |
| "Holo", | |
| 180000L, | |
| Color(0xFFE0FFFF), | |
| listOf(Color(0xFF002244), Color(0xFF88CCFF)), | |
| listOf(Color(0xFFE0FFFF), Color(0xFF00FFCC)), | |
| 0 | |
| ) | |
| ) | |
| /** | |
| * Utility function to convert milliseconds into a formatted mm:ss string. | |
| */ | |
| fun formatTimeMs(ms: Long): String { | |
| val totalSeconds = ms / 1000 | |
| val m = (totalSeconds / 60).toString().padStart(length = 2, padChar = '0') | |
| val s = (totalSeconds % 60).toString().padStart(length = 2, padChar = '0') | |
| return "$m:$s" | |
| } | |
| // ========================================== | |
| // SHARED COMPONENTS | |
| // ========================================== | |
| /** | |
| * A reusable, canvas-drawn profile avatar designed for SharedElement transitions. | |
| */ | |
| @Composable | |
| fun ProfileAvatar( | |
| themeColor: Color, | |
| showDot: Boolean, | |
| modifier: Modifier = Modifier | |
| ) { | |
| Box( | |
| modifier = modifier | |
| .clip(shape = CircleShape) | |
| .background(color = GlassSurface) | |
| .border(width = 1.dp, color = Color.White.copy(alpha = 0.1f), shape = CircleShape) | |
| ) { | |
| Canvas(modifier = Modifier.fillMaxSize()) { | |
| drawCircle(color = themeColor.copy(alpha = 0.2f), radius = size.width / 2) | |
| drawCircle( | |
| color = themeColor, | |
| radius = size.width / 4, | |
| center = Offset(x = size.width / 2, y = size.height / 3) | |
| ) | |
| drawArc( | |
| color = themeColor.copy(alpha = 0.8f), | |
| startAngle = 140f, | |
| sweepAngle = 260f, | |
| useCenter = false, | |
| topLeft = Offset(x = size.width * 0.15f, y = size.height * 0.45f), | |
| size = Size(width = size.width * 0.7f, height = size.height * 0.7f), | |
| style = Stroke(width = size.width * 0.08f, cap = StrokeCap.Round) | |
| ) | |
| } | |
| if (showDot) { | |
| Box( | |
| modifier = Modifier | |
| .align(alignment = Alignment.TopEnd) | |
| .offset(x = (-2).dp, y = 2.dp) | |
| .size(size = 10.dp) | |
| .clip(shape = CircleShape) | |
| .background(color = themeColor) | |
| .border(width = 2.dp, color = VoidBlack, shape = CircleShape) | |
| .shadow(elevation = 4.dp, spotColor = themeColor) | |
| ) | |
| } | |
| } | |
| } | |
| /** | |
| * The top glassmorphic navigation bar. Provides safe area padding dynamically. | |
| * Explicitly requests Shared scopes to prevent compiler resolution errors. | |
| */ | |
| @OptIn(ExperimentalSharedTransitionApi::class) | |
| @Composable | |
| fun GlassHeader( | |
| sharedTransitionScope: SharedTransitionScope, | |
| animatedVisibilityScope: AnimatedVisibilityScope, | |
| themeColor: Color, | |
| onAvatarClick: () -> Unit, | |
| modifier: Modifier = Modifier | |
| ) { | |
| with(sharedTransitionScope) { | |
| Row( | |
| modifier = modifier | |
| .statusBarsPadding() | |
| .fillMaxWidth() | |
| .padding(horizontal = 24.dp, vertical = 24.dp), | |
| verticalAlignment = Alignment.CenterVertically | |
| ) { | |
| ProfileAvatar( | |
| themeColor = themeColor, | |
| showDot = false, | |
| modifier = Modifier | |
| .sharedElement( | |
| sharedContentState = rememberSharedContentState(key = "profile_avatar"), | |
| animatedVisibilityScope = animatedVisibilityScope, | |
| boundsTransform = { _, _ -> spring(dampingRatio = 0.7f, stiffness = 120f) } | |
| ) | |
| .size(size = 48.dp) | |
| .clickable( | |
| interactionSource = remember { MutableInteractionSource() }, | |
| indication = null, | |
| onClick = onAvatarClick | |
| ) | |
| ) | |
| Spacer(modifier = Modifier.width(width = 16.dp)) | |
| Row( | |
| modifier = Modifier | |
| .weight(weight = 1f) | |
| .height(height = 48.dp) | |
| .clip(shape = RoundedCornerShape(size = 24.dp)) | |
| .background(color = GlassSurface) | |
| .border( | |
| width = 1.dp, | |
| color = Color.White.copy(alpha = 0.05f), | |
| shape = RoundedCornerShape(size = 24.dp) | |
| ) | |
| .padding(horizontal = 16.dp), | |
| verticalAlignment = Alignment.CenterVertically | |
| ) { | |
| Canvas(modifier = Modifier.size(size = 16.dp)) { | |
| drawCircle( | |
| color = Color.White.copy(alpha = 0.6f), | |
| radius = 6.dp.toPx(), | |
| center = Offset(x = 6.dp.toPx(), y = 6.dp.toPx()), | |
| style = Stroke(width = 2.dp.toPx()) | |
| ) | |
| drawLine( | |
| color = Color.White.copy(alpha = 0.6f), | |
| start = Offset(x = 10.dp.toPx(), y = 10.dp.toPx()), | |
| end = Offset(x = 16.dp.toPx(), y = 16.dp.toPx()), | |
| strokeWidth = 2.dp.toPx(), | |
| cap = StrokeCap.Round | |
| ) | |
| } | |
| Spacer(modifier = Modifier.width(width = 12.dp)) | |
| Text( | |
| text = "Search frequencies...", | |
| color = Color.White.copy(alpha = 0.4f), | |
| fontSize = 14.sp | |
| ) | |
| } | |
| Spacer(modifier = Modifier.width(width = 16.dp)) | |
| Box( | |
| modifier = Modifier | |
| .size(size = 48.dp) | |
| .clip(shape = CircleShape) | |
| .background(color = GlassSurface) | |
| .border( | |
| width = 1.dp, | |
| color = Color.White.copy(alpha = 0.05f), | |
| shape = CircleShape | |
| ), | |
| contentAlignment = Alignment.Center | |
| ) { | |
| Canvas(modifier = Modifier.size(size = 20.dp)) { | |
| val w = size.width | |
| val h = size.height | |
| drawLine( | |
| color = Color.White.copy(alpha = 0.8f), | |
| start = Offset(x = 0f, y = h * 0.25f), | |
| end = Offset(x = w, y = h * 0.25f), | |
| strokeWidth = 2.dp.toPx(), | |
| cap = StrokeCap.Round | |
| ) | |
| drawCircle( | |
| color = Color.White, | |
| radius = 3.dp.toPx(), | |
| center = Offset(x = w * 0.3f, y = h * 0.25f) | |
| ) | |
| drawLine( | |
| color = Color.White.copy(alpha = 0.8f), | |
| start = Offset(x = 0f, y = h * 0.75f), | |
| end = Offset(x = w, y = h * 0.75f), | |
| strokeWidth = 2.dp.toPx(), | |
| cap = StrokeCap.Round | |
| ) | |
| drawCircle( | |
| color = Color.White, | |
| radius = 3.dp.toPx(), | |
| center = Offset(x = w * 0.7f, y = h * 0.75f) | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Renders a simple stat item for the profile screen. | |
| */ | |
| @Composable | |
| fun ProfileStat(label: String, value: String, themeColor: Color) { | |
| Column(horizontalAlignment = Alignment.CenterHorizontally) { | |
| Text(text = value, color = Color.White, fontSize = 24.sp, fontWeight = FontWeight.Bold) | |
| Spacer(modifier = Modifier.height(height = 4.dp)) | |
| Text( | |
| text = label, | |
| color = themeColor.copy(alpha = 0.6f), | |
| fontSize = 12.sp, | |
| letterSpacing = 1.sp | |
| ) | |
| } | |
| } | |
| /** | |
| * An animated music visualizer that reacts to the playback state. | |
| */ | |
| @Composable | |
| fun AudioVisualizer(color: Color, isPlaying: Boolean, modifier: Modifier = Modifier) { | |
| Row( | |
| modifier = modifier.height(height = 24.dp), | |
| horizontalArrangement = Arrangement.spacedBy(space = 3.dp), | |
| verticalAlignment = Alignment.Bottom | |
| ) { | |
| val infiniteTransition = rememberInfiniteTransition(label = "eq_transition") | |
| listOf(0.8f, 0.4f, 0.9f, 0.5f).forEachIndexed { index, targetHeight -> | |
| val heightMultiplier by infiniteTransition.animateFloat( | |
| initialValue = 0.2f, | |
| targetValue = targetHeight, | |
| animationSpec = infiniteRepeatable( | |
| animation = tween( | |
| durationMillis = 300 + (index * 120), | |
| easing = FastOutSlowInEasing | |
| ), | |
| repeatMode = RepeatMode.Reverse | |
| ), | |
| label = "bar_$index" | |
| ) | |
| val actualHeight by animateFloatAsState( | |
| targetValue = if (isPlaying) heightMultiplier else 0.15f, | |
| animationSpec = spring(stiffness = Spring.StiffnessMedium), | |
| label = "pause_drop_$index" | |
| ) | |
| Box( | |
| modifier = Modifier | |
| .width(width = 4.dp) | |
| .fillMaxHeight(fraction = actualHeight) | |
| .clip(shape = CircleShape) | |
| .background(color = color) | |
| .shadow(elevation = 8.dp, spotColor = color) | |
| ) | |
| } | |
| } | |
| } | |
| // ========================================== | |
| // SCROLLING TRACKLIST COMPONENT | |
| // ========================================== | |
| @Composable | |
| fun TrackList( | |
| tracks: List<Track>, | |
| currentTrack: Track, | |
| isPlaying: Boolean, | |
| listTopPadding: Dp, | |
| onTrackSelect: (Track) -> Unit, | |
| modifier: Modifier = Modifier | |
| ) { | |
| val listState = rememberLazyListState() | |
| LazyColumn( | |
| state = listState, | |
| modifier = modifier.fillMaxSize(), | |
| contentPadding = PaddingValues(top = listTopPadding, bottom = 140.dp), | |
| verticalArrangement = Arrangement.spacedBy(space = 4.dp) | |
| ) { | |
| itemsIndexed( | |
| items = tracks, | |
| key = { _, track -> track.title } | |
| ) { index, track -> | |
| val isSelected = track == currentTrack | |
| val interactionSource = remember { MutableInteractionSource() } | |
| val transition = updateTransition(targetState = isSelected, label = "track_selection") | |
| val layoutSpringDp = spring<Dp>(dampingRatio = 0.7f, stiffness = 250f) | |
| val visualSpring = spring<Float>(dampingRatio = 0.6f, stiffness = 200f) | |
| val visualSpringDp = spring<Dp>(dampingRatio = 0.6f, stiffness = 200f) | |
| val fadeTween = tween<Float>(durationMillis = 300, easing = FastOutSlowInEasing) | |
| val cardHeight by transition.animateDp( | |
| transitionSpec = { layoutSpringDp }, | |
| label = "height" | |
| ) { if (it) 100.dp else 64.dp } | |
| val cardScale by transition.animateFloat( | |
| transitionSpec = { visualSpring }, | |
| label = "scale" | |
| ) { if (it) 1.0f else 0.95f } | |
| val cardAlpha by transition.animateFloat( | |
| transitionSpec = { fadeTween }, | |
| label = "alpha" | |
| ) { if (it) 1f else 0.4f } | |
| val glassOpacity by transition.animateFloat( | |
| transitionSpec = { fadeTween }, | |
| label = "glass" | |
| ) { if (it) 1f else 0f } | |
| val numScale by transition.animateFloat( | |
| transitionSpec = { visualSpring }, | |
| label = "num_scale" | |
| ) { if (it) 5f else 1f } | |
| val numOffsetX by transition.animateDp( | |
| transitionSpec = { visualSpringDp }, | |
| label = "num_x" | |
| ) { 16.dp } | |
| val numAlpha by transition.animateFloat( | |
| transitionSpec = { fadeTween }, | |
| label = "num_alpha" | |
| ) { if (it) 0.15f else 0.4f } | |
| val textOffsetX by transition.animateDp( | |
| transitionSpec = { layoutSpringDp }, | |
| label = "text_x" | |
| ) { if (it) 124.dp else 44.dp } | |
| Box( | |
| modifier = Modifier | |
| .animateItem() | |
| .fillMaxWidth() | |
| .padding(horizontal = 24.dp) | |
| .height(height = cardHeight.coerceAtLeast(minimumValue = 0.dp)) | |
| .graphicsLayer { | |
| scaleX = cardScale | |
| scaleY = cardScale | |
| alpha = cardAlpha | |
| } | |
| .background( | |
| color = GlassSurface.copy(alpha = GlassSurface.alpha * glassOpacity), | |
| shape = RoundedCornerShape(size = 24.dp) | |
| ) | |
| .border( | |
| width = 1.dp, | |
| color = track.primaryColor.copy(alpha = 0.3f * glassOpacity), | |
| shape = RoundedCornerShape(size = 24.dp) | |
| ) | |
| .clickable( | |
| interactionSource = interactionSource, | |
| indication = null, | |
| onClick = { onTrackSelect(track) } | |
| ) | |
| .padding(horizontal = 20.dp) | |
| ) { | |
| // Morphing Track Number | |
| Box( | |
| modifier = Modifier | |
| .align(alignment = Alignment.CenterStart) | |
| .offset(x = numOffsetX) | |
| ) { | |
| Text( | |
| text = "${index + 1}".padStart(length = 2, padChar = '0'), | |
| color = if (isSelected) track.primaryColor else Color.White, | |
| fontSize = 14.sp, | |
| fontWeight = FontWeight.Black, | |
| fontFamily = FontFamily.Monospace, | |
| modifier = Modifier | |
| .requiredSize(size = 0.dp) | |
| .wrapContentSize(unbounded = true, align = Alignment.CenterStart) | |
| .graphicsLayer { | |
| scaleX = numScale | |
| scaleY = numScale | |
| alpha = numAlpha | |
| transformOrigin = | |
| TransformOrigin(pivotFractionX = 0f, pivotFractionY = 0.5f) | |
| } | |
| ) | |
| } | |
| // Left Neon Activity Indicator | |
| val indicatorHeight by transition.animateDp( | |
| transitionSpec = { layoutSpringDp }, | |
| label = "ind_h" | |
| ) { if (it) 48.dp else 12.dp } | |
| Box( | |
| modifier = Modifier | |
| .align(alignment = Alignment.CenterStart) | |
| .width(width = 4.dp) | |
| .height(height = indicatorHeight.coerceAtLeast(minimumValue = 0.dp)) | |
| .clip(shape = CircleShape) | |
| .background( | |
| color = if (isSelected) track.primaryColor else Color.White.copy( | |
| alpha = 0.3f | |
| ) | |
| ) | |
| ) | |
| // Track Title and Artist | |
| Column( | |
| modifier = Modifier | |
| .align(alignment = Alignment.CenterStart) | |
| .offset(x = textOffsetX), | |
| horizontalAlignment = Alignment.Start | |
| ) { | |
| Text( | |
| text = track.title, | |
| color = Color.White, | |
| fontSize = 18.sp, | |
| fontWeight = if (isSelected) FontWeight.Black else FontWeight.SemiBold | |
| ) | |
| Text( | |
| text = track.artist, | |
| color = if (isSelected) track.primaryColor else Color.White.copy(alpha = 0.6f), | |
| fontSize = 13.sp, | |
| fontWeight = FontWeight.Medium | |
| ) | |
| } | |
| // Playback Visualizer / Duration | |
| Box(modifier = Modifier.align(alignment = Alignment.CenterEnd)) { | |
| Crossfade( | |
| targetState = isSelected, | |
| animationSpec = tween(durationMillis = 300), | |
| label = "eq_fade" | |
| ) { selected -> | |
| if (selected) { | |
| AudioVisualizer(color = track.primaryColor, isPlaying = isPlaying) | |
| } else { | |
| Text( | |
| text = formatTimeMs(ms = track.durationMs), | |
| color = Color.White.copy(alpha = 0.4f), | |
| fontSize = 12.sp, | |
| fontFamily = FontFamily.Monospace | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // ========================================== | |
| // BESPOKE TRACK LOGOS & VINYL GRAPHICS | |
| // ========================================== | |
| /** | |
| * Renders a specific geometric logo based on the provided style integer. | |
| */ | |
| @Composable | |
| fun TrackLogo(style: Int, modifier: Modifier = Modifier, isMini: Boolean = false) { | |
| Canvas(modifier = modifier) { | |
| val foilBrush = Brush.linearGradient( | |
| colors = listOf( | |
| Color(0xFFFFD700).copy(alpha = 0.8f), | |
| Color(0xFFFFA500).copy(alpha = 0.9f), | |
| Color(0xFFFF8C00).copy(alpha = 0.6f) | |
| ), | |
| start = Offset(x = 0f, y = 0f), | |
| end = Offset(x = size.width, y = size.height) | |
| ) | |
| val center = Offset(x = size.width / 2, y = size.height / 2) | |
| val radius = size.width / 2 | |
| if (!isMini) { | |
| drawCircle( | |
| brush = foilBrush, | |
| radius = radius * 0.95f, | |
| center = center, | |
| style = Stroke( | |
| width = 2f, | |
| pathEffect = PathEffect.dashPathEffect( | |
| intervals = floatArrayOf(8f, 12f), | |
| phase = 0f | |
| ) | |
| ) | |
| ) | |
| } | |
| drawCircle( | |
| brush = foilBrush, | |
| radius = radius * 0.85f, | |
| center = center, | |
| style = Stroke(width = if (isMini) 2f else 4f) | |
| ) | |
| val path = Path() | |
| val w = size.width | |
| val h = size.height | |
| when (style % 6) { | |
| 0 -> { | |
| path.moveTo(w * 0.5f, h * 0.25f); path.lineTo(w * 0.25f, h * 0.75f) | |
| path.moveTo(w * 0.5f, h * 0.25f); path.lineTo(w * 0.75f, h * 0.75f) | |
| path.moveTo(w * 0.35f, h * 0.55f); path.lineTo(w * 0.65f, h * 0.55f) | |
| path.moveTo(w * 0.15f, h * 0.35f); path.lineTo( | |
| w * 0.5f, | |
| h * 0.85f | |
| ); path.lineTo(w * 0.85f, h * 0.35f) | |
| } | |
| 1 -> { | |
| path.moveTo(w * 0.5f, h * 0.25f); path.lineTo(w * 0.25f, h * 0.75f) | |
| path.lineTo(w * 0.75f, h * 0.75f); path.close() | |
| path.moveTo(w * 0.5f, h * 0.4f); path.lineTo(w * 0.5f, h * 0.75f) | |
| } | |
| 2 -> { | |
| path.moveTo(w * 0.5f, h * 0.25f); path.lineTo(w * 0.7f, h * 0.35f) | |
| path.lineTo(w * 0.7f, h * 0.65f); path.lineTo(w * 0.5f, h * 0.75f) | |
| path.lineTo(w * 0.3f, h * 0.65f); path.lineTo(w * 0.3f, h * 0.35f); path.close() | |
| path.moveTo(w * 0.5f, h * 0.5f); path.lineTo(w * 0.5f, h * 0.75f) | |
| path.moveTo(w * 0.5f, h * 0.5f); path.lineTo(w * 0.3f, h * 0.35f) | |
| path.moveTo(w * 0.5f, h * 0.5f); path.lineTo(w * 0.7f, h * 0.35f) | |
| } | |
| 3 -> { | |
| path.moveTo(w * 0.5f, h * 0.2f); path.lineTo(w * 0.8f, h * 0.5f) | |
| path.lineTo(w * 0.5f, h * 0.8f); path.lineTo(w * 0.2f, h * 0.5f); path.close() | |
| path.moveTo(w * 0.35f, h * 0.35f); path.lineTo(w * 0.65f, h * 0.65f) | |
| path.moveTo(w * 0.65f, h * 0.35f); path.lineTo(w * 0.35f, h * 0.65f) | |
| } | |
| 4 -> { | |
| path.moveTo(w * 0.65f, h * 0.25f); path.lineTo(w * 0.35f, h * 0.25f) | |
| path.lineTo(w * 0.65f, h * 0.5f); path.lineTo(w * 0.35f, h * 0.5f) | |
| path.lineTo(w * 0.65f, h * 0.75f); path.lineTo(w * 0.35f, h * 0.75f) | |
| } | |
| 5 -> { | |
| path.moveTo(w * 0.3f, h * 0.25f); path.lineTo(w * 0.7f, h * 0.25f) | |
| path.lineTo(w * 0.3f, h * 0.75f); path.lineTo(w * 0.7f, h * 0.75f); path.close() | |
| } | |
| } | |
| drawPath( | |
| path = path, | |
| brush = foilBrush, | |
| style = Stroke( | |
| width = if (isMini) 3f else 6f, | |
| cap = StrokeCap.Round, | |
| join = StrokeJoin.Round | |
| ) | |
| ) | |
| } | |
| } | |
| /** | |
| * The physical representation of the vinyl record that rotates continuously during playback. | |
| */ | |
| @Composable | |
| fun VinylRecord( | |
| isPlaying: Boolean, | |
| themeColor: Color, | |
| stampGradient: List<Color>, | |
| modifier: Modifier = Modifier | |
| ) { | |
| val motorSpeed by animateFloatAsState( | |
| targetValue = if (isPlaying) 1f else 0f, | |
| animationSpec = spring(dampingRatio = 0.8f, stiffness = 40f), | |
| label = "motor_speed" | |
| ) | |
| val motorEngagementScale by animateFloatAsState( | |
| targetValue = if (isPlaying) 1f else 0.96f, | |
| animationSpec = spring(dampingRatio = 0.5f, stiffness = 200f), | |
| label = "motor_bump" | |
| ) | |
| var accumulatedRotation by remember { mutableFloatStateOf(value = 0f) } | |
| LaunchedEffect(Unit) { | |
| var lastFrame = withFrameMillis { it } | |
| while (true) { | |
| val currentFrame = withFrameMillis { it } | |
| accumulatedRotation += ((currentFrame - lastFrame) * 0.09f) * motorSpeed | |
| lastFrame = currentFrame | |
| } | |
| } | |
| Canvas( | |
| modifier = modifier | |
| .fillMaxSize() | |
| .scale(scale = motorEngagementScale) | |
| .graphicsLayer { rotationZ = accumulatedRotation } // Bypasses recomposition | |
| .shadow( | |
| elevation = if (isPlaying) 20.dp else 8.dp, | |
| shape = CircleShape, | |
| spotColor = Color.Black | |
| ) | |
| .clip(shape = CircleShape) | |
| .background(color = Color(0xFF111113)) | |
| ) { | |
| val center = Offset(x = size.width / 2, y = size.height / 2) | |
| val radius = size.width / 2 | |
| // Grooves | |
| for (i in 1..12) { | |
| drawCircle( | |
| color = Color.White.copy(alpha = 0.08f), | |
| radius = radius - (i * 10f), | |
| center = center, | |
| style = Stroke(width = 1.5f) | |
| ) | |
| } | |
| // Holographic Resin Sheen | |
| drawRect( | |
| brush = Brush.sweepGradient( | |
| colors = listOf( | |
| Color.Transparent, | |
| themeColor.copy(alpha = 0.4f), | |
| Color.Transparent, | |
| themeColor.copy(alpha = 0.4f), | |
| Color.Transparent | |
| ), | |
| center = center | |
| ), | |
| blendMode = BlendMode.Screen | |
| ) | |
| // Center Stamp | |
| drawCircle( | |
| brush = Brush.linearGradient(colors = stampGradient), | |
| radius = radius * 0.35f, | |
| center = center | |
| ) | |
| // Spindle Hole | |
| drawCircle(color = VoidBlack, radius = radius * 0.06f, center = center) | |
| } | |
| } | |
| // ========================================== | |
| // MEDIA CONTROLS | |
| // ========================================== | |
| enum class ControlAction { PREV, PLAY_PAUSE, NEXT } | |
| @Composable | |
| fun ControlButton( | |
| action: ControlAction, | |
| gradient: List<Color>, | |
| isPlaying: Boolean, | |
| buttonSize: Dp, | |
| onClick: () -> Unit | |
| ) { | |
| val interactionSource = remember { MutableInteractionSource() } | |
| val isPressed by interactionSource.collectIsPressedAsState() | |
| val scale by animateFloatAsState( | |
| targetValue = if (isPressed) 0.85f else 1f, | |
| animationSpec = spring(dampingRatio = 0.6f), | |
| label = "btn_scale" | |
| ) | |
| val isPrimary = action == ControlAction.PLAY_PAUSE | |
| val controlBrush = Brush.linearGradient(colors = gradient) | |
| Box( | |
| modifier = Modifier | |
| .size(size = buttonSize) | |
| .scale(scale = scale) | |
| .clip(shape = CircleShape) | |
| .background(color = if (isPrimary) GlassSurface else Color.Transparent) | |
| .border( | |
| width = if (isPrimary) 2.dp else 0.dp, | |
| brush = Brush.linearGradient(colors = gradient.map { it.copy(alpha = 0.5f) }), | |
| shape = CircleShape | |
| ) | |
| .clickable( | |
| interactionSource = interactionSource, | |
| indication = null, | |
| onClick = onClick | |
| ), | |
| contentAlignment = Alignment.Center | |
| ) { | |
| Canvas(modifier = Modifier.size(size = buttonSize * 0.4f)) { | |
| val w = this.size.width | |
| val h = this.size.height | |
| when (action) { | |
| ControlAction.PLAY_PAUSE -> { | |
| if (isPlaying) { | |
| drawRoundRect( | |
| brush = controlBrush, | |
| topLeft = Offset(x = w * 0.15f, y = 0f), | |
| size = Size(width = w * 0.25f, height = h), | |
| cornerRadius = CornerRadius(x = 4f, y = 4f) | |
| ) | |
| drawRoundRect( | |
| brush = controlBrush, | |
| topLeft = Offset(x = w * 0.6f, y = 0f), | |
| size = Size(width = w * 0.25f, height = h), | |
| cornerRadius = CornerRadius(x = 4f, y = 4f) | |
| ) | |
| } else { | |
| val path = Path().apply { | |
| moveTo(w * 0.2f, 0f); lineTo( | |
| w * 0.9f, | |
| h / 2 | |
| ); lineTo(w * 0.2f, h); close() | |
| } | |
| drawPath(path = path, brush = controlBrush) | |
| } | |
| } | |
| ControlAction.NEXT -> { | |
| val path = Path().apply { | |
| moveTo(w * 0.1f, 0f); lineTo( | |
| w * 0.7f, | |
| h / 2 | |
| ); lineTo(w * 0.1f, h); close() | |
| } | |
| drawPath(path = path, brush = controlBrush) | |
| drawRoundRect( | |
| brush = controlBrush, | |
| topLeft = Offset(x = w * 0.75f, y = 0f), | |
| size = Size(width = w * 0.15f, height = h), | |
| cornerRadius = CornerRadius(x = 2f, y = 2f) | |
| ) | |
| } | |
| ControlAction.PREV -> { | |
| drawRoundRect( | |
| brush = controlBrush, | |
| topLeft = Offset(x = w * 0.1f, y = 0f), | |
| size = Size(width = w * 0.15f, height = h), | |
| cornerRadius = CornerRadius(x = 2f, y = 2f) | |
| ) | |
| val path = Path().apply { | |
| moveTo(w * 0.9f, 0f); lineTo( | |
| w * 0.3f, | |
| h / 2 | |
| ); lineTo(w * 0.9f, h); close() | |
| } | |
| drawPath(path = path, brush = controlBrush) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| @Composable | |
| fun PlayerControls( | |
| track: Track, | |
| isPlaying: Boolean, | |
| isVisible: Boolean, | |
| onTogglePlay: () -> Unit, | |
| modifier: Modifier = Modifier | |
| ) { | |
| var currentTimeMs by remember { mutableLongStateOf(value = 0L) } | |
| LaunchedEffect(track) { currentTimeMs = 0L } | |
| LaunchedEffect(isPlaying) { | |
| if (isPlaying) { | |
| var lastFrame = withFrameMillis { it } | |
| while (currentTimeMs < track.durationMs) { | |
| val currentFrame = withFrameMillis { it } | |
| currentTimeMs += (currentFrame - lastFrame) | |
| lastFrame = currentFrame | |
| } | |
| } | |
| } | |
| val progress = (currentTimeMs.toFloat() / track.durationMs.toFloat()).coerceIn( | |
| minimumValue = 0f, | |
| maximumValue = 1f | |
| ) | |
| var showBar by remember { mutableStateOf(value = false) } | |
| var showPrev by remember { mutableStateOf(value = false) } | |
| var showPlay by remember { mutableStateOf(value = false) } | |
| var showNext by remember { mutableStateOf(value = false) } | |
| // Cascading entry animation logic | |
| LaunchedEffect(isVisible) { | |
| if (isVisible) { | |
| showBar = true; delay(timeMillis = 60) | |
| showPrev = true; delay(timeMillis = 60) | |
| showPlay = true; delay(timeMillis = 60) | |
| showNext = true | |
| } else { | |
| showBar = false; showPrev = false; showPlay = false; showNext = false | |
| } | |
| } | |
| val playfulSpring = spring<Float>(dampingRatio = 0.45f, stiffness = 250f) | |
| val fastCollapse = tween<Float>(durationMillis = 150, easing = FastOutLinearInEasing) | |
| val barScaleX by animateFloatAsState( | |
| targetValue = if (showBar) 1f else 0f, | |
| animationSpec = if (showBar) playfulSpring else fastCollapse, | |
| label = "bar_scale" | |
| ) | |
| val barAlpha by animateFloatAsState( | |
| targetValue = if (showBar) 1f else 0f, | |
| animationSpec = tween(durationMillis = 200), | |
| label = "bar_alpha" | |
| ) | |
| val prevScale by animateFloatAsState( | |
| targetValue = if (showPrev) 1f else 0f, | |
| animationSpec = if (showPrev) playfulSpring else fastCollapse, | |
| label = "prev_scale" | |
| ) | |
| val playScale by animateFloatAsState( | |
| targetValue = if (showPlay) 1f else 0f, | |
| animationSpec = if (showPlay) playfulSpring else fastCollapse, | |
| label = "play_scale" | |
| ) | |
| val nextScale by animateFloatAsState( | |
| targetValue = if (showNext) 1f else 0f, | |
| animationSpec = if (showNext) playfulSpring else fastCollapse, | |
| label = "next_scale" | |
| ) | |
| val accentColor1 by animateColorAsState( | |
| targetValue = track.accentGradient[0], | |
| animationSpec = tween(durationMillis = 800), | |
| label = "acc_color1" | |
| ) | |
| val accentColor2 by animateColorAsState( | |
| targetValue = track.accentGradient[1], | |
| animationSpec = tween(durationMillis = 800), | |
| label = "acc_color2" | |
| ) | |
| val animatedAccentGradient = listOf(accentColor1, accentColor2) | |
| Column( | |
| modifier = modifier | |
| .fillMaxWidth() | |
| .padding(horizontal = 40.dp) | |
| ) { | |
| Row( | |
| verticalAlignment = Alignment.CenterVertically, | |
| horizontalArrangement = Arrangement.SpaceBetween, | |
| modifier = Modifier | |
| .fillMaxWidth() | |
| .graphicsLayer { alpha = barAlpha } | |
| ) { | |
| Text( | |
| text = formatTimeMs(ms = currentTimeMs), | |
| color = Color.White.copy(alpha = 0.8f), | |
| fontSize = 12.sp, | |
| fontFamily = FontFamily.Monospace | |
| ) | |
| Spacer(modifier = Modifier.width(width = 16.dp)) | |
| Canvas( | |
| modifier = Modifier | |
| .weight(weight = 1f) | |
| .height(height = 4.dp) | |
| .graphicsLayer { scaleX = barScaleX }) { | |
| drawRoundRect( | |
| color = Color.White.copy(alpha = 0.1f), | |
| cornerRadius = CornerRadius(x = 2.dp.toPx(), y = 2.dp.toPx()) | |
| ) | |
| drawRoundRect( | |
| brush = Brush.horizontalGradient(colors = animatedAccentGradient), | |
| size = Size(width = size.width * progress, height = size.height), | |
| cornerRadius = CornerRadius(x = 2.dp.toPx(), y = 2.dp.toPx()) | |
| ) | |
| drawCircle( | |
| color = Color.White, | |
| radius = 6.dp.toPx(), | |
| center = Offset(x = size.width * progress, y = size.height / 2), | |
| blendMode = BlendMode.Screen | |
| ) | |
| } | |
| Spacer(modifier = Modifier.width(width = 16.dp)) | |
| Text( | |
| text = "-" + formatTimeMs(ms = track.durationMs - currentTimeMs), | |
| color = Color.White.copy(alpha = 0.6f), | |
| fontSize = 12.sp, | |
| fontFamily = FontFamily.Monospace | |
| ) | |
| } | |
| Spacer(modifier = Modifier.height(height = 40.dp)) | |
| Row( | |
| modifier = Modifier.fillMaxWidth(), | |
| horizontalArrangement = Arrangement.SpaceEvenly, | |
| verticalAlignment = Alignment.CenterVertically | |
| ) { | |
| Box(modifier = Modifier.graphicsLayer { | |
| scaleX = prevScale; scaleY = prevScale; alpha = | |
| prevScale.coerceIn(minimumValue = 0f, maximumValue = 1f) | |
| }) { | |
| ControlButton( | |
| action = ControlAction.PREV, | |
| gradient = animatedAccentGradient, | |
| isPlaying = isPlaying, | |
| buttonSize = 56.dp, | |
| onClick = {}) | |
| } | |
| Box(modifier = Modifier.graphicsLayer { | |
| scaleX = playScale; scaleY = playScale; alpha = | |
| playScale.coerceIn(minimumValue = 0f, maximumValue = 1f) | |
| }) { | |
| ControlButton( | |
| action = ControlAction.PLAY_PAUSE, | |
| gradient = animatedAccentGradient, | |
| isPlaying = isPlaying, | |
| buttonSize = 80.dp, | |
| onClick = onTogglePlay | |
| ) | |
| } | |
| Box(modifier = Modifier.graphicsLayer { | |
| scaleX = nextScale; scaleY = nextScale; alpha = | |
| nextScale.coerceIn(minimumValue = 0f, maximumValue = 1f) | |
| }) { | |
| ControlButton( | |
| action = ControlAction.NEXT, | |
| gradient = animatedAccentGradient, | |
| isPlaying = isPlaying, | |
| buttonSize = 56.dp, | |
| onClick = {}) | |
| } | |
| } | |
| } | |
| } | |
| // ========================================== | |
| // CORE APPLICATION CONTAINER | |
| // ========================================== | |
| @OptIn(ExperimentalSharedTransitionApi::class) | |
| @Composable | |
| fun MusicPlayer() { | |
| val view = LocalView.current | |
| if (!view.isInEditMode) { | |
| SideEffect { | |
| val window = (view.context as Activity).window | |
| WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = false | |
| } | |
| } | |
| val coroutineScope = rememberCoroutineScope() | |
| var currentScreen by remember { mutableStateOf(value = AppScreen.LIST) } | |
| var stage by remember { mutableIntStateOf(value = 0) } | |
| var isAnimating by remember { mutableStateOf(value = false) } | |
| var isPlaying by remember { mutableStateOf(value = true) } | |
| var currentTrack by remember { mutableStateOf(value = Playlist.first()) } | |
| // Dynamic Color Engine | |
| val ambientThemeColor by animateColorAsState( | |
| targetValue = currentTrack.primaryColor, | |
| animationSpec = tween(durationMillis = 1200, easing = FastOutSlowInEasing), | |
| label = "ambient_theme" | |
| ) | |
| val sleeveColor1 by animateColorAsState( | |
| targetValue = currentTrack.primaryGradient[0], | |
| animationSpec = tween(durationMillis = 1200), | |
| label = "sleeve_color1" | |
| ) | |
| val sleeveColor2 by animateColorAsState( | |
| targetValue = currentTrack.primaryGradient[1], | |
| animationSpec = tween(durationMillis = 1200), | |
| label = "sleeve_color2" | |
| ) | |
| val animatedSleeveGradient = listOf(sleeveColor1, sleeveColor2) | |
| // Spatial Morphing Configurations | |
| val spatialGlide = spring<Rect>(dampingRatio = 0.8f, stiffness = 80f) | |
| val physicsSlide = spring<Float>(dampingRatio = 0.75f, stiffness = 90f) | |
| // Hoisted Border Fill Animation (Animates ONLY when currentTrack changes) | |
| val borderFillProgress = remember { Animatable(initialValue = 1f) } | |
| LaunchedEffect(currentTrack) { | |
| borderFillProgress.snapTo(targetValue = 0f) | |
| borderFillProgress.animateTo( | |
| targetValue = 1f, | |
| animationSpec = tween(durationMillis = 1000, easing = FastOutSlowInEasing) | |
| ) | |
| } | |
| fun playOpeningChoreography() { | |
| if (isAnimating) return | |
| coroutineScope.launch { | |
| isAnimating = true | |
| currentScreen = AppScreen.PLAYER | |
| stage = 1; delay(timeMillis = 550) | |
| stage = 2; delay(timeMillis = 600) | |
| stage = 3; isAnimating = false | |
| } | |
| } | |
| fun playClosingChoreography() { | |
| if (isAnimating) return | |
| coroutineScope.launch { | |
| isAnimating = true | |
| stage = 2; delay(timeMillis = 550) | |
| stage = 1; delay(timeMillis = 500) | |
| stage = 0; currentScreen = AppScreen.LIST | |
| delay(timeMillis = 550); isAnimating = false | |
| } | |
| } | |
| Box( | |
| modifier = Modifier | |
| .fillMaxSize() | |
| .background(color = VoidBlack) | |
| ) { | |
| val bgAlpha by animateFloatAsState( | |
| targetValue = if (currentScreen == AppScreen.PLAYER) 1f else 0f, | |
| animationSpec = tween(durationMillis = 500), | |
| label = "bg_alpha" | |
| ) | |
| // Universal Ambient Background Layer | |
| Box( | |
| modifier = Modifier | |
| .fillMaxSize() | |
| .graphicsLayer { alpha = bgAlpha } | |
| .background( | |
| brush = Brush.verticalGradient( | |
| colors = listOf( | |
| ambientThemeColor.copy( | |
| alpha = 0.2f | |
| ), VoidBlack, VoidBlack | |
| ) | |
| ) | |
| ) | |
| .blur(radius = 80.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded) | |
| ) | |
| SharedTransitionLayout { | |
| val sharedTransitionScope = this | |
| AnimatedContent( | |
| targetState = currentScreen, | |
| transitionSpec = { | |
| fadeIn(animationSpec = tween(durationMillis = 400)) togetherWith fadeOut( | |
| animationSpec = tween(durationMillis = 400) | |
| ) | |
| }, | |
| label = "screen_nav" | |
| ) { targetScreen -> | |
| val animatedVisibilityScope = this | |
| when (targetScreen) { | |
| AppScreen.LIST -> { | |
| val neonGlowAlpha by animateFloatAsState( | |
| targetValue = if (isPlaying) 1f else 0.3f, | |
| animationSpec = tween(durationMillis = 600), | |
| label = "neon_glow" | |
| ) | |
| Box(modifier = Modifier.fillMaxSize()) { | |
| val density = LocalDensity.current | |
| val topInsets = | |
| WindowInsets.statusBars.asPaddingValues().calculateTopPadding() | |
| // Dynamic Math for exact 12.dp resting gap and perfect clipping bounds | |
| val headerHeightDp = topInsets + 96.dp | |
| val topFadeStartDp = headerHeightDp - 40.dp | |
| val topFadeEndDp = headerHeightDp | |
| val listTopPaddingDp = headerHeightDp + 12.dp | |
| Box( | |
| modifier = Modifier | |
| .fillMaxSize() | |
| .graphicsLayer { | |
| compositingStrategy = CompositingStrategy.Offscreen | |
| } | |
| .drawWithContent { | |
| drawContent() | |
| val topFadeStart = density.run { topFadeStartDp.toPx() } | |
| val topFadeEnd = density.run { topFadeEndDp.toPx() } | |
| val bottomFadeStart = | |
| size.height - density.run { 160.dp.toPx() } | |
| val bottomFadeEnd = | |
| size.height - density.run { 104.dp.toPx() } | |
| // Hardware alpha mask protecting the header and mini-player | |
| drawRect( | |
| brush = Brush.verticalGradient( | |
| colorStops = arrayOf( | |
| 0f to Color.Transparent, | |
| (topFadeStart / size.height).coerceIn( | |
| minimumValue = 0f, | |
| maximumValue = 1f | |
| ) to Color.Transparent, | |
| (topFadeEnd / size.height).coerceIn( | |
| minimumValue = 0f, | |
| maximumValue = 1f | |
| ) to Color.Black, | |
| (bottomFadeStart / size.height).coerceIn( | |
| minimumValue = 0f, | |
| maximumValue = 1f | |
| ) to Color.Black, | |
| (bottomFadeEnd / size.height).coerceIn( | |
| minimumValue = 0f, | |
| maximumValue = 1f | |
| ) to Color.Transparent, | |
| 1f to Color.Transparent | |
| ) | |
| ), | |
| blendMode = BlendMode.DstIn | |
| ) | |
| } | |
| ) { | |
| TrackList( | |
| tracks = Playlist, | |
| currentTrack = currentTrack, | |
| isPlaying = isPlaying, | |
| listTopPadding = listTopPaddingDp, | |
| onTrackSelect = { track -> | |
| currentTrack = track | |
| isPlaying = true | |
| } | |
| ) | |
| } | |
| // 2. The Transparent Sticky Header with explicit Scope Context | |
| Box( | |
| modifier = Modifier | |
| .align(alignment = Alignment.TopCenter) | |
| .fillMaxWidth() | |
| ) { | |
| GlassHeader( | |
| sharedTransitionScope = sharedTransitionScope, | |
| animatedVisibilityScope = animatedVisibilityScope, | |
| themeColor = ambientThemeColor, | |
| onAvatarClick = { currentScreen = AppScreen.PROFILE } | |
| ) | |
| } | |
| // Calculate the border fill from the hoisted state | |
| val bp = borderFillProgress.value.coerceIn( | |
| minimumValue = 0f, | |
| maximumValue = 1f | |
| ) | |
| val nextBp = | |
| (bp + 0.001f).coerceIn(minimumValue = 0f, maximumValue = 1f) | |
| val dynamicBorderBrush = if (bp >= 0.999f) { | |
| SolidColor(value = ambientThemeColor.copy(alpha = neonGlowAlpha * 0.5f)) | |
| } else { | |
| Brush.horizontalGradient( | |
| colorStops = arrayOf( | |
| 0f to ambientThemeColor.copy(alpha = neonGlowAlpha * 0.5f), | |
| bp to ambientThemeColor.copy(alpha = neonGlowAlpha * 0.5f), | |
| nextBp to Color.Transparent, | |
| 1f to Color.Transparent | |
| ) | |
| ) | |
| } | |
| // 3. The Solid Black Mini-Player | |
| Row( | |
| modifier = Modifier | |
| .align(alignment = Alignment.BottomCenter) | |
| .padding(bottom = 32.dp, start = 16.dp, end = 16.dp) | |
| .fillMaxWidth() | |
| .height(height = 72.dp) | |
| .background( | |
| color = Color.Black, | |
| shape = RoundedCornerShape(size = 20.dp) | |
| ) | |
| .background( | |
| color = GlassSurface, | |
| shape = RoundedCornerShape(size = 20.dp) | |
| ) | |
| .border( | |
| width = 1.dp, | |
| brush = dynamicBorderBrush, | |
| shape = RoundedCornerShape(size = 20.dp) | |
| ) | |
| .clickable( | |
| interactionSource = remember { MutableInteractionSource() }, | |
| indication = null, | |
| onClick = { playOpeningChoreography() } | |
| ) | |
| .padding(all = 8.dp), | |
| verticalAlignment = Alignment.CenterVertically | |
| ) { | |
| with(sharedTransitionScope) { | |
| Box( | |
| modifier = Modifier | |
| .sharedElement( | |
| sharedContentState = rememberSharedContentState(key = "album_sleeve"), | |
| animatedVisibilityScope = animatedVisibilityScope, | |
| boundsTransform = { _, _ -> spatialGlide } | |
| ) | |
| .size(size = 56.dp) | |
| .clip(shape = RoundedCornerShape(size = 8.dp)) | |
| .background(brush = Brush.linearGradient(colors = animatedSleeveGradient)), | |
| contentAlignment = Alignment.Center | |
| ) { | |
| TrackLogo( | |
| style = currentTrack.logoStyle, | |
| modifier = Modifier.size(size = 28.dp), | |
| isMini = true | |
| ) | |
| } | |
| } | |
| Spacer(modifier = Modifier.width(width = 16.dp)) | |
| Column(modifier = Modifier.weight(weight = 1f)) { | |
| Text( | |
| text = currentTrack.title, | |
| color = Color.White, | |
| fontWeight = FontWeight.Bold | |
| ) | |
| Text( | |
| text = currentTrack.artist, | |
| color = Color.White.copy(alpha = 0.6f), | |
| fontSize = 12.sp | |
| ) | |
| } | |
| val accentColor1 by animateColorAsState( | |
| targetValue = currentTrack.accentGradient[0], | |
| animationSpec = tween(durationMillis = 800), | |
| label = "mini_acc1" | |
| ) | |
| val accentColor2 by animateColorAsState( | |
| targetValue = currentTrack.accentGradient[1], | |
| animationSpec = tween(durationMillis = 800), | |
| label = "mini_acc2" | |
| ) | |
| ControlButton( | |
| action = ControlAction.PLAY_PAUSE, | |
| gradient = listOf(accentColor1, accentColor2), | |
| isPlaying = isPlaying, | |
| buttonSize = 48.dp, | |
| onClick = { isPlaying = !isPlaying }) | |
| } | |
| } | |
| } | |
| AppScreen.PLAYER -> { | |
| Column( | |
| modifier = Modifier | |
| .fillMaxSize() | |
| .padding(top = 100.dp), | |
| horizontalAlignment = Alignment.CenterHorizontally | |
| ) { | |
| Box( | |
| modifier = Modifier | |
| .fillMaxWidth() | |
| .height(height = 340.dp), | |
| contentAlignment = Alignment.Center | |
| ) { | |
| val slideDistancePx = with(LocalDensity.current) { 160.dp.toPx() } | |
| val vinylOffsetX by animateFloatAsState( | |
| targetValue = when (stage) { | |
| 1 -> 0f; 2 -> slideDistancePx; 3 -> 0f; else -> 0f | |
| }, animationSpec = physicsSlide, label = "vinyl_slide" | |
| ) | |
| val vinylAlpha by animateFloatAsState( | |
| targetValue = if (stage >= 1) 1f else 0f, | |
| animationSpec = if (stage == 0) snap() else tween( | |
| durationMillis = 150, | |
| delayMillis = 400 | |
| ), | |
| label = "vinyl_alpha" | |
| ) | |
| val sleeveAlpha by animateFloatAsState( | |
| targetValue = if (stage == 3) 0f else 1f, | |
| animationSpec = tween(durationMillis = 500), | |
| label = "sleeve_alpha" | |
| ) | |
| val sleeveScale by animateFloatAsState( | |
| targetValue = if (stage == 3) 0.85f else 1f, | |
| animationSpec = physicsSlide, | |
| label = "sleeve_scale" | |
| ) | |
| VinylRecord( | |
| isPlaying = isPlaying && stage >= 2, | |
| themeColor = ambientThemeColor, | |
| stampGradient = animatedSleeveGradient, | |
| modifier = Modifier | |
| .size(size = 280.dp) | |
| .zIndex(zIndex = 1f) | |
| .graphicsLayer { | |
| translationX = vinylOffsetX; alpha = vinylAlpha | |
| } | |
| .clickable( | |
| interactionSource = remember { MutableInteractionSource() }, | |
| indication = null, | |
| onClick = { playClosingChoreography() } | |
| ) | |
| ) | |
| with(sharedTransitionScope) { | |
| Box( | |
| modifier = Modifier | |
| .sharedElement( | |
| sharedContentState = rememberSharedContentState(key = "album_sleeve"), | |
| animatedVisibilityScope = animatedVisibilityScope, | |
| boundsTransform = { _, _ -> spatialGlide } | |
| ) | |
| .size(size = 300.dp) | |
| .zIndex(zIndex = 2f) | |
| .graphicsLayer { | |
| alpha = sleeveAlpha; scaleX = sleeveScale; scaleY = | |
| sleeveScale | |
| } | |
| .clip(shape = RoundedCornerShape(size = 12.dp)) | |
| .background(brush = Brush.linearGradient(colors = animatedSleeveGradient)) | |
| ) { | |
| Box( | |
| modifier = Modifier.fillMaxSize(), | |
| contentAlignment = Alignment.Center | |
| ) { | |
| TrackLogo( | |
| style = currentTrack.logoStyle, | |
| modifier = Modifier.size(size = 140.dp), | |
| isMini = false | |
| ) | |
| } | |
| Box( | |
| modifier = Modifier | |
| .align(alignment = Alignment.Center) | |
| .size(size = 285.dp) | |
| .border( | |
| width = 1.dp, | |
| color = Color.White.copy(alpha = 0.05f), | |
| shape = CircleShape | |
| ) | |
| ) | |
| Box( | |
| modifier = Modifier | |
| .align(alignment = Alignment.CenterEnd) | |
| .width(width = 6.dp) | |
| .fillMaxHeight() | |
| .background( | |
| brush = Brush.horizontalGradient( | |
| colors = listOf( | |
| Color.Transparent, | |
| Color.Black.copy(alpha = 0.4f) | |
| ) | |
| ) | |
| ) | |
| ) | |
| } | |
| } | |
| } | |
| Spacer(modifier = Modifier.height(height = 48.dp)) | |
| AnimatedVisibility( | |
| visible = stage >= 1, | |
| enter = slideInVertically(initialOffsetY = { 50 }) + fadeIn( | |
| animationSpec = tween(durationMillis = 500) | |
| ), | |
| exit = slideOutVertically(targetOffsetY = { 50 }) + fadeOut( | |
| animationSpec = tween(durationMillis = 300) | |
| ) | |
| ) { | |
| Column(horizontalAlignment = Alignment.CenterHorizontally) { | |
| Text( | |
| text = currentTrack.title, | |
| color = Color.White, | |
| fontSize = 28.sp, | |
| fontWeight = FontWeight.Black | |
| ) | |
| Spacer(modifier = Modifier.height(height = 4.dp)) | |
| Text( | |
| text = currentTrack.artist, | |
| color = ambientThemeColor, | |
| fontSize = 18.sp, | |
| fontWeight = FontWeight.Medium | |
| ) | |
| } | |
| } | |
| Spacer(modifier = Modifier.height(height = 32.dp)) | |
| PlayerControls( | |
| track = currentTrack, | |
| isPlaying = isPlaying, | |
| isVisible = stage >= 1, | |
| onTogglePlay = { isPlaying = !isPlaying }) | |
| } | |
| } | |
| AppScreen.PROFILE -> { | |
| Column( | |
| modifier = Modifier | |
| .fillMaxSize() | |
| .padding(top = 120.dp), | |
| horizontalAlignment = Alignment.CenterHorizontally | |
| ) { | |
| with(sharedTransitionScope) { | |
| ProfileAvatar( | |
| themeColor = ambientThemeColor, | |
| showDot = false, | |
| modifier = Modifier | |
| .sharedElement( | |
| sharedContentState = rememberSharedContentState(key = "profile_avatar"), | |
| animatedVisibilityScope = animatedVisibilityScope, | |
| boundsTransform = { _, _ -> | |
| spring( | |
| dampingRatio = 0.7f, | |
| stiffness = 120f | |
| ) | |
| } | |
| ) | |
| .size(size = 160.dp) | |
| ) | |
| } | |
| Spacer(modifier = Modifier.height(height = 40.dp)) | |
| Text( | |
| text = "VOID ARCHITECT", | |
| color = Color.White, | |
| fontSize = 32.sp, | |
| fontWeight = FontWeight.Black | |
| ) | |
| Spacer(modifier = Modifier.height(height = 8.dp)) | |
| Text( | |
| text = "Sonic Explorer", | |
| color = ambientThemeColor, | |
| fontSize = 16.sp, | |
| letterSpacing = 2.sp | |
| ) | |
| Spacer(modifier = Modifier.height(height = 64.dp)) | |
| Row( | |
| modifier = Modifier.fillMaxWidth(), | |
| horizontalArrangement = Arrangement.SpaceEvenly | |
| ) { | |
| ProfileStat( | |
| label = "PLAYLISTS", | |
| value = "12", | |
| themeColor = ambientThemeColor | |
| ) | |
| ProfileStat( | |
| label = "FOLLOWERS", | |
| value = "14.2K", | |
| themeColor = ambientThemeColor | |
| ) | |
| ProfileStat( | |
| label = "FOLLOWING", | |
| value = "89", | |
| themeColor = ambientThemeColor | |
| ) | |
| } | |
| Spacer(modifier = Modifier.weight(weight = 1f)) | |
| Box( | |
| modifier = Modifier | |
| .padding(bottom = 64.dp) | |
| .size(size = 64.dp) | |
| .clip(shape = CircleShape) | |
| .background(color = GlassSurface) | |
| .border( | |
| width = 1.dp, | |
| color = Color.White.copy(alpha = 0.1f), | |
| shape = CircleShape | |
| ) | |
| .clickable( | |
| interactionSource = remember { MutableInteractionSource() }, | |
| indication = null, | |
| onClick = { currentScreen = AppScreen.LIST } | |
| ), | |
| contentAlignment = Alignment.Center | |
| ) { | |
| Canvas(modifier = Modifier.size(size = 24.dp)) { | |
| drawLine( | |
| color = Color.White, | |
| start = Offset(x = 0f, y = 0f), | |
| end = Offset(x = this.size.width, y = this.size.height), | |
| strokeWidth = 2.dp.toPx(), | |
| cap = StrokeCap.Round | |
| ) | |
| drawLine( | |
| color = Color.White, | |
| start = Offset(x = 0f, y = this.size.height), | |
| end = Offset(x = this.size.width, y = 0f), | |
| strokeWidth = 2.dp.toPx(), | |
| cap = StrokeCap.Round | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment