Adopt the shared neiam look and feel

eddae77 · gmorell · 2026-11-05 16:27

32 files +537 -66
Message
{commit_body(@commit)}

Files changed

modified README.md
+18 −0
@@ -41,6 +41,24 @@ Size (40–120 dp), opacity, auto-collapse delay, and toggles for title, prev/ne
41 41 seek ±10 s, the progress ring, double-tap play/pause, and hiding when no session exists.
42 42 Settings apply live, and the settings screen includes a working preview.
43 43
44 +## Look & feel
45 +
46 +Shared with the other neiam Android apps (`bigtrippr`, `fieri-eternal`), with
47 +bigtrippr as the reference copy:
48 +
49 +- `ui/theme/AppTheme.kt`: the daisyUI palettes (Light, Dark, Her, After Dark,
50 + Forest, Sky, Clays, Stones, Blueprint) defined in OKLCh, with every Material 3 slot
51 + pinned so Material's baseline purple never shows. **System** follows the OS
52 + between Light and Dark.
53 +- `Typography.kt`: B612 throughout. `Shapes.kt`: the shared corner radii.
54 +- `ThemePicker.kt`: the web picker's list with swatches. `Common.kt`: an `AppBarTitle`
55 + in the accent color, B612 Bold.
56 +- System bars are painted in the palette's base color, and the window starts on After Dark
57 + so launch doesn't flash white.
58 +
59 +The bubble follows the chosen palette too: base-100 body, primary play button
60 +and progress ring, base-content text.
61 +
44 62 ## Architecture
45 63
46 64 | Package | Role |
modified app/src/main/AndroidManifest.xml
+2 −2
@@ -25,13 +25,13 @@
25 25 android:roundIcon="@mipmap/ic_icon"
26 26 android:allowBackup="false"
27 27 android:supportsRtl="true"
28 − android:theme="@style/Theme.Floaty">
28 + android:theme="@style/Theme.MediaBubbles">
29 29
30 30 <activity
31 31 android:name=".MainActivity"
32 32 android:exported="true"
33 33 android:launchMode="singleTask"
34 − android:theme="@style/Theme.Floaty">
34 + android:theme="@style/Theme.MediaBubbles">
35 35 <intent-filter>
36 36 <action android:name="android.intent.action.MAIN"/>
37 37 <category android:name="android.intent.category.LAUNCHER"/>
modified app/src/main/java/org/neiam/mediabubbles/MainActivity.kt
+21 −3
@@ -4,23 +4,41 @@ import android.content.Intent
4 4 import android.os.Bundle
5 5 import android.provider.Settings
6 6 import androidx.activity.ComponentActivity
7 +import androidx.activity.SystemBarStyle
7 8 import androidx.activity.compose.setContent
9 +import androidx.activity.enableEdgeToEdge
8 10 import androidx.compose.foundation.layout.fillMaxSize
9 11 import androidx.compose.material3.MaterialTheme
10 12 import androidx.compose.material3.Surface
13 +import androidx.compose.runtime.LaunchedEffect
14 +import androidx.compose.runtime.collectAsState
15 +import androidx.compose.runtime.getValue
11 16 import androidx.compose.ui.Modifier
17 +import androidx.compose.ui.graphics.toArgb
12 18 import org.neiam.mediabubbles.overlay.OverlayService
13 −import org.neiam.mediabubbles.ui.FloatyTheme
14 19 import org.neiam.mediabubbles.ui.SettingsScreen
20 +import org.neiam.mediabubbles.ui.theme.MediaBubblesTheme
21 +import org.neiam.mediabubbles.ui.theme.appThemeForKey
15 22
16 23 class MainActivity : ComponentActivity() {
17 24 override fun onCreate(savedInstanceState: Bundle?) {
18 25 super.onCreate(savedInstanceState)
19 26 handle(intent)
27 + val prefs = container.prefs
20 28 setContent {
21 − FloatyTheme {
29 + val themeKey by prefs.themeKey.collectAsState()
30 + val theme = appThemeForKey(themeKey)
31 +
32 + // System bars painted in the palette's base, as in the other neiam apps.
33 + LaunchedEffect(theme) {
34 + val bar = theme.bg.toArgb()
35 + val style = if (theme.isLight) SystemBarStyle.light(bar, bar) else SystemBarStyle.dark(bar)
36 + enableEdgeToEdge(statusBarStyle = style, navigationBarStyle = style)
37 + }
38 +
39 + MediaBubblesTheme(theme) {
22 40 Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
23 − SettingsScreen()
41 + SettingsScreen(themeKey = themeKey, onPickTheme = prefs::setThemeKey)
24 42 }
25 43 }
26 44 }
modified app/src/main/java/org/neiam/mediabubbles/data/BubblePrefs.kt
+11 −0
@@ -3,6 +3,7 @@ package org.neiam.mediabubbles.data
3 3 import android.content.Context
4 4 import kotlinx.coroutines.flow.MutableStateFlow
5 5 import kotlinx.coroutines.flow.StateFlow
6 +import org.neiam.mediabubbles.ui.theme.SYSTEM_THEME_KEY
6 7
7 8 /** Everything the user can tune about the bubble. */
8 9 data class BubbleConfig(
@@ -55,6 +56,15 @@ class BubblePrefs(context: Context) {
55 56
56 57 fun reset() = update { BubbleConfig() }
57 58
59 + /** Chosen palette key; nothing chosen follows the OS, as on the web. */
60 + private val _themeKey = MutableStateFlow(sp.getString(K_THEME, null) ?: SYSTEM_THEME_KEY)
61 + val themeKey: StateFlow<String> = _themeKey
62 +
63 + fun setThemeKey(key: String) {
64 + sp.edit().putString(K_THEME, key).apply()
65 + _themeKey.value = key
66 + }
67 +
58 68 /** The user wants the bubble shown; used to bring it back after reboot / process death. */
59 69 var bubbleWanted: Boolean
60 70 get() = sp.getBoolean(K_WANTED, false)
@@ -106,5 +116,6 @@ class BubblePrefs(context: Context) {
106 116 const val K_WANTED = "bubble_wanted"
107 117 const val K_POS_RIGHT = "pos_right"
108 118 const val K_POS_Y = "pos_y"
119 + const val K_THEME = "theme_key"
109 120 }
110 121 }
modified app/src/main/java/org/neiam/mediabubbles/overlay/MediaBubble.kt
+17 −12
@@ -57,14 +57,17 @@ import org.neiam.mediabubbles.data.BubbleConfig
57 57 import org.neiam.mediabubbles.data.BubblePrefs
58 58 import org.neiam.mediabubbles.media.MediaRepository
59 59 import org.neiam.mediabubbles.media.NowPlaying
60 +import org.neiam.mediabubbles.ui.theme.LocalAppTheme
60 61
61 −// Fixed dark palette (afterdark): the bubble sits over arbitrary maps, so it
62 −// shouldn't flip with the system theme.
63 −private val BubbleBg = Color(0xFF1B1840)
64 −private val ArtFallbackBg = Color(0xFF2A2675)
65 −private val Accent = Color(0xFFACABD5)
66 −private val OnAccent = Color(0xFF201D65)
67 −private val RingTrack = Color(0x40FFFFFF)
62 +// The bubble follows the chosen neiam palette: base-100 body, base-200 art
63 +// well, primary for the play button and progress, base-content for text.
64 +private val BubbleBg @Composable get() = LocalAppTheme.current.bg
65 +private val ArtFallbackBg @Composable get() = LocalAppTheme.current.cardBg
66 +private val Accent @Composable get() = LocalAppTheme.current.primary
67 +private val OnAccent @Composable get() = LocalAppTheme.current.onPrimary
68 +private val Content @Composable get() = LocalAppTheme.current.content
69 +private val Dim @Composable get() = LocalAppTheme.current.dim
70 +private val RingTrack @Composable get() = LocalAppTheme.current.content.copy(alpha = 0.25f)
68 71
69 72 class BubbleActions(
70 73 val toggleExpanded: () -> Unit,
@@ -141,7 +144,7 @@ fun MediaBubble(
141 144 .graphicsLayer { alpha = cfg.opacity },
142 145 shape = RoundedCornerShape(percent = 50),
143 146 color = BubbleBg,
144 − contentColor = Color.White,
147 + contentColor = Content,
145 148 shadowElevation = 3.dp,
146 149 ) {
147 150 Row(verticalAlignment = Alignment.CenterVertically) {
@@ -211,14 +214,16 @@ private fun ArtDisc(
211 214
212 215 if (cfg.showProgress && np != null && np.durationMs > 0) {
213 216 val progress = rememberProgress(np)
217 + val track = RingTrack
218 + val accent = Accent
214 219 Canvas(Modifier.size(size)) {
215 220 val sw = ring.toPx() * 0.7f
216 221 val inset = ring.toPx() / 2
217 222 val arcSize = Size(this.size.width - inset * 2, this.size.height - inset * 2)
218 223 val tl = Offset(inset, inset)
219 − drawArc(RingTrack, 0f, 360f, false, tl, arcSize, style = Stroke(sw))
224 + drawArc(track, 0f, 360f, false, tl, arcSize, style = Stroke(sw))
220 225 drawArc(
221 − Accent, -90f, 360f * progress, false, tl, arcSize,
226 + accent, -90f, 360f * progress, false, tl, arcSize,
222 227 style = Stroke(sw, cap = StrokeCap.Round),
223 228 )
224 229 }
@@ -244,7 +249,7 @@ private fun TitleBlock(np: NowPlaying?, size: Dp) {
244 249 text = sub,
245 250 maxLines = 1,
246 251 fontSize = (base * 0.82f).sp,
247 − color = Color.White.copy(alpha = 0.7f),
252 + color = Dim,
248 253 modifier = Modifier.basicMarquee(iterations = Int.MAX_VALUE),
249 254 )
250 255 }
@@ -292,7 +297,7 @@ private fun ControlButton(
292 297 ) {
293 298 Icon(
294 299 icon, description, Modifier.size(size * 0.62f),
295 − tint = if (filled) OnAccent else Color.White,
300 + tint = if (filled) OnAccent else Content,
296 301 )
297 302 }
298 303 }
modified app/src/main/java/org/neiam/mediabubbles/overlay/OverlayService.kt
+5 −2

Click to load diff…

added app/src/main/java/org/neiam/mediabubbles/ui/Common.kt
+19 −0

Click to load diff…

modified app/src/main/java/org/neiam/mediabubbles/ui/SettingsScreen.kt
+59 −10

Click to load diff…

deleted app/src/main/java/org/neiam/mediabubbles/ui/Theme.kt
+0 −36

Click to load diff…

added app/src/main/java/org/neiam/mediabubbles/ui/ThemePicker.kt
+71 −0

Click to load diff…

added app/src/main/java/org/neiam/mediabubbles/ui/theme/AppTheme.kt
+215 −0

Click to load diff…

added app/src/main/java/org/neiam/mediabubbles/ui/theme/Shapes.kt
+31 −0

Click to load diff…

added app/src/main/java/org/neiam/mediabubbles/ui/theme/Theme.kt
+18 −0

Click to load diff…

added app/src/main/java/org/neiam/mediabubbles/ui/theme/Typography.kt
+39 −0

Click to load diff…

modified ?
+0 −0

Click to load diff…

modified ?
+0 −0

Click to load diff…

added app/src/main/res/values/colors.xml
+5 −0

Click to load diff…

modified app/src/main/res/values/themes.xml
+6 −1

Click to load diff…

Parents: 19cd52b