feat: rename app to SatsAmp + appearance themes (violet/blue/green/orange accents, System/Dark/Light modes)
- project dir ~/Projects/RadAmp -> ~/Projects/SatsAmp; classes, strings, launcher label, UA strings renamed; applicationId + radamp.db kept so existing installs/data survive - UiPrefs (SharedPreferences) + UiTokens: 4 accents x dark/light, every text/accent pair AA-verified (accentOnFill picks ink vs white per accent) - legacy Premium* colors rewritten to LocalTokens.current reads; theme switches live, persists across restart - Appearance dialog in Library top bar (swatches + System/Dark/Light), verified on-device: orange live-switch, light mode + persistence, folder-tab regression pass
This commit is contained in:
parent
6628965e68
commit
1b7c6febbb
1351 changed files with 1971 additions and 1663 deletions
|
|
@ -9,18 +9,18 @@
|
|||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<application
|
||||
android:name=".RadAmpApp"
|
||||
android:name=".SatsAmpApp"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@style/Theme.RadAmp">
|
||||
android:theme="@style/Theme.SatsAmp">
|
||||
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.RadAmp">
|
||||
android:theme="@style/Theme.SatsAmp">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
package com.rada
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class RadAmpApp : Application()
|
||||
13
app/src/main/java/com/rada/SatsAmpApp.kt
Normal file
13
app/src/main/java/com/rada/SatsAmpApp.kt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package com.rada
|
||||
|
||||
import android.app.Application
|
||||
import com.rada.ui.theme.UiPrefs
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class SatsAmpApp : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
UiPrefs.init(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ import com.rada.data.local.entity.SavedSongEntity
|
|||
version = 2,
|
||||
exportSchema = false,
|
||||
)
|
||||
abstract class RadAmpDatabase : RoomDatabase() {
|
||||
abstract class SatsAmpDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun savedSongDao(): SavedSongDao
|
||||
abstract fun playlistDao(): PlaylistDao
|
||||
|
|
@ -50,10 +50,10 @@ abstract class RadAmpDatabase : RoomDatabase() {
|
|||
}
|
||||
}
|
||||
|
||||
fun create(context: Context): RadAmpDatabase {
|
||||
fun create(context: Context): SatsAmpDatabase {
|
||||
return Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
RadAmpDatabase::class.java,
|
||||
SatsAmpDatabase::class.java,
|
||||
"radamp.db",
|
||||
).addMigrations(MIGRATION_1_2).build()
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ class StreamFetcher @Inject constructor() {
|
|||
}
|
||||
|
||||
private fun download(url: String): String {
|
||||
val request = Request.Builder().url(url).header("User-Agent", "RadAmp/1.0").build()
|
||||
val request = Request.Builder().url(url).header("User-Agent", "SatsAmp/1.0").build()
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) throw IllegalStateException("Failed to fetch playlist: ${response.code}")
|
||||
return response.body?.string().orEmpty()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.rada.di
|
||||
|
||||
import android.content.Context
|
||||
import com.rada.data.local.RadAmpDatabase
|
||||
import com.rada.data.local.SatsAmpDatabase
|
||||
import com.rada.data.local.dao.PlaylistDao
|
||||
import com.rada.data.local.dao.SavedSongDao
|
||||
import com.rada.data.repository.LibraryRepository
|
||||
|
|
@ -23,18 +23,18 @@ object AppModule {
|
|||
@Singleton
|
||||
fun provideDatabase(
|
||||
@ApplicationContext context: Context,
|
||||
): RadAmpDatabase = RadAmpDatabase.create(context)
|
||||
): SatsAmpDatabase = SatsAmpDatabase.create(context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSavedSongDao(
|
||||
database: RadAmpDatabase,
|
||||
database: SatsAmpDatabase,
|
||||
): SavedSongDao = database.savedSongDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePlaylistDao(
|
||||
database: RadAmpDatabase,
|
||||
database: SatsAmpDatabase,
|
||||
): PlaylistDao = database.playlistDao()
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ class IcyStreamReader(
|
|||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.header("Icy-MetaData", "1")
|
||||
.header("User-Agent", "RadAmp/0.1")
|
||||
.header("User-Agent", "SatsAmp/0.1")
|
||||
.build()
|
||||
|
||||
val response = okHttpClient.newCall(request).execute()
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class MetadataEmbedder(private val context: Context) {
|
|||
val tag = audio.tag
|
||||
tag.setField(FieldKey.ARTIST, artist.ifEmpty { "Unknown Artist" })
|
||||
tag.setField(FieldKey.TITLE, title.ifEmpty { "Unknown Title" })
|
||||
tag.setField(FieldKey.ALBUM, stationName.ifEmpty { "RadAmp" })
|
||||
tag.setField(FieldKey.ALBUM, stationName.ifEmpty { "SatsAmp" })
|
||||
tag.setField(FieldKey.GENRE, "Radio")
|
||||
tag.setField(FieldKey.COMMENT, "Recorded from $stationName")
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import androidx.navigation.compose.rememberNavController
|
|||
import com.rada.player.PlaybackManager
|
||||
import com.rada.ui.navigation.NavGraph
|
||||
import com.rada.ui.theme.GlassBackground
|
||||
import com.rada.ui.theme.RadAmpTheme
|
||||
import com.rada.ui.theme.SatsAmpTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ class MainActivity : ComponentActivity() {
|
|||
playbackManager.connect()
|
||||
|
||||
setContent {
|
||||
RadAmpTheme {
|
||||
SatsAmpTheme {
|
||||
GlassBackground {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import androidx.compose.material.icons.filled.MusicNote
|
|||
import androidx.compose.material.icons.filled.PlaylistAdd
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Shuffle
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
|
|
@ -75,13 +76,8 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||
import com.rada.data.local.entity.PlaylistEntity
|
||||
import com.rada.data.local.entity.SavedSongEntity
|
||||
import com.rada.data.repository.SortOrder
|
||||
import com.rada.ui.theme.PremiumBorder
|
||||
import com.rada.ui.theme.PremiumPurple
|
||||
import com.rada.ui.theme.PremiumPurpleLight
|
||||
import com.rada.ui.theme.PremiumSurface
|
||||
import com.rada.ui.theme.PremiumSurfaceVariant
|
||||
import com.rada.ui.theme.PremiumText
|
||||
import com.rada.ui.theme.PremiumTextSecondary
|
||||
import com.rada.ui.theme.AppearanceDialog
|
||||
import com.rada.ui.theme.LocalTokens
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
@ -97,6 +93,7 @@ fun LibraryScreen(
|
|||
var showSearch by remember { mutableStateOf(false) }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
var showImportMenu by remember { mutableStateOf(false) }
|
||||
var showAppearance by remember { mutableStateOf(false) }
|
||||
var playlistToDelete by remember { mutableStateOf<PlaylistEntity?>(null) }
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
|
@ -154,7 +151,7 @@ fun LibraryScreen(
|
|||
Icon(
|
||||
Icons.Default.PlaylistAdd,
|
||||
contentDescription = "Import songs",
|
||||
tint = if (uiState.isImporting) PremiumTextSecondary.copy(alpha = 0.4f) else PremiumPurple,
|
||||
tint = if (uiState.isImporting) LocalTokens.current.textSecondary.copy(alpha = 0.4f) else LocalTokens.current.accentFill,
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
|
|
@ -189,11 +186,18 @@ fun LibraryScreen(
|
|||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Delete folder",
|
||||
tint = PremiumTextSecondary,
|
||||
tint = LocalTokens.current.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { showAppearance = true }) {
|
||||
Icon(
|
||||
Icons.Default.Settings,
|
||||
contentDescription = "Appearance settings",
|
||||
tint = LocalTokens.current.textSecondary,
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
|
|
@ -214,12 +218,12 @@ fun LibraryScreen(
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
edgePadding = 16.dp,
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = PremiumPurple,
|
||||
contentColor = LocalTokens.current.accentFill,
|
||||
indicator = { tabPositions ->
|
||||
if (selectedTabIndex < tabPositions.size) {
|
||||
TabRowDefaults.SecondaryIndicator(
|
||||
modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]),
|
||||
color = PremiumPurple,
|
||||
color = LocalTokens.current.accentFill,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
@ -270,7 +274,7 @@ fun LibraryScreen(
|
|||
text = label,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = if (selectedTabIndex == index) PremiumPurple else PremiumTextSecondary,
|
||||
color = if (selectedTabIndex == index) LocalTokens.current.accentFill else LocalTokens.current.textSecondary,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -293,16 +297,16 @@ fun LibraryScreen(
|
|||
Icon(
|
||||
Icons.Default.Search,
|
||||
contentDescription = "Search",
|
||||
tint = PremiumTextSecondary,
|
||||
tint = LocalTokens.current.textSecondary,
|
||||
)
|
||||
},
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = PremiumSurface,
|
||||
unfocusedContainerColor = PremiumSurface,
|
||||
focusedIndicatorColor = PremiumPurple.copy(alpha = 0.5f),
|
||||
unfocusedIndicatorColor = PremiumBorder,
|
||||
focusedTextColor = PremiumText,
|
||||
unfocusedTextColor = PremiumText,
|
||||
focusedContainerColor = LocalTokens.current.panel,
|
||||
unfocusedContainerColor = LocalTokens.current.panel,
|
||||
focusedIndicatorColor = LocalTokens.current.accentFill.copy(alpha = 0.5f),
|
||||
unfocusedIndicatorColor = LocalTokens.current.border,
|
||||
focusedTextColor = LocalTokens.current.text,
|
||||
unfocusedTextColor = LocalTokens.current.text,
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
)
|
||||
|
|
@ -321,7 +325,7 @@ fun LibraryScreen(
|
|||
Text(
|
||||
text = "Sort",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = PremiumTextSecondary,
|
||||
color = LocalTokens.current.textSecondary,
|
||||
)
|
||||
SortChip("Recent", uiState.sortOrder == SortOrder.DATE_SAVED, uiState.sortAscending) {
|
||||
viewModel.setSortOrder(SortOrder.DATE_SAVED)
|
||||
|
|
@ -345,14 +349,14 @@ fun LibraryScreen(
|
|||
Icons.Default.MusicNote,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = PremiumPurple.copy(alpha = 0.3f),
|
||||
tint = LocalTokens.current.accentFill.copy(alpha = 0.3f),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = if (uiState.selectedPlaylistId != null) "Playlist is empty"
|
||||
else "No saved songs yet — tap + to import songs from your device",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = PremiumTextSecondary,
|
||||
color = LocalTokens.current.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -376,6 +380,10 @@ fun LibraryScreen(
|
|||
|
||||
// Long-press a folder tab: clear choice between deleting just the group
|
||||
// (songs stay in All Songs) or deleting its songs and files as well.
|
||||
if (showAppearance) {
|
||||
AppearanceDialog(onDismiss = { showAppearance = false })
|
||||
}
|
||||
|
||||
playlistToDelete?.let { playlist ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { playlistToDelete = null },
|
||||
|
|
@ -387,7 +395,7 @@ fun LibraryScreen(
|
|||
viewModel.deletePlaylist(playlist, deleteSongs = false)
|
||||
playlistToDelete = null
|
||||
}) {
|
||||
Text("Delete group only", color = PremiumText)
|
||||
Text("Delete group only", color = LocalTokens.current.text)
|
||||
}
|
||||
TextButton(onClick = {
|
||||
viewModel.deletePlaylist(playlist, deleteSongs = true)
|
||||
|
|
@ -399,7 +407,7 @@ fun LibraryScreen(
|
|||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { playlistToDelete = null }) {
|
||||
Text("Cancel", color = PremiumTextSecondary)
|
||||
Text("Cancel", color = LocalTokens.current.textSecondary)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -427,14 +435,14 @@ private fun SortChip(
|
|||
} else null,
|
||||
label = { Text(label) },
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
containerColor = PremiumSurface,
|
||||
labelColor = PremiumTextSecondary,
|
||||
selectedContainerColor = PremiumPurple.copy(alpha = 0.22f),
|
||||
selectedLabelColor = PremiumPurpleLight,
|
||||
containerColor = LocalTokens.current.panel,
|
||||
labelColor = LocalTokens.current.textSecondary,
|
||||
selectedContainerColor = LocalTokens.current.accentFill.copy(alpha = 0.22f),
|
||||
selectedLabelColor = LocalTokens.current.accentText,
|
||||
),
|
||||
border = FilterChipDefaults.filterChipBorder(
|
||||
borderColor = PremiumBorder,
|
||||
selectedBorderColor = PremiumPurple,
|
||||
borderColor = LocalTokens.current.border,
|
||||
selectedBorderColor = LocalTokens.current.accentFill,
|
||||
enabled = true,
|
||||
selected = selected,
|
||||
),
|
||||
|
|
@ -459,7 +467,7 @@ private fun SongListItem(
|
|||
Icon(
|
||||
Icons.Default.MusicNote,
|
||||
contentDescription = null,
|
||||
tint = if (isPlaying) PremiumPurple else PremiumTextSecondary,
|
||||
tint = if (isPlaying) LocalTokens.current.accentFill else LocalTokens.current.textSecondary,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
|
@ -470,12 +478,12 @@ private fun SongListItem(
|
|||
fontWeight = if (isPlaying) FontWeight.Bold else FontWeight.Normal,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = PremiumText,
|
||||
color = LocalTokens.current.text,
|
||||
)
|
||||
Text(
|
||||
text = song.artist,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = PremiumTextSecondary,
|
||||
color = LocalTokens.current.textSecondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
|
@ -483,7 +491,7 @@ private fun SongListItem(
|
|||
Text(
|
||||
text = song.stationName,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = PremiumTextSecondary.copy(alpha = 0.6f),
|
||||
color = LocalTokens.current.textSecondary.copy(alpha = 0.6f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -492,14 +500,14 @@ private fun SongListItem(
|
|||
if (song.isFavorite) Icons.Default.Favorite
|
||||
else Icons.Default.FavoriteBorder,
|
||||
contentDescription = if (song.isFavorite) "Unfavorite" else "Favorite",
|
||||
tint = if (song.isFavorite) PremiumPurple else PremiumTextSecondary,
|
||||
tint = if (song.isFavorite) LocalTokens.current.accentFill else LocalTokens.current.textSecondary,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Delete",
|
||||
tint = PremiumTextSecondary,
|
||||
tint = LocalTokens.current.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,17 +40,16 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.rada.player.PlaybackManager
|
||||
import com.rada.ui.theme.PremiumPurple
|
||||
import com.rada.ui.theme.PremiumSurface
|
||||
import com.rada.ui.theme.RadAmpGlass
|
||||
import com.rada.ui.theme.PremiumText
|
||||
import com.rada.ui.theme.PremiumTextSecondary
|
||||
import com.rada.ui.theme.LocalTokens
|
||||
import com.rada.ui.theme.SatsAmpGlass
|
||||
|
||||
@Composable
|
||||
fun NowPlayingBar(
|
||||
onNavigateToPlayer: () -> Unit,
|
||||
viewModel: NowPlayingViewModel = hiltViewModel(),
|
||||
) {
|
||||
val tokens = LocalTokens.current
|
||||
|
||||
val stationName by viewModel.currentStationName.collectAsState()
|
||||
val isPlaying by viewModel.isPlaying.collectAsState()
|
||||
val bufferState by viewModel.bufferState.collectAsState()
|
||||
|
|
@ -65,11 +64,11 @@ fun NowPlayingBar(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp)
|
||||
.clip(RoundedCornerShape(RadAmpGlass.rCard))
|
||||
.clip(RoundedCornerShape(SatsAmpGlass.rCard))
|
||||
// glass: textSafe 90% overlay + 6% white + highlight border
|
||||
.background(RadAmpGlass.textSafe)
|
||||
.background(RadAmpGlass.surface)
|
||||
.border(1.dp, RadAmpGlass.border, RoundedCornerShape(RadAmpGlass.rCard))
|
||||
.background(tokens.textSafe)
|
||||
.background(tokens.glassFill)
|
||||
.border(1.dp, tokens.border, RoundedCornerShape(SatsAmpGlass.rCard))
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
|
|
@ -86,8 +85,8 @@ fun NowPlayingBar(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(2.dp),
|
||||
color = PremiumPurple,
|
||||
trackColor = PremiumSurface,
|
||||
color = LocalTokens.current.accentFill,
|
||||
trackColor = LocalTokens.current.panel,
|
||||
)
|
||||
|
||||
Row(
|
||||
|
|
@ -101,7 +100,7 @@ fun NowPlayingBar(
|
|||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(PremiumPurple),
|
||||
.background(LocalTokens.current.accentFill),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
|
|
@ -127,7 +126,7 @@ fun NowPlayingBar(
|
|||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = PremiumText,
|
||||
color = LocalTokens.current.text,
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
|
|
@ -137,7 +136,7 @@ fun NowPlayingBar(
|
|||
Icon(
|
||||
imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
|
||||
contentDescription = if (isPlaying) "Pause" else "Play",
|
||||
tint = PremiumPurple,
|
||||
tint = LocalTokens.current.accentFill,
|
||||
modifier = Modifier.size(28.dp),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,17 +89,10 @@ import androidx.compose.ui.unit.sp
|
|||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.rada.domain.model.Station
|
||||
import com.rada.player.PlaybackManager
|
||||
import com.rada.ui.theme.LocalTokens
|
||||
import com.rada.ui.theme.GradientEnd
|
||||
import com.rada.ui.theme.GradientMid
|
||||
import com.rada.ui.theme.GradientStart
|
||||
import com.rada.ui.theme.PremiumCyan
|
||||
import com.rada.ui.theme.PremiumPurple
|
||||
import com.rada.ui.theme.PremiumPurpleLight
|
||||
import com.rada.ui.theme.PremiumSurface
|
||||
import com.rada.ui.theme.PremiumSurfaceVariant
|
||||
import com.rada.ui.theme.PremiumText
|
||||
import com.rada.ui.theme.PremiumTextSecondary
|
||||
import com.rada.ui.theme.PremiumBorder
|
||||
|
||||
@Composable
|
||||
fun PlayerScreen(
|
||||
|
|
@ -195,7 +188,7 @@ fun PlayerScreen(
|
|||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
bottomBar = {
|
||||
BottomAppBar(
|
||||
containerColor = PremiumSurface.copy(alpha = 0.94f),
|
||||
containerColor = LocalTokens.current.panel.copy(alpha = 0.94f),
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
) {
|
||||
NavigationBarItem(
|
||||
|
|
@ -287,7 +280,7 @@ private fun EmptyStateView(
|
|||
text = "Find your sound",
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = PremiumText,
|
||||
color = LocalTokens.current.text,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
|
|
@ -296,7 +289,7 @@ private fun EmptyStateView(
|
|||
Text(
|
||||
text = "Choose a station or add music\nto start listening",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = PremiumTextSecondary,
|
||||
color = LocalTokens.current.textSecondary,
|
||||
textAlign = TextAlign.Center,
|
||||
lineHeight = 24.sp,
|
||||
)
|
||||
|
|
@ -310,7 +303,7 @@ private fun EmptyStateView(
|
|||
.height(56.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = PremiumPurple,
|
||||
containerColor = LocalTokens.current.accentFill,
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
|
|
@ -335,7 +328,7 @@ private fun EmptyStateView(
|
|||
.height(56.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = PremiumPurpleLight,
|
||||
contentColor = LocalTokens.current.accentText,
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
|
|
@ -355,7 +348,8 @@ private fun EmptyStateView(
|
|||
|
||||
@Composable
|
||||
private fun WaveGraphic(modifier: Modifier = Modifier) {
|
||||
val gradientColors = listOf(GradientStart, GradientMid, GradientEnd)
|
||||
val gradientColors = listOf(LocalTokens.current.accentFill, GradientMid, GradientEnd)
|
||||
val accentGlow = LocalTokens.current.accentFill
|
||||
|
||||
Canvas(modifier = modifier) {
|
||||
val width = size.width
|
||||
|
|
@ -397,7 +391,7 @@ private fun WaveGraphic(modifier: Modifier = Modifier) {
|
|||
// Draw subtle glow at center
|
||||
drawCircle(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(PremiumPurple.copy(alpha = 0.15f), Color.Transparent),
|
||||
colors = listOf(accentGlow.copy(alpha = 0.15f), Color.Transparent),
|
||||
center = Offset(width / 2, centerY),
|
||||
radius = width / 3,
|
||||
),
|
||||
|
|
@ -583,12 +577,12 @@ private fun PlaylistPickerDialog(
|
|||
Icon(Icons.Default.Search, contentDescription = "Search")
|
||||
},
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedContainerColor = PremiumSurface,
|
||||
unfocusedContainerColor = PremiumSurface,
|
||||
focusedBorderColor = PremiumPurple.copy(alpha = 0.5f),
|
||||
unfocusedBorderColor = PremiumBorder,
|
||||
focusedTextColor = PremiumText,
|
||||
unfocusedTextColor = PremiumText,
|
||||
focusedContainerColor = LocalTokens.current.panel,
|
||||
unfocusedContainerColor = LocalTokens.current.panel,
|
||||
focusedBorderColor = LocalTokens.current.accentFill.copy(alpha = 0.5f),
|
||||
unfocusedBorderColor = LocalTokens.current.border,
|
||||
focusedTextColor = LocalTokens.current.text,
|
||||
unfocusedTextColor = LocalTokens.current.text,
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
)
|
||||
|
|
@ -617,7 +611,7 @@ private fun PlaylistPickerDialog(
|
|||
Text(
|
||||
text = playlist.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = PremiumText,
|
||||
color = LocalTokens.current.text,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -626,14 +620,14 @@ private fun PlaylistPickerDialog(
|
|||
Text(
|
||||
text = "No matching playlists",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = PremiumTextSecondary,
|
||||
color = LocalTokens.current.textSecondary,
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "No playlists yet. Create one below.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = PremiumTextSecondary,
|
||||
color = LocalTokens.current.textSecondary,
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
|
|
@ -648,10 +642,10 @@ private fun PlaylistPickerDialog(
|
|||
label = { Text("Playlist name") },
|
||||
singleLine = true,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = PremiumPurple,
|
||||
unfocusedBorderColor = PremiumBorder,
|
||||
focusedTextColor = PremiumText,
|
||||
unfocusedTextColor = PremiumText,
|
||||
focusedBorderColor = LocalTokens.current.accentFill,
|
||||
unfocusedBorderColor = LocalTokens.current.border,
|
||||
focusedTextColor = LocalTokens.current.text,
|
||||
unfocusedTextColor = LocalTokens.current.text,
|
||||
),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
|
@ -701,7 +695,7 @@ private fun SaveSongButton(
|
|||
saveState == PlaybackManager.SaveState.SAVING -> MaterialTheme.colorScheme.secondary
|
||||
saveState == PlaybackManager.SaveState.SUCCESS -> Color(0xFF10B981)
|
||||
saveState == PlaybackManager.SaveState.ERROR -> MaterialTheme.colorScheme.error
|
||||
canSave -> PremiumPurple
|
||||
canSave -> LocalTokens.current.accentFill
|
||||
else -> MaterialTheme.colorScheme.surfaceVariant
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,13 +57,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.rada.data.local.entity.PlaylistEntity
|
||||
import com.rada.ui.theme.PremiumBorder
|
||||
import com.rada.ui.theme.PremiumPurple
|
||||
import com.rada.ui.theme.PremiumPurpleLight
|
||||
import com.rada.ui.theme.PremiumSurface
|
||||
import com.rada.ui.theme.PremiumSurfaceVariant
|
||||
import com.rada.ui.theme.PremiumText
|
||||
import com.rada.ui.theme.PremiumTextSecondary
|
||||
import com.rada.ui.theme.LocalTokens
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
@ -176,12 +170,12 @@ fun PlaylistManagerScreen(
|
|||
Icon(Icons.Default.Search, contentDescription = "Search")
|
||||
},
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedContainerColor = PremiumSurface,
|
||||
unfocusedContainerColor = PremiumSurface,
|
||||
focusedBorderColor = PremiumPurple.copy(alpha = 0.5f),
|
||||
unfocusedBorderColor = PremiumBorder,
|
||||
focusedTextColor = PremiumText,
|
||||
unfocusedTextColor = PremiumText,
|
||||
focusedContainerColor = LocalTokens.current.panel,
|
||||
unfocusedContainerColor = LocalTokens.current.panel,
|
||||
focusedBorderColor = LocalTokens.current.accentFill.copy(alpha = 0.5f),
|
||||
unfocusedBorderColor = LocalTokens.current.border,
|
||||
focusedTextColor = LocalTokens.current.text,
|
||||
unfocusedTextColor = LocalTokens.current.text,
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
)
|
||||
|
|
@ -194,22 +188,22 @@ fun PlaylistManagerScreen(
|
|||
DropdownMenuItem(
|
||||
text = { Text("Name A–Z") },
|
||||
onClick = { viewModel.setSortOrder(PlaylistSortOrder.NAME_ASC); showSortMenu = false },
|
||||
leadingIcon = { if (sortOrder == PlaylistSortOrder.NAME_ASC) Icon(Icons.Default.Search, null, tint = PremiumPurple) else null },
|
||||
leadingIcon = { if (sortOrder == PlaylistSortOrder.NAME_ASC) Icon(Icons.Default.Search, null, tint = LocalTokens.current.accentFill) else null },
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Name Z–A") },
|
||||
onClick = { viewModel.setSortOrder(PlaylistSortOrder.NAME_DESC); showSortMenu = false },
|
||||
leadingIcon = { if (sortOrder == PlaylistSortOrder.NAME_DESC) Icon(Icons.Default.Search, null, tint = PremiumPurple) else null },
|
||||
leadingIcon = { if (sortOrder == PlaylistSortOrder.NAME_DESC) Icon(Icons.Default.Search, null, tint = LocalTokens.current.accentFill) else null },
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Newest first") },
|
||||
onClick = { viewModel.setSortOrder(PlaylistSortOrder.DATE_NEWEST); showSortMenu = false },
|
||||
leadingIcon = { if (sortOrder == PlaylistSortOrder.DATE_NEWEST) Icon(Icons.Default.Search, null, tint = PremiumPurple) else null },
|
||||
leadingIcon = { if (sortOrder == PlaylistSortOrder.DATE_NEWEST) Icon(Icons.Default.Search, null, tint = LocalTokens.current.accentFill) else null },
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Oldest first") },
|
||||
onClick = { viewModel.setSortOrder(PlaylistSortOrder.DATE_OLDEST); showSortMenu = false },
|
||||
leadingIcon = { if (sortOrder == PlaylistSortOrder.DATE_OLDEST) Icon(Icons.Default.Search, null, tint = PremiumPurple) else null },
|
||||
leadingIcon = { if (sortOrder == PlaylistSortOrder.DATE_OLDEST) Icon(Icons.Default.Search, null, tint = LocalTokens.current.accentFill) else null },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -224,13 +218,13 @@ fun PlaylistManagerScreen(
|
|||
Icons.AutoMirrored.Filled.PlaylistPlay,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = PremiumPurple.copy(alpha = 0.3f),
|
||||
tint = LocalTokens.current.accentFill.copy(alpha = 0.3f),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = if (searchQuery.isNotBlank()) "No matching playlists" else "No playlists yet",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = PremiumTextSecondary,
|
||||
color = LocalTokens.current.textSecondary,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TextButton(
|
||||
|
|
@ -287,7 +281,7 @@ private fun PlaylistListItem(
|
|||
Icon(
|
||||
Icons.AutoMirrored.Filled.PlaylistPlay,
|
||||
contentDescription = null,
|
||||
tint = PremiumPurple,
|
||||
tint = LocalTokens.current.accentFill,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
|
@ -296,14 +290,14 @@ private fun PlaylistListItem(
|
|||
text = playlist.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = PremiumText,
|
||||
color = LocalTokens.current.text,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onRename) {
|
||||
Icon(
|
||||
Icons.Default.Edit,
|
||||
contentDescription = "Rename",
|
||||
tint = PremiumTextSecondary,
|
||||
tint = LocalTokens.current.textSecondary,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onDelete) {
|
||||
|
|
@ -334,10 +328,10 @@ private fun CreatePlaylistDialog(
|
|||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = PremiumPurple,
|
||||
unfocusedBorderColor = PremiumBorder,
|
||||
focusedTextColor = PremiumText,
|
||||
unfocusedTextColor = PremiumText,
|
||||
focusedBorderColor = LocalTokens.current.accentFill,
|
||||
unfocusedBorderColor = LocalTokens.current.border,
|
||||
focusedTextColor = LocalTokens.current.text,
|
||||
unfocusedTextColor = LocalTokens.current.text,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
@ -376,10 +370,10 @@ private fun RenamePlaylistDialog(
|
|||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = PremiumPurple,
|
||||
unfocusedBorderColor = PremiumBorder,
|
||||
focusedTextColor = PremiumText,
|
||||
unfocusedTextColor = PremiumText,
|
||||
focusedBorderColor = LocalTokens.current.accentFill,
|
||||
unfocusedBorderColor = LocalTokens.current.border,
|
||||
focusedTextColor = LocalTokens.current.text,
|
||||
unfocusedTextColor = LocalTokens.current.text,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -52,13 +52,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.rada.domain.model.Station
|
||||
import com.rada.ui.theme.PremiumBorder
|
||||
import com.rada.ui.theme.PremiumPurple
|
||||
import com.rada.ui.theme.PremiumPurpleLight
|
||||
import com.rada.ui.theme.PremiumSurface
|
||||
import com.rada.ui.theme.PremiumSurfaceVariant
|
||||
import com.rada.ui.theme.PremiumText
|
||||
import com.rada.ui.theme.PremiumTextSecondary
|
||||
import com.rada.ui.theme.LocalTokens
|
||||
|
||||
private val genreColors = mapOf(
|
||||
"Lofi" to 0xFF7C3AED,
|
||||
|
|
@ -154,12 +148,12 @@ fun StationBrowserScreen(
|
|||
Icon(Icons.Default.Search, contentDescription = "Search")
|
||||
},
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedContainerColor = PremiumSurface,
|
||||
unfocusedContainerColor = PremiumSurface,
|
||||
focusedBorderColor = PremiumPurple.copy(alpha = 0.5f),
|
||||
unfocusedBorderColor = PremiumBorder,
|
||||
focusedTextColor = PremiumText,
|
||||
unfocusedTextColor = PremiumText,
|
||||
focusedContainerColor = LocalTokens.current.panel,
|
||||
unfocusedContainerColor = LocalTokens.current.panel,
|
||||
focusedBorderColor = LocalTokens.current.accentFill.copy(alpha = 0.5f),
|
||||
unfocusedBorderColor = LocalTokens.current.border,
|
||||
focusedTextColor = LocalTokens.current.text,
|
||||
unfocusedTextColor = LocalTokens.current.text,
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
)
|
||||
|
|
@ -222,10 +216,10 @@ private fun AddStationDialog(
|
|||
label = { Text("Station name") },
|
||||
singleLine = true,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = PremiumPurple,
|
||||
unfocusedBorderColor = PremiumBorder,
|
||||
focusedTextColor = PremiumText,
|
||||
unfocusedTextColor = PremiumText,
|
||||
focusedBorderColor = LocalTokens.current.accentFill,
|
||||
unfocusedBorderColor = LocalTokens.current.border,
|
||||
focusedTextColor = LocalTokens.current.text,
|
||||
unfocusedTextColor = LocalTokens.current.text,
|
||||
),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
|
@ -237,10 +231,10 @@ private fun AddStationDialog(
|
|||
placeholder = { Text("https://...") },
|
||||
singleLine = true,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = PremiumPurple,
|
||||
unfocusedBorderColor = PremiumBorder,
|
||||
focusedTextColor = PremiumText,
|
||||
unfocusedTextColor = PremiumText,
|
||||
focusedBorderColor = LocalTokens.current.accentFill,
|
||||
unfocusedBorderColor = LocalTokens.current.border,
|
||||
focusedTextColor = LocalTokens.current.text,
|
||||
unfocusedTextColor = LocalTokens.current.text,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -291,10 +285,10 @@ private fun RenameStationDialog(
|
|||
label = { Text("Station name") },
|
||||
singleLine = true,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = PremiumPurple,
|
||||
unfocusedBorderColor = PremiumBorder,
|
||||
focusedTextColor = PremiumText,
|
||||
unfocusedTextColor = PremiumText,
|
||||
focusedBorderColor = LocalTokens.current.accentFill,
|
||||
unfocusedBorderColor = LocalTokens.current.border,
|
||||
focusedTextColor = LocalTokens.current.text,
|
||||
unfocusedTextColor = LocalTokens.current.text,
|
||||
),
|
||||
)
|
||||
if (isRenamed && originalName != null) {
|
||||
|
|
@ -302,7 +296,7 @@ private fun RenameStationDialog(
|
|||
Text(
|
||||
text = "Renamed from \"$originalName\"",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = PremiumTextSecondary,
|
||||
color = LocalTokens.current.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -360,7 +354,7 @@ private fun GenreHeader(genre: String, count: Int) {
|
|||
Text(
|
||||
text = "$count",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = PremiumTextSecondary,
|
||||
color = LocalTokens.current.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -379,7 +373,7 @@ private fun StationCard(
|
|||
.padding(horizontal = 12.dp)
|
||||
.clickable(onClick = onClick),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = PremiumSurfaceVariant,
|
||||
containerColor = LocalTokens.current.glassFillStrong,
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
) {
|
||||
|
|
@ -393,13 +387,13 @@ private fun StationCard(
|
|||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.background(PremiumPurple.copy(alpha = 0.15f)),
|
||||
.background(LocalTokens.current.accentFill.copy(alpha = 0.15f)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Radio,
|
||||
contentDescription = null,
|
||||
tint = PremiumPurpleLight,
|
||||
tint = LocalTokens.current.accentText,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
|
|
@ -409,13 +403,13 @@ private fun StationCard(
|
|||
text = station.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = PremiumText,
|
||||
color = LocalTokens.current.text,
|
||||
)
|
||||
if (station.genre.isNotEmpty()) {
|
||||
Text(
|
||||
text = station.genre,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = PremiumTextSecondary,
|
||||
color = LocalTokens.current.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -423,7 +417,7 @@ private fun StationCard(
|
|||
Icon(
|
||||
Icons.Default.Edit,
|
||||
contentDescription = "Rename",
|
||||
tint = PremiumTextSecondary,
|
||||
tint = LocalTokens.current.textSecondary,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
|
|
@ -432,7 +426,7 @@ private fun StationCard(
|
|||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Remove",
|
||||
tint = PremiumTextSecondary,
|
||||
tint = LocalTokens.current.textSecondary,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,13 +55,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.rada.domain.model.StreamEntry
|
||||
import com.rada.ui.theme.PremiumBorder
|
||||
import com.rada.ui.theme.PremiumPurple
|
||||
import com.rada.ui.theme.PremiumPurpleLight
|
||||
import com.rada.ui.theme.PremiumSurface
|
||||
import com.rada.ui.theme.PremiumSurfaceVariant
|
||||
import com.rada.ui.theme.PremiumText
|
||||
import com.rada.ui.theme.PremiumTextSecondary
|
||||
import com.rada.ui.theme.LocalTokens
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
@ -140,12 +134,12 @@ fun StreamsScreen(
|
|||
Icon(Icons.Default.Search, contentDescription = "Search")
|
||||
},
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedContainerColor = PremiumSurface,
|
||||
unfocusedContainerColor = PremiumSurface,
|
||||
focusedBorderColor = PremiumPurple.copy(alpha = 0.5f),
|
||||
unfocusedBorderColor = PremiumBorder,
|
||||
focusedTextColor = PremiumText,
|
||||
unfocusedTextColor = PremiumText,
|
||||
focusedContainerColor = LocalTokens.current.panel,
|
||||
unfocusedContainerColor = LocalTokens.current.panel,
|
||||
focusedBorderColor = LocalTokens.current.accentFill.copy(alpha = 0.5f),
|
||||
unfocusedBorderColor = LocalTokens.current.border,
|
||||
focusedTextColor = LocalTokens.current.text,
|
||||
unfocusedTextColor = LocalTokens.current.text,
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
)
|
||||
|
|
@ -163,14 +157,14 @@ fun StreamsScreen(
|
|||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(16.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = PremiumPurple,
|
||||
color = LocalTokens.current.accentFill,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
}
|
||||
Text(
|
||||
text = statusText,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (error != null) Color(0xFFEF4444) else PremiumTextSecondary,
|
||||
color = if (error != null) Color(0xFFEF4444) else LocalTokens.current.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -185,20 +179,20 @@ fun StreamsScreen(
|
|||
Icon(
|
||||
Icons.Default.Radio,
|
||||
contentDescription = null,
|
||||
tint = PremiumTextSecondary,
|
||||
tint = LocalTokens.current.textSecondary,
|
||||
modifier = Modifier.size(48.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = "No streams yet",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = PremiumText,
|
||||
color = LocalTokens.current.text,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Text(
|
||||
text = "Tap + to add a stream URL or an .m3u playlist",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = PremiumTextSecondary,
|
||||
color = LocalTokens.current.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -262,10 +256,10 @@ private fun AddStreamDialog(
|
|||
placeholder = { Text("https://example.com/stream.m3u") },
|
||||
singleLine = true,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = PremiumPurple,
|
||||
unfocusedBorderColor = PremiumBorder,
|
||||
focusedTextColor = PremiumText,
|
||||
unfocusedTextColor = PremiumText,
|
||||
focusedBorderColor = LocalTokens.current.accentFill,
|
||||
unfocusedBorderColor = LocalTokens.current.border,
|
||||
focusedTextColor = LocalTokens.current.text,
|
||||
unfocusedTextColor = LocalTokens.current.text,
|
||||
),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
|
@ -276,17 +270,17 @@ private fun AddStreamDialog(
|
|||
label = { Text("Display name (optional)") },
|
||||
singleLine = true,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = PremiumPurple,
|
||||
unfocusedBorderColor = PremiumBorder,
|
||||
focusedTextColor = PremiumText,
|
||||
unfocusedTextColor = PremiumText,
|
||||
focusedBorderColor = LocalTokens.current.accentFill,
|
||||
unfocusedBorderColor = LocalTokens.current.border,
|
||||
focusedTextColor = LocalTokens.current.text,
|
||||
unfocusedTextColor = LocalTokens.current.text,
|
||||
),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Paste a direct stream URL, or an .m3u/.pls playlist to add all of its streams.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = PremiumTextSecondary,
|
||||
color = LocalTokens.current.textSecondary,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
@ -332,10 +326,10 @@ private fun RenameStreamDialog(
|
|||
label = { Text("Display name") },
|
||||
singleLine = true,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = PremiumPurple,
|
||||
unfocusedBorderColor = PremiumBorder,
|
||||
focusedTextColor = PremiumText,
|
||||
unfocusedTextColor = PremiumText,
|
||||
focusedBorderColor = LocalTokens.current.accentFill,
|
||||
unfocusedBorderColor = LocalTokens.current.border,
|
||||
focusedTextColor = LocalTokens.current.text,
|
||||
unfocusedTextColor = LocalTokens.current.text,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
@ -367,20 +361,20 @@ private fun GroupHeader(group: String, count: Int) {
|
|||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.clip(CircleShape)
|
||||
.background(PremiumPurple),
|
||||
.background(LocalTokens.current.accentFill),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(
|
||||
text = group.uppercase(),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = PremiumPurpleLight,
|
||||
color = LocalTokens.current.accentText,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "$count",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = PremiumTextSecondary,
|
||||
color = LocalTokens.current.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -399,7 +393,7 @@ private fun StreamCard(
|
|||
.padding(horizontal = 12.dp)
|
||||
.clickable(onClick = onClick),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isPlaying) PremiumPurple.copy(alpha = 0.12f) else PremiumSurfaceVariant,
|
||||
containerColor = if (isPlaying) LocalTokens.current.accentFill.copy(alpha = 0.12f) else LocalTokens.current.glassFillStrong,
|
||||
),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
) {
|
||||
|
|
@ -413,13 +407,13 @@ private fun StreamCard(
|
|||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.background(if (isPlaying) PremiumPurple else PremiumPurple.copy(alpha = 0.15f)),
|
||||
.background(if (isPlaying) LocalTokens.current.accentFill else LocalTokens.current.accentFill.copy(alpha = 0.15f)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
if (isPlaying) Icons.Default.Stop else Icons.Default.PlayArrow,
|
||||
contentDescription = null,
|
||||
tint = if (isPlaying) Color.White else PremiumPurpleLight,
|
||||
tint = if (isPlaying) Color.White else LocalTokens.current.accentText,
|
||||
modifier = Modifier.size(22.dp),
|
||||
)
|
||||
}
|
||||
|
|
@ -429,12 +423,12 @@ private fun StreamCard(
|
|||
text = entry.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = PremiumText,
|
||||
color = LocalTokens.current.text,
|
||||
)
|
||||
Text(
|
||||
text = entry.url,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = PremiumTextSecondary,
|
||||
color = LocalTokens.current.textSecondary,
|
||||
maxLines = 1,
|
||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||
)
|
||||
|
|
@ -443,7 +437,7 @@ private fun StreamCard(
|
|||
Icon(
|
||||
Icons.Default.Edit,
|
||||
contentDescription = "Rename",
|
||||
tint = PremiumTextSecondary,
|
||||
tint = LocalTokens.current.textSecondary,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
|
|
@ -451,7 +445,7 @@ private fun StreamCard(
|
|||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Remove",
|
||||
tint = PremiumTextSecondary,
|
||||
tint = LocalTokens.current.textSecondary,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
155
app/src/main/java/com/rada/ui/theme/AppearanceDialog.kt
Normal file
155
app/src/main/java/com/rada/ui/theme/AppearanceDialog.kt
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
package com.rada.ui.theme
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/** Static swatch preview colors for the picker (independent of active theme). */
|
||||
internal val AccentSwatches = mapOf(
|
||||
AccentMode.VIOLET to Color(0xFF7C4DFF),
|
||||
AccentMode.BLUE to Color(0xFF4C7DFF),
|
||||
AccentMode.GREEN to Color(0xFF34C759),
|
||||
AccentMode.ORANGE to Color(0xFFF97316),
|
||||
)
|
||||
|
||||
/**
|
||||
* Appearance sheet: accent color (violet/blue/green/orange) + brightness
|
||||
* (System / Dark "black+white" / Light "white+black"). Persists via UiPrefs;
|
||||
* the whole UI recomposes live through LocalTokens.
|
||||
*/
|
||||
@Composable
|
||||
fun AppearanceDialog(onDismiss: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val tokens = LocalTokens.current
|
||||
val lightMode = !MaterialTheme.colorScheme.background.luminanceIsDark()
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
containerColor = tokens.panel,
|
||||
titleContentColor = tokens.text,
|
||||
textContentColor = tokens.textSecondary,
|
||||
shape = RoundedCornerShape(SatsAmpGlass.rCard),
|
||||
title = { Text("Appearance", fontWeight = FontWeight.Bold) },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
"ACCENT",
|
||||
fontSize = 12.sp,
|
||||
letterSpacing = 0.4.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = tokens.textMuted,
|
||||
)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||
AccentMode.entries.forEach { mode ->
|
||||
val selected = UiPrefs.accent == mode
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.clip(CircleShape)
|
||||
.background(AccentSwatches.getValue(mode))
|
||||
.then(
|
||||
if (selected) Modifier.border(2.dp, tokens.text, CircleShape)
|
||||
else Modifier.border(1.dp, tokens.borderInteractive, CircleShape)
|
||||
)
|
||||
.clickable { UiPrefs.setAccent(mode, context) },
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (selected) {
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = "${mode.label} accent selected",
|
||||
tint = if (mode == AccentMode.GREEN || mode == AccentMode.ORANGE) Color(0xFF141419) else Color.White,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(22.dp))
|
||||
Text(
|
||||
"MODE",
|
||||
fontSize = 12.sp,
|
||||
letterSpacing = 0.4.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = tokens.textMuted,
|
||||
)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
AppMode.entries.forEach { mode ->
|
||||
val selected = UiPrefs.mode == mode
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(SatsAmpGlass.rInput))
|
||||
.background(if (selected) tokens.accentSoft else Color.Transparent)
|
||||
.border(
|
||||
1.dp,
|
||||
if (selected) tokens.accentFill else tokens.borderInteractive,
|
||||
RoundedCornerShape(SatsAmpGlass.rInput),
|
||||
)
|
||||
.clickable { UiPrefs.setMode(mode, context) }
|
||||
.padding(vertical = 10.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
mode.label,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = if (selected) FontWeight.Bold else FontWeight.Medium,
|
||||
color = if (selected) tokens.accentText else tokens.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(14.dp))
|
||||
Text(
|
||||
when (UiPrefs.mode) {
|
||||
AppMode.LIGHT -> if (lightMode) "White background, black text." else "Applied after reopen if the system keeps dark mode."
|
||||
AppMode.DARK -> "Black background, white text."
|
||||
AppMode.SYSTEM -> "Follows your system dark-mode setting."
|
||||
},
|
||||
fontSize = 12.sp,
|
||||
color = tokens.textMuted,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Done", color = tokens.accentText, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun Color.luminanceIsDark(): Boolean {
|
||||
fun f(c: Float) = if (c <= 0.04045f) c / 12.92f else Math.pow(((c + 0.055) / 1.055).toDouble(), 2.4).toFloat()
|
||||
val l = 0.2126f * f(red) + 0.7152f * f(green) + 0.0722f * f(blue)
|
||||
return l < 0.45f
|
||||
}
|
||||
|
|
@ -1,46 +1,32 @@
|
|||
package com.rada.ui.theme
|
||||
|
||||
import com.rada.ui.theme.LocalTokens
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
// ── RadAmp Glass palette (see design/glass-radio-theme.css for tokens + WCAG data) ──
|
||||
// Base: flat dark charcoal gradient #23232B → #1B1B22
|
||||
val PremiumBlack = Color(0xFF1B1B22) // base bottom
|
||||
val PremiumDark = Color(0xFF23232B) // base top
|
||||
val PremiumSurface = Color(0xFF202029) // solid panel (~90% opaque base)
|
||||
val PremiumSurfaceVariant = Color(0xFF262631)
|
||||
val PremiumBorder = Color(0x1AFFFFFF) // glass border 10% white (decorative)
|
||||
// Interactive borders need 3:1 (WCAG 1.4.11): 36% white over glass
|
||||
val PremiumBorderInteractive = Color(0x5CFFFFFF)
|
||||
// ── SatsAmp fixed palette ──
|
||||
// User-configurable colors live in UiTokens (Tokens.kt) and are read via
|
||||
// LocalTokens.current at call sites. Only mode-independent constants here.
|
||||
|
||||
val PremiumPurple = Color(0xFF7C4DFF) // accent
|
||||
val PremiumPurpleHover = Color(0xFF8E66FF)
|
||||
val PremiumPurplePressed = Color(0xFF6A3DE0)
|
||||
val PremiumPurpleSoft = Color(0x297C4DFF) // accent soft fill 16% — chips/active
|
||||
val PremiumPurpleLight = Color(0xFFA78BFA)
|
||||
// AA-safe accent TEXT on glass (7C4DFF is only 2.71:1 on glass)
|
||||
val PremiumAccentText = Color(0xFFA78BFF)
|
||||
val PremiumBlack = Color(0xFF1B1B22)
|
||||
val BaseTop = Color(0xFF23232B)
|
||||
val BaseBottom = Color(0xFF1B1B22)
|
||||
val PremiumSurfaceSolid = Color(0xFF202029)
|
||||
|
||||
val PremiumError = Color(0xFFEF4444)
|
||||
val PremiumWarning = Color(0xFFF59E0B)
|
||||
|
||||
// Fixed brand colors (not user-configurable)
|
||||
val PremiumBlue = Color(0xFF3B82F6)
|
||||
val PremiumCyan = Color(0xFF06B6D4)
|
||||
val PremiumPink = Color(0xFFEC4899)
|
||||
|
||||
// Text tokens: all pairs ≥4.5:1 on base AND on glass (measured; see demo page)
|
||||
val PremiumText = Color(0xFFFFFFFF) // 13.1:1 on glass
|
||||
val PremiumTextSecondary = Color(0xFFD9D9DE) // 10.5:1 on glass (body 88% white)
|
||||
val PremiumTextMuted = Color(0xFF8C8C96) // 5.1:1 on base (caption floor, never dimmer)
|
||||
|
||||
val PremiumSuccess = Color(0xFF34C759)
|
||||
val PremiumError = Color(0xFFEF4444)
|
||||
val PremiumWarning = Color(0xFFF59E0B)
|
||||
|
||||
// Gradient colors for accents
|
||||
val GradientStart = Color(0xFF7C4DFF)
|
||||
// Accent-following gradient stops resolve to the active accent at composition
|
||||
val GradientMid = Color(0xFF3B82F6)
|
||||
val GradientEnd = Color(0xFF06B6D4)
|
||||
// Base background gradient (flat charcoal, very subtle)
|
||||
val BaseTop = Color(0xFF23232B)
|
||||
val BaseBottom = Color(0xFF1B1B22)
|
||||
// Live indicator
|
||||
val LiveGreen = Color(0xFF34C759)
|
||||
|
||||
// Legacy aliases (fixed violet) — for non-composable contexts only
|
||||
val PremiumPurpleStatic = Color(0xFF7C4DFF)
|
||||
|
||||
|
||||
// Legacy terminal palette (cliamp-inspired)
|
||||
val TerminalBlack = Color(0xFF000000)
|
||||
|
|
@ -64,7 +50,7 @@ val TerminalCyan = Color(0xFF00D4FF)
|
|||
val TerminalMagenta = Color(0xFFFF55FF)
|
||||
|
||||
// Legacy aliases used by existing code
|
||||
val SaveButton = PremiumSuccess
|
||||
val SaveButtonDisabled = PremiumSurfaceVariant
|
||||
val SaveConfirm = PremiumSuccess
|
||||
val SaveButton = Color(0xFF34C759)
|
||||
val SaveButtonDisabled = Color(0xFF262631)
|
||||
val SaveConfirm = Color(0xFF34C759)
|
||||
val ErrorRed = PremiumError
|
||||
|
|
|
|||
|
|
@ -1,106 +1,145 @@
|
|||
package com.rada.ui.theme
|
||||
|
||||
import com.rada.ui.theme.LocalTokens
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
private val PremiumColorScheme = darkColorScheme(
|
||||
primary = PremiumPurple,
|
||||
onPrimary = Color.White, // 4.81:1 on #7C4DFF
|
||||
primaryContainer = PremiumPurpleSoft,
|
||||
onPrimaryContainer = PremiumAccentText, // AA-safe accent text on glass
|
||||
val LocalTokens = staticCompositionLocalOf { darkTokens(AccentMode.VIOLET) }
|
||||
|
||||
secondary = PremiumBlue,
|
||||
onSecondary = Color.White,
|
||||
secondaryContainer = PremiumBlue.copy(alpha = 0.15f),
|
||||
onSecondaryContainer = PremiumBlue,
|
||||
|
||||
tertiary = PremiumCyan,
|
||||
onTertiary = Color.White,
|
||||
tertiaryContainer = PremiumCyan.copy(alpha = 0.15f),
|
||||
onTertiaryContainer = PremiumCyan,
|
||||
|
||||
background = BaseBottom, // charcoal gradient bottom
|
||||
onBackground = PremiumText,
|
||||
|
||||
surface = PremiumDark, // base top
|
||||
onSurface = PremiumText,
|
||||
surfaceVariant = PremiumSurface, // solid panel
|
||||
onSurfaceVariant = PremiumTextSecondary,
|
||||
|
||||
error = PremiumError,
|
||||
onError = Color.White,
|
||||
errorContainer = PremiumError.copy(alpha = 0.15f),
|
||||
onErrorContainer = PremiumError,
|
||||
|
||||
outline = PremiumBorder, // decorative 10% white glass border
|
||||
outlineVariant = PremiumBorder.copy(alpha = 0.5f),
|
||||
inverseSurface = PremiumText,
|
||||
inverseOnSurface = BaseBottom,
|
||||
inversePrimary = PremiumAccentText,
|
||||
surfaceTint = PremiumPurple,
|
||||
)
|
||||
@Composable
|
||||
fun SatsAmpTheme(
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val dark = when (UiPrefs.mode) {
|
||||
AppMode.DARK -> true
|
||||
AppMode.LIGHT -> false
|
||||
AppMode.SYSTEM -> isSystemInDarkTheme()
|
||||
}
|
||||
val tok = tokensFor(UiPrefs.accent, dark)
|
||||
val schemeArgs = SchemeArgs(
|
||||
primary = tok.accentFill,
|
||||
onPrimary = tok.accentOnFill,
|
||||
primaryContainer = tok.accentSoft,
|
||||
onPrimaryContainer = tok.accentText,
|
||||
secondary = tok.accentText,
|
||||
onSecondary = tok.text,
|
||||
background = tok.bgBottom,
|
||||
onBackground = tok.text,
|
||||
surface = tok.bgTop,
|
||||
onSurface = tok.text,
|
||||
surfaceVariant = tok.panel,
|
||||
onSurfaceVariant = tok.textSecondary,
|
||||
error = PremiumError,
|
||||
onError = Color.White,
|
||||
outline = tok.border,
|
||||
outlineVariant = tok.borderInteractive,
|
||||
inverseSurface = tok.text,
|
||||
inverseOnSurface = tok.bgBottom,
|
||||
inversePrimary = tok.accentText,
|
||||
surfaceTint = tok.accentFill,
|
||||
)
|
||||
val colorScheme = if (dark) buildDark(schemeArgs) else buildLight(schemeArgs)
|
||||
CompositionLocalProvider(LocalTokens provides tok) {
|
||||
MaterialTheme(colorScheme = colorScheme, content = content)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RadAmp Glass tokens — mirrors design/glass-radio-theme.css.
|
||||
* SatsAmp Glass tokens — mirrors design/glass-radio-theme.css.
|
||||
* Compose has no backdrop blur without a lib, so "glass" = translucent white
|
||||
* layer + inner highlight border over the charcoal base. Text never sits on
|
||||
* glass alone without the 90%-opaque overlay (GlassTextSafe).
|
||||
* glass alone without the 90%-opaque overlay (textSafe).
|
||||
*/
|
||||
object RadAmpGlass {
|
||||
val bgGradient = Brush.verticalGradient(listOf(BaseTop, BaseBottom))
|
||||
// very subtle violet glows, painted under content, never behind text
|
||||
val glowTop = Brush.radialGradient(
|
||||
colors = listOf(PremiumPurple.copy(alpha = 0.14f), Color.Transparent),
|
||||
center = androidx.compose.ui.geometry.Offset(900f, 260f),
|
||||
radius = 900f,
|
||||
)
|
||||
val surface = Color(0x0FFFFFFF) // rgba(255,255,255,0.06)
|
||||
val surfaceStrong = Color(0x17FFFFFF) // hover 0.09
|
||||
val border = Color(0x1AFFFFFF) // 0.10 decorative
|
||||
val borderInteractive = Color(0x5CFFFFFF)// 0.36 → 3:1 on glass
|
||||
val highlight = Color(0x24FFFFFF) // inset top highlight 0.14
|
||||
val textSafe = Color(0xE61B1B22) // 90% opaque overlay behind text
|
||||
object SatsAmpGlass {
|
||||
val rCard = 20.dp
|
||||
val rInput = 14.dp
|
||||
}
|
||||
|
||||
/** App background: charcoal (or paper) gradient + accent glow UNDER all content. */
|
||||
@Composable
|
||||
fun GlassBackground(content: @Composable () -> Unit) {
|
||||
Box(Modifier.fillMaxSize().background(RadAmpGlass.bgGradient)) {
|
||||
Box(Modifier.fillMaxSize().background(RadAmpGlass.glowTop))
|
||||
val tok = LocalTokens.current
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(Brush.verticalGradient(listOf(tok.bgTop, tok.bgBottom))),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(tok.glow, Color.Transparent),
|
||||
center = androidx.compose.ui.geometry.Offset(900f, 260f),
|
||||
radius = 900f,
|
||||
),
|
||||
),
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
object RadAmpGradients {
|
||||
/** Gradient helpers that follow the active accent. */
|
||||
object SatsAmpGradients {
|
||||
val primary = Brush.linearGradient(
|
||||
colors = listOf(GradientStart, GradientMid, GradientEnd)
|
||||
colors = listOf(PremiumPurpleStatic, GradientMid, GradientEnd)
|
||||
)
|
||||
val primaryHorizontal = Brush.horizontalGradient(
|
||||
colors = listOf(GradientStart, GradientMid, GradientEnd)
|
||||
colors = listOf(PremiumPurpleStatic, GradientMid, GradientEnd)
|
||||
)
|
||||
val subtle = Brush.verticalGradient(
|
||||
colors = listOf(PremiumSurface, PremiumBlack)
|
||||
)
|
||||
val cardGlow = Brush.radialGradient(
|
||||
colors = listOf(PremiumPurple.copy(alpha = 0.1f), Color.Transparent)
|
||||
colors = listOf(PremiumSurfaceSolid, PremiumBlack)
|
||||
)
|
||||
@Composable
|
||||
fun cardGlow(): Brush {
|
||||
val tok = LocalTokens.current
|
||||
return Brush.radialGradient(colors = listOf(tok.glow, Color.Transparent))
|
||||
}
|
||||
@Composable
|
||||
fun accentBrush(): Brush {
|
||||
val tok = LocalTokens.current
|
||||
return Brush.linearGradient(listOf(tok.accentFill, tok.accentFillHover))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RadAmpTheme(
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
MaterialTheme(
|
||||
colorScheme = PremiumColorScheme,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
private data class SchemeArgs(
|
||||
val primary: Color, val onPrimary: Color, val primaryContainer: Color, val onPrimaryContainer: Color,
|
||||
val secondary: Color, val onSecondary: Color, val background: Color, val onBackground: Color,
|
||||
val surface: Color, val onSurface: Color, val surfaceVariant: Color, val onSurfaceVariant: Color,
|
||||
val error: Color, val onError: Color, val outline: Color, val outlineVariant: Color,
|
||||
val inverseSurface: Color, val inverseOnSurface: Color, val inversePrimary: Color, val surfaceTint: Color,
|
||||
)
|
||||
|
||||
private fun buildDark(a: SchemeArgs) = darkColorScheme(
|
||||
primary = a.primary, onPrimary = a.onPrimary, primaryContainer = a.primaryContainer,
|
||||
onPrimaryContainer = a.onPrimaryContainer, secondary = a.secondary, onSecondary = a.onSecondary,
|
||||
background = a.background, onBackground = a.onBackground, surface = a.surface, onSurface = a.onSurface,
|
||||
surfaceVariant = a.surfaceVariant, onSurfaceVariant = a.onSurfaceVariant,
|
||||
error = a.error, onError = a.onError, outline = a.outline, outlineVariant = a.outlineVariant,
|
||||
inverseSurface = a.inverseSurface, inverseOnSurface = a.inverseOnSurface,
|
||||
inversePrimary = a.inversePrimary, surfaceTint = a.surfaceTint,
|
||||
)
|
||||
|
||||
private fun buildLight(a: SchemeArgs) = lightColorScheme(
|
||||
primary = a.primary, onPrimary = a.onPrimary, primaryContainer = a.primaryContainer,
|
||||
onPrimaryContainer = a.onPrimaryContainer, secondary = a.secondary, onSecondary = a.onSecondary,
|
||||
background = a.background, onBackground = a.onBackground, surface = a.surface, onSurface = a.onSurface,
|
||||
surfaceVariant = a.surfaceVariant, onSurfaceVariant = a.onSurfaceVariant,
|
||||
error = a.error, onError = a.onError, outline = a.outline, outlineVariant = a.outlineVariant,
|
||||
inverseSurface = a.inverseSurface, inverseOnSurface = a.inverseOnSurface,
|
||||
inversePrimary = a.inversePrimary, surfaceTint = a.surfaceTint,
|
||||
)
|
||||
|
|
|
|||
139
app/src/main/java/com/rada/ui/theme/Tokens.kt
Normal file
139
app/src/main/java/com/rada/ui/theme/Tokens.kt
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
package com.rada.ui.theme
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/** User-selectable accent colors. AA-safe variants live in UiTokens. */
|
||||
enum class AccentMode(val label: String) { VIOLET("Violet"), BLUE("Blue"), GREEN("Green"), ORANGE("Orange") }
|
||||
|
||||
/** App brightness mode. DARK = black-with-white, LIGHT = white-with-black. */
|
||||
enum class AppMode(val label: String) { SYSTEM("System"), DARK("Dark"), LIGHT("Light") }
|
||||
|
||||
/**
|
||||
* Tiny state-backed prefs (Compose-observable, persisted to SharedPreferences).
|
||||
* No DataStore dependency needed: two enums, read once at app start.
|
||||
*/
|
||||
object UiPrefs {
|
||||
var accent by mutableStateOf(AccentMode.VIOLET)
|
||||
private set
|
||||
var mode by mutableStateOf(AppMode.SYSTEM)
|
||||
private set
|
||||
|
||||
fun init(context: Context) {
|
||||
val sp = context.getSharedPreferences("ui_prefs", Context.MODE_PRIVATE)
|
||||
accent = runCatching { AccentMode.valueOf(sp.getString("accent", null) ?: "VIOLET") }
|
||||
.getOrDefault(AccentMode.VIOLET)
|
||||
mode = runCatching { AppMode.valueOf(sp.getString("mode", null) ?: "SYSTEM") }
|
||||
.getOrDefault(AppMode.SYSTEM)
|
||||
}
|
||||
|
||||
fun setAccent(value: AccentMode, context: Context) {
|
||||
accent = value
|
||||
context.getSharedPreferences("ui_prefs", Context.MODE_PRIVATE).edit().putString("accent", value.name).apply()
|
||||
}
|
||||
|
||||
fun setMode(value: AppMode, context: Context) {
|
||||
mode = value
|
||||
context.getSharedPreferences("ui_prefs", Context.MODE_PRIVATE).edit().putString("mode", value.name).apply()
|
||||
}
|
||||
}
|
||||
|
||||
/** Full surface + accent token set for one (accent, brightness) combination. */
|
||||
data class UiTokens(
|
||||
// background
|
||||
val bgTop: Color,
|
||||
val bgBottom: Color,
|
||||
val glow: Color, // painted under content only, never behind text
|
||||
// glass surfaces
|
||||
val glassFill: Color, // translucent layer
|
||||
val glassFillStrong: Color,
|
||||
val border: Color, // decorative 1px
|
||||
val borderInteractive: Color, // >=3:1 vs glass (WCAG 1.4.11)
|
||||
val textSafe: Color, // ~90% opaque overlay under text on glass
|
||||
val panel: Color, // solid panel for lists/body
|
||||
val panelBorder: Color,
|
||||
// text
|
||||
val text: Color,
|
||||
val textSecondary: Color,
|
||||
val textMuted: Color, // caption floor, never below 4.5:1
|
||||
// accent (all AA-verified — see design/glass-radio-theme.css + demo)
|
||||
val accentFill: Color,
|
||||
val accentFillHover: Color,
|
||||
val accentOnFill: Color, // icon/label color ON accent fills
|
||||
val accentText: Color, // accent-colored TEXT on glass/base
|
||||
val accentSoft: Color, // active-state fill behind accent text/icons
|
||||
val success: Color,
|
||||
val focusRing: Color,
|
||||
)
|
||||
|
||||
// ── dark brightness (charcoal #23232B → #1B1B22) ──
|
||||
private val InkDark = Color(0xFF141419)
|
||||
|
||||
internal fun darkTokens(a: AccentMode) = when (a) {
|
||||
AccentMode.VIOLET -> UiTokens(
|
||||
bgTop = BaseTop, bgBottom = BaseBottom, glow = Color(0xFF7C4DFF).copy(alpha = 0.14f),
|
||||
glassFill = Color(0x0FFFFFFF), glassFillStrong = Color(0x17FFFFFF),
|
||||
border = Color(0x1AFFFFFF), borderInteractive = Color(0x5CFFFFFF),
|
||||
textSafe = Color(0xE61B1B22), panel = PremiumSurfaceSolid, panelBorder = Color(0x14FFFFFF),
|
||||
text = Color.White, textSecondary = Color(0xFFD9D9DE), textMuted = Color(0xFF8C8C96),
|
||||
accentFill = Color(0xFF7C4DFF), accentFillHover = Color(0xFF8E66FF),
|
||||
accentOnFill = Color.White, accentText = Color(0xFFA78BFF),
|
||||
accentSoft = Color(0x297C4DFF), success = Color(0xFF34C759), focusRing = Color(0xFFA78BFF),
|
||||
)
|
||||
AccentMode.BLUE -> UiTokens(
|
||||
bgTop = BaseTop, bgBottom = BaseBottom, glow = Color(0xFF4C7DFF).copy(alpha = 0.13f),
|
||||
glassFill = Color(0x0FFFFFFF), glassFillStrong = Color(0x17FFFFFF),
|
||||
border = Color(0x1AFFFFFF), borderInteractive = Color(0x5CFFFFFF),
|
||||
textSafe = Color(0xE61B1B22), panel = PremiumSurfaceSolid, panelBorder = Color(0x14FFFFFF),
|
||||
text = Color.White, textSecondary = Color(0xFFD9D9DE), textMuted = Color(0xFF8C8C96),
|
||||
accentFill = Color(0xFF4C7DFF), accentFillHover = Color(0xFF6390FF),
|
||||
accentOnFill = InkDark, accentText = Color(0xFF93B4FF),
|
||||
accentSoft = Color(0x294C7DFF), success = Color(0xFF34C759), focusRing = Color(0xFF93B4FF),
|
||||
)
|
||||
AccentMode.GREEN -> UiTokens(
|
||||
bgTop = BaseTop, bgBottom = BaseBottom, glow = Color(0xFF1FA855).copy(alpha = 0.13f),
|
||||
glassFill = Color(0x0FFFFFFF), glassFillStrong = Color(0x17FFFFFF),
|
||||
border = Color(0x1AFFFFFF), borderInteractive = Color(0x5CFFFFFF),
|
||||
textSafe = Color(0xE61B1B22), panel = PremiumSurfaceSolid, panelBorder = Color(0x14FFFFFF),
|
||||
text = Color.White, textSecondary = Color(0xFFD9D9DE), textMuted = Color(0xFF8C8C96),
|
||||
accentFill = Color(0xFF1FA855), accentFillHover = Color(0xFF2BBD64),
|
||||
accentOnFill = InkDark, accentText = Color(0xFF5BD98A),
|
||||
accentSoft = Color(0x291FA855), success = Color(0xFF34C759), focusRing = Color(0xFF5BD98A),
|
||||
)
|
||||
AccentMode.ORANGE -> UiTokens(
|
||||
bgTop = BaseTop, bgBottom = BaseBottom, glow = Color(0xFFF97316).copy(alpha = 0.12f),
|
||||
glassFill = Color(0x0FFFFFFF), glassFillStrong = Color(0x17FFFFFF),
|
||||
border = Color(0x1AFFFFFF), borderInteractive = Color(0x5CFFFFFF),
|
||||
textSafe = Color(0xE61B1B22), panel = PremiumSurfaceSolid, panelBorder = Color(0x14FFFFFF),
|
||||
text = Color.White, textSecondary = Color(0xFFD9D9DE), textMuted = Color(0xFF8C8C96),
|
||||
accentFill = Color(0xFFF97316), accentFillHover = Color(0xFFFB8A3C),
|
||||
accentOnFill = InkDark, accentText = Color(0xFFFFB37E),
|
||||
accentSoft = Color(0x29F97316), success = Color(0xFF34C759), focusRing = Color(0xFFFFB37E),
|
||||
)
|
||||
}
|
||||
|
||||
// ── light brightness (paper #F2F1F7 → #E7E5F0; dark ink text) ──
|
||||
private fun lightTokens(a: AccentMode): UiTokens {
|
||||
val (fill, hover, textOnGlass, soft) = when (a) {
|
||||
AccentMode.VIOLET -> arrayOf(Color(0xFF6A3DE0), Color(0xFF5B2EDB), Color(0xFF5B2EDB), Color(0x1F6A3DE0))
|
||||
AccentMode.BLUE -> arrayOf(Color(0xFF2563EB), Color(0xFF1D4ED8), Color(0xFF1D4ED8), Color(0x1F2563EB))
|
||||
AccentMode.GREEN -> arrayOf(Color(0xFF0C7A38), Color(0xFF0F7A36), Color(0xFF0F7A36), Color(0x1F0C7A38))
|
||||
AccentMode.ORANGE -> arrayOf(Color(0xFFC2570A), Color(0xFFB34700), Color(0xFFB34700), Color(0x1FC2570A))
|
||||
}
|
||||
return UiTokens(
|
||||
bgTop = Color(0xFFF2F1F7), bgBottom = Color(0xFFE7E5F0), glow = fill.copy(alpha = 0.10f),
|
||||
glassFill = Color(0x9EFFFFFF), glassFillStrong = Color(0xC7FFFFFF),
|
||||
border = Color(0x1A23232B), borderInteractive = Color(0x7323232B),
|
||||
textSafe = Color(0xEBFBFAFE), panel = Color(0xFFFBFAFE), panelBorder = Color(0x1A23232B),
|
||||
text = Color(0xFF1B1B22), textSecondary = Color(0xCC1B1B22), textMuted = Color(0x9E1B1B22),
|
||||
accentFill = fill, accentFillHover = hover,
|
||||
accentOnFill = Color.White, accentText = textOnGlass,
|
||||
accentSoft = soft, success = Color(0xFF0F7A36), focusRing = textOnGlass,
|
||||
)
|
||||
}
|
||||
|
||||
fun tokensFor(accent: AccentMode, dark: Boolean): UiTokens =
|
||||
if (dark) darkTokens(accent) else lightTokens(accent)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">RadAmp</string>
|
||||
<string name="app_name">SatsAmp</string>
|
||||
<string name="playback_channel_name">Playback</string>
|
||||
<string name="playback_channel_description">Radio playback controls</string>
|
||||
<string name="notification_play">Play</string>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.RadAmp" parent="android:Theme.Material.NoActionBar" />
|
||||
<style name="Theme.SatsAmp" parent="android:Theme.Material.NoActionBar" />
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue