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

@@ -36,6 +36,7 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
}

View File

@@ -11,6 +11,8 @@
<application
android:name=".RadioApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.Radio247">

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,7 +107,9 @@ fun SettingsScreen(
}
}
Column(modifier = modifier.fillMaxSize()) {
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = { Text("Settings") },
navigationIcon = {
@@ -120,10 +123,12 @@ fun SettingsScreen(
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") {

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#0D1B2A"
android:pathData="M0,0h108v108h-108z" />
</vector>

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Adaptive icon safe zone: 72dp centered in 108dp (18dp inset each side) -->
<!-- Tower mast -->
<path
android:fillColor="#FFC107"
android:pathData="M52,28h4v40h-4z" />
<!-- Tower base / tripod -->
<path
android:fillColor="#FFC107"
android:pathData="M42,72l12,-8l12,8z" />
<path
android:fillColor="#FFC107"
android:pathData="M44,72h20v3h-20z" />
<!-- Antenna tip -->
<path
android:fillColor="#FFD54F"
android:pathData="M54,28m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" />
<!-- Inner broadcast waves (left) -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFC107"
android:strokeWidth="2.5"
android:strokeLineCap="round"
android:pathData="M42,42a12,12 0,0 1,0 -14" />
<!-- Inner broadcast waves (right) -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFC107"
android:strokeWidth="2.5"
android:strokeLineCap="round"
android:pathData="M66,42a12,12 0,0 0,0 -14" />
<!-- Outer broadcast waves (left) -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFB300"
android:strokeWidth="2.5"
android:strokeLineCap="round"
android:pathData="M34,48a22,22 0,0 1,0 -26" />
<!-- Outer broadcast waves (right) -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFB300"
android:strokeWidth="2.5"
android:strokeLineCap="round"
android:pathData="M74,48a22,22 0,0 0,0 -26" />
</vector>

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- Radio tower with broadcast waves -->
<!-- Tower body -->
<path
android:fillColor="#FFFFFF"
android:pathData="M11,2h2v16h-2z" />
<!-- Tower base -->
<path
android:fillColor="#FFFFFF"
android:pathData="M8,20l4,-4l4,4z" />
<!-- Left wave inner -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M7.5,7.5a5,5 0,0 1,0 -5" />
<!-- Right wave inner -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M16.5,7.5a5,5 0,0 0,0 -5" />
<!-- Left wave outer -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M4.5,9.5a9,9 0,0 1,0 -9" />
<!-- Right wave outer -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M19.5,9.5a9,9 0,0 0,0 -9" />
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<!-- Background -->
<path
android:fillColor="#0D1B2A"
android:pathData="M0,0h48v48h-48z" />
<!-- Tower mast -->
<path
android:fillColor="#FFC107"
android:pathData="M22.5,10h3v22h-3z" />
<!-- Tower base -->
<path
android:fillColor="#FFC107"
android:pathData="M16,34l8,-5l8,5z" />
<path
android:fillColor="#FFC107"
android:pathData="M17,34h14v2h-14z" />
<!-- Antenna tip -->
<path
android:fillColor="#FFD54F"
android:pathData="M24,10m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0" />
<!-- Inner waves -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFC107"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M17,20a7,7 0,0 1,0 -8" />
<path
android:fillColor="#00000000"
android:strokeColor="#FFC107"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M31,20a7,7 0,0 0,0 -8" />
<!-- Outer waves -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFB300"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M12,24a13,13 0,0 1,0 -16" />
<path
android:fillColor="#00000000"
android:strokeColor="#FFB300"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M36,24a13,13 0,0 0,0 -16" />
</vector>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<!-- Background -->
<path
android:fillColor="#0D1B2A"
android:pathData="M0,0h48v48h-48z" />
<!-- Tower mast -->
<path
android:fillColor="#FFC107"
android:pathData="M22.5,10h3v22h-3z" />
<!-- Tower base -->
<path
android:fillColor="#FFC107"
android:pathData="M16,34l8,-5l8,5z" />
<path
android:fillColor="#FFC107"
android:pathData="M17,34h14v2h-14z" />
<!-- Antenna tip -->
<path
android:fillColor="#FFD54F"
android:pathData="M24,10m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0" />
<!-- Inner waves -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFC107"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M17,20a7,7 0,0 1,0 -8" />
<path
android:fillColor="#00000000"
android:strokeColor="#FFC107"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M31,20a7,7 0,0 0,0 -8" />
<!-- Outer waves -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFB300"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M12,24a13,13 0,0 1,0 -16" />
<path
android:fillColor="#00000000"
android:strokeColor="#FFB300"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M36,24a13,13 0,0 0,0 -16" />
</vector>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<!-- Background -->
<path
android:fillColor="#0D1B2A"
android:pathData="M0,0h48v48h-48z" />
<!-- Tower mast -->
<path
android:fillColor="#FFC107"
android:pathData="M22.5,10h3v22h-3z" />
<!-- Tower base -->
<path
android:fillColor="#FFC107"
android:pathData="M16,34l8,-5l8,5z" />
<path
android:fillColor="#FFC107"
android:pathData="M17,34h14v2h-14z" />
<!-- Antenna tip -->
<path
android:fillColor="#FFD54F"
android:pathData="M24,10m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0" />
<!-- Inner waves -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFC107"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M17,20a7,7 0,0 1,0 -8" />
<path
android:fillColor="#00000000"
android:strokeColor="#FFC107"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M31,20a7,7 0,0 0,0 -8" />
<!-- Outer waves -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFB300"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M12,24a13,13 0,0 1,0 -16" />
<path
android:fillColor="#00000000"
android:strokeColor="#FFB300"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M36,24a13,13 0,0 0,0 -16" />
</vector>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<!-- Background -->
<path
android:fillColor="#0D1B2A"
android:pathData="M0,0h48v48h-48z" />
<!-- Tower mast -->
<path
android:fillColor="#FFC107"
android:pathData="M22.5,10h3v22h-3z" />
<!-- Tower base -->
<path
android:fillColor="#FFC107"
android:pathData="M16,34l8,-5l8,5z" />
<path
android:fillColor="#FFC107"
android:pathData="M17,34h14v2h-14z" />
<!-- Antenna tip -->
<path
android:fillColor="#FFD54F"
android:pathData="M24,10m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0" />
<!-- Inner waves -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFC107"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M17,20a7,7 0,0 1,0 -8" />
<path
android:fillColor="#00000000"
android:strokeColor="#FFC107"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M31,20a7,7 0,0 0,0 -8" />
<!-- Outer waves -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFB300"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M12,24a13,13 0,0 1,0 -16" />
<path
android:fillColor="#00000000"
android:strokeColor="#FFB300"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M36,24a13,13 0,0 0,0 -16" />
</vector>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<!-- Background -->
<path
android:fillColor="#0D1B2A"
android:pathData="M0,0h48v48h-48z" />
<!-- Tower mast -->
<path
android:fillColor="#FFC107"
android:pathData="M22.5,10h3v22h-3z" />
<!-- Tower base -->
<path
android:fillColor="#FFC107"
android:pathData="M16,34l8,-5l8,5z" />
<path
android:fillColor="#FFC107"
android:pathData="M17,34h14v2h-14z" />
<!-- Antenna tip -->
<path
android:fillColor="#FFD54F"
android:pathData="M24,10m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0" />
<!-- Inner waves -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFC107"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M17,20a7,7 0,0 1,0 -8" />
<path
android:fillColor="#00000000"
android:strokeColor="#FFC107"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M31,20a7,7 0,0 0,0 -8" />
<!-- Outer waves -->
<path
android:fillColor="#00000000"
android:strokeColor="#FFB300"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M12,24a13,13 0,0 1,0 -16" />
<path
android:fillColor="#00000000"
android:strokeColor="#FFB300"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:pathData="M36,24a13,13 0,0 0,0 -16" />
</vector>