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:
cottongin
2026-03-10 21:18:48 -04:00
parent 5a59b3a86b
commit d381e80828
17 changed files with 468 additions and 30 deletions

View File

@@ -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(

View File

@@ -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
}
}
}

View File

@@ -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()

View File

@@ -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"

View File

@@ -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") {