feat: add Material 3 theme and screen navigation

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-10 02:37:26 -04:00
parent ca7b757812
commit cacbb0d98d
10 changed files with 301 additions and 15 deletions

View File

@@ -55,6 +55,8 @@ dependencies {
implementation(libs.datastore.preferences) implementation(libs.datastore.preferences)
implementation(libs.okhttp) implementation(libs.okhttp)
implementation(libs.lifecycle.viewmodel.compose) implementation(libs.lifecycle.viewmodel.compose)
implementation(libs.lifecycle.viewmodel.ktx)
implementation(libs.material.icons.extended)
implementation(libs.lifecycle.runtime.compose) implementation(libs.lifecycle.runtime.compose)
implementation(libs.lifecycle.service) implementation(libs.lifecycle.service)
implementation(libs.coroutines.core) implementation(libs.coroutines.core)

View File

@@ -2,27 +2,42 @@ package xyz.cottongin.radio247
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.material3.Text import androidx.compose.runtime.getValue
import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import xyz.cottongin.radio247.ui.navigation.Screen
import xyz.cottongin.radio247.ui.screens.nowplaying.NowPlayingScreen
import xyz.cottongin.radio247.ui.screens.settings.SettingsScreen
import xyz.cottongin.radio247.ui.screens.stationlist.StationListScreen
import xyz.cottongin.radio247.ui.theme.Radio247Theme
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
Radio247Placeholder() Radio247Theme {
var currentScreen by remember { mutableStateOf<Screen>(Screen.StationList) }
BackHandler(enabled = currentScreen != Screen.StationList) {
currentScreen = Screen.StationList
}
when (currentScreen) {
Screen.StationList -> StationListScreen(
onNavigateToNowPlaying = { currentScreen = Screen.NowPlaying },
onNavigateToSettings = { currentScreen = Screen.Settings }
)
Screen.NowPlaying -> NowPlayingScreen(
onBack = { currentScreen = Screen.StationList }
)
Screen.Settings -> SettingsScreen(
onBack = { currentScreen = Screen.StationList }
)
}
}
} }
} }
} }
@Composable
private fun Radio247Placeholder() {
Text("24/7 Radio")
}
@Preview(showBackground = true)
@Composable
private fun Radio247PlaceholderPreview() {
Radio247Placeholder()
}

View File

@@ -0,0 +1,7 @@
package xyz.cottongin.radio247.ui.navigation
sealed class Screen {
data object StationList : Screen()
data object NowPlaying : Screen()
data object Settings : Screen()
}

View File

@@ -0,0 +1,23 @@
package xyz.cottongin.radio247.ui.screens.nowplaying
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun NowPlayingScreen(
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier) {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
Text("Now Playing")
}
}

View File

@@ -0,0 +1,23 @@
package xyz.cottongin.radio247.ui.screens.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun SettingsScreen(
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier) {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
Text("Settings")
}
}

View File

@@ -0,0 +1,21 @@
package xyz.cottongin.radio247.ui.screens.stationlist
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun StationListScreen(
onNavigateToNowPlaying: () -> Unit,
onNavigateToSettings: () -> Unit,
modifier: Modifier = Modifier
) {
// Placeholder - full implementation in Task 10
Column(modifier) {
Text("Station List")
Button(onClick = onNavigateToNowPlaying) { Text("Now Playing") }
Button(onClick = onNavigateToSettings) { Text("Settings") }
}
}

View File

@@ -0,0 +1,23 @@
package xyz.cottongin.radio247.ui.theme
import androidx.compose.ui.graphics.Color
// Radio/broadcast themed palette - dark blues, amber accents
// Light scheme
val BlueGray900 = Color(0xFF0D1B2A)
val BlueGray800 = Color(0xFF1B263B)
val BlueGray700 = Color(0xFF415A77)
val BlueGray600 = Color(0xFF778DA9)
val BlueGray500 = Color(0xFFE0E1DD)
val Amber500 = Color(0xFFFFB300)
val Amber400 = Color(0xFFFFC107)
val Amber300 = Color(0xFFFFD54F)
// Dark scheme
val BlueGray100 = Color(0xFFE8EDF2)
val BlueGray200 = Color(0xFFC5CBD3)
val BlueGray300 = Color(0xFF778DA9)
val BlueGray400 = Color(0xFF415A77)
val BlueGray950 = Color(0xFF0A0F14)
val Amber200 = Color(0xFFFFE082)
val Amber100 = Color(0xFFFFECB3)

View File

@@ -0,0 +1,70 @@
package xyz.cottongin.radio247.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Amber400,
onPrimary = BlueGray950,
primaryContainer = BlueGray700,
onPrimaryContainer = BlueGray100,
secondary = BlueGray400,
onSecondary = BlueGray100,
secondaryContainer = BlueGray800,
onSecondaryContainer = BlueGray200,
tertiary = Amber300,
onTertiary = BlueGray950,
background = BlueGray950,
onBackground = BlueGray100,
surface = BlueGray900,
onSurface = BlueGray100,
surfaceVariant = BlueGray800,
onSurfaceVariant = BlueGray300,
outline = BlueGray600,
outlineVariant = BlueGray700
)
private val LightColorScheme = lightColorScheme(
primary = Amber500,
onPrimary = BlueGray900,
primaryContainer = Amber300,
onPrimaryContainer = BlueGray900,
secondary = BlueGray600,
onSecondary = BlueGray100,
secondaryContainer = BlueGray200,
onSecondaryContainer = BlueGray800,
tertiary = Amber400,
onTertiary = BlueGray900,
background = BlueGray100,
onBackground = BlueGray900,
surface = BlueGray500,
onSurface = BlueGray900,
surfaceVariant = BlueGray200,
onSurfaceVariant = BlueGray700,
outline = BlueGray600,
outlineVariant = BlueGray400
)
@Composable
fun Radio247Theme(content: @Composable () -> Unit) {
val dynamicColor = Build.VERSION.SDK_INT >= 31
val darkTheme = isSystemInDarkTheme()
val colorScheme = when {
dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@@ -0,0 +1,100 @@
package xyz.cottongin.radio247.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
val Typography = Typography(
displayLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 57.sp,
lineHeight = 64.sp
),
displayMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 45.sp,
lineHeight = 52.sp
),
displaySmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 36.sp,
lineHeight = 44.sp
),
headlineLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 32.sp,
lineHeight = 40.sp
),
headlineMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 28.sp,
lineHeight = 36.sp
),
headlineSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 24.sp,
lineHeight = 32.sp
),
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 22.sp,
lineHeight = 28.sp
),
titleMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
lineHeight = 24.sp
),
titleSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp
),
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp
),
bodyMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp
),
bodySmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 16.sp
),
labelLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp
),
labelMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 12.sp,
lineHeight = 16.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp
)
)

View File

@@ -30,6 +30,8 @@ datastore-preferences = { group = "androidx.datastore", name = "datastore-prefer
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
okhttp-mockwebserver = { group = "com.squareup.okhttp3", name = "mockwebserver", version.ref = "okhttp" } okhttp-mockwebserver = { group = "com.squareup.okhttp3", name = "mockwebserver", version.ref = "okhttp" }
lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
lifecycle-service = { group = "androidx.lifecycle", name = "lifecycle-service", version.ref = "lifecycle" } lifecycle-service = { group = "androidx.lifecycle", name = "lifecycle-service", version.ref = "lifecycle" }
coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }