fix: polish pass — dark mode, notifications, icons, edge-to-edge
- Wrap SettingsScreen in Scaffold to fix invisible text in dark mode - Fix notification showing raw ICY metadata instead of parsed track info - Add proper white-on-transparent notification icon - Create branded adaptive launcher icon (amber tower on BlueGray900) - Add edge-to-edge support with proper window inset handling - Add About section to Settings with version and app info - Enable BuildConfig generation for version display Made-with: Cursor
This commit is contained in:
@@ -7,6 +7,7 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.setContent
|
||||
@@ -26,6 +27,7 @@ import xyz.cottongin.radio247.ui.theme.Radio247Theme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
val requestNotificationPermission = rememberLauncherForActivityResult(
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package xyz.cottongin.radio247.audio
|
||||
|
||||
object MetadataFormatter {
|
||||
fun formatTrackInfo(metadata: IcyMetadata?, fallback: String = "\u266A"): String {
|
||||
if (metadata == null) return fallback
|
||||
return when {
|
||||
metadata.artist != null && metadata.title != null ->
|
||||
"${metadata.artist} \u2014 ${metadata.title}"
|
||||
metadata.title != null -> metadata.title
|
||||
metadata.artist != null -> metadata.artist
|
||||
else -> fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import androidx.core.app.NotificationCompat
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import xyz.cottongin.radio247.R
|
||||
import xyz.cottongin.radio247.audio.IcyMetadata
|
||||
import xyz.cottongin.radio247.audio.MetadataFormatter
|
||||
import xyz.cottongin.radio247.data.model.Station
|
||||
|
||||
class NotificationHelper(private val context: Context) {
|
||||
@@ -41,11 +42,11 @@ class NotificationHelper(private val context: Context) {
|
||||
.setContentText(
|
||||
when {
|
||||
isReconnecting -> "Reconnecting..."
|
||||
metadata?.title != null -> metadata.raw
|
||||
metadata != null -> MetadataFormatter.formatTrackInfo(metadata, fallback = "Playing")
|
||||
else -> "Playing"
|
||||
}
|
||||
)
|
||||
.setSmallIcon(R.drawable.ic_radio_placeholder)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setOngoing(true)
|
||||
.setStyle(
|
||||
androidx.media.app.NotificationCompat.MediaStyle()
|
||||
|
||||
@@ -26,12 +26,16 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
@@ -81,6 +85,7 @@ import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
@@ -109,6 +114,7 @@ import coil3.size.Size
|
||||
import xyz.cottongin.radio247.R
|
||||
import xyz.cottongin.radio247.RadioApplication
|
||||
import xyz.cottongin.radio247.audio.IcyMetadata
|
||||
import xyz.cottongin.radio247.audio.MetadataFormatter
|
||||
import xyz.cottongin.radio247.audio.StreamInfo
|
||||
import xyz.cottongin.radio247.service.PlaybackState
|
||||
import com.skydoves.cloudy.cloudy
|
||||
@@ -432,7 +438,8 @@ private fun ArtworkImage(
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_radio_placeholder),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(120.dp)
|
||||
modifier = Modifier.size(120.dp),
|
||||
colorFilter = ColorFilter.tint(contrastingTextColor(bgColor))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -903,13 +910,16 @@ private fun PortraitContent(
|
||||
val cardShape = RoundedCornerShape(14.dp)
|
||||
val backBtnShape = RoundedCornerShape(10.dp)
|
||||
|
||||
val statusBarPadding = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
val navBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp, end = 24.dp, top = 48.dp, bottom = 8.dp),
|
||||
.padding(start = 24.dp, end = 24.dp, top = statusBarPadding + 48.dp, bottom = 8.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ArtworkImage(
|
||||
@@ -968,12 +978,12 @@ private fun PortraitContent(
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(navBarPadding + 8.dp))
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, top = 12.dp)
|
||||
.padding(start = 12.dp, top = statusBarPadding + 12.dp)
|
||||
.size(40.dp)
|
||||
.clip(backBtnShape)
|
||||
.background(bgColor.copy(alpha = 0.5f))
|
||||
@@ -1135,16 +1145,8 @@ private fun OverlayCards(
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTrackInfo(metadata: IcyMetadata?): String {
|
||||
if (metadata == null) return "\u266A"
|
||||
return when {
|
||||
metadata.artist != null && metadata.title != null ->
|
||||
"${metadata.artist} \u2014 ${metadata.title}"
|
||||
metadata.title != null -> metadata.title
|
||||
metadata.artist != null -> metadata.artist
|
||||
else -> "\u266A"
|
||||
}
|
||||
}
|
||||
private fun formatTrackInfo(metadata: IcyMetadata?): String =
|
||||
MetadataFormatter.formatTrackInfo(metadata)
|
||||
|
||||
private fun formatElapsed(millis: Long): String {
|
||||
if (millis <= 0) return "0s"
|
||||
|
||||
@@ -32,6 +32,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Switch
|
||||
@@ -106,24 +107,28 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = modifier.fillMaxSize()) {
|
||||
TopAppBar(
|
||||
title = { Text("Settings") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
titleContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onSurface
|
||||
Scaffold(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Settings") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
titleContentColor = MaterialTheme.colorScheme.onSurface,
|
||||
navigationIconContentColor = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(16.dp)
|
||||
) {
|
||||
@@ -256,6 +261,35 @@ fun SettingsScreen(
|
||||
HorizontalDivider()
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// ── ABOUT ──
|
||||
SectionHeader("ABOUT")
|
||||
Text(
|
||||
text = "24/7 Radio",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = "Version ${xyz.cottongin.radio247.BuildConfig.VERSION_NAME}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Personal-use internet radio streaming app with ultra-low latency playback and Icecast/Shoutcast metadata support.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = xyz.cottongin.radio247.BuildConfig.APPLICATION_ID,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// ── RESET ──
|
||||
SectionHeader("RESET")
|
||||
SettingRow("Also delete saved stations/playlists") {
|
||||
|
||||
Reference in New Issue
Block a user