diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index 75571344..81f11912 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -44,6 +44,14 @@
Dark
System
+ Surface
+ Background material of the app. Current: %1$s
+ Watery
+ Icy
+ Chrome
+ Acrylic
+ Glass
+
Accent color
Current palette: %1$s
Accent color
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt
index 6eb9bb8c..297f3998 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt
@@ -35,6 +35,7 @@ import dev.krtirtho.spotube.core.navigation.Routes
import dev.krtirtho.spotube.core.navigation.TOP_LEVEL_ROUTES
import dev.krtirtho.spotube.core.navigation.rememberNavigationState
import dev.krtirtho.spotube.core.navigation.toEntries
+import dev.krtirtho.spotube.core.ui.base.LocalBaseUIColors
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
import dev.krtirtho.spotube.core.ui.base.rememberBaseUITheme
import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope
@@ -156,8 +157,11 @@ fun App(
}
SpotubeTheme(settings = userSettings) {
- val baseUITheme = rememberBaseUITheme()
- CompositionLocalProvider(LocalBaseUITheme provides baseUITheme) {
+ val baseUITheme = rememberBaseUITheme(surfaceTheme = userSettings.surfaceTheme)
+ CompositionLocalProvider(
+ LocalBaseUITheme provides baseUITheme,
+ LocalBaseUIColors provides baseUITheme.colors,
+ ) {
Box(modifier = Modifier.fillMaxSize()) {
val currentRoute =
navigationState.backStacks[navigationState.topLevelRoute]?.last()
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/BaseUIColorScheme.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/BaseUIColorScheme.kt
new file mode 100644
index 00000000..412c97b4
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/BaseUIColorScheme.kt
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package dev.krtirtho.spotube.core.ui.base
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.lerp
+import androidx.compose.ui.graphics.luminance
+
+/**
+ * The app's own colour system, independent of [MaterialTheme].
+ *
+ * Each [SurfaceStyle] is a complete visual theme with its own palette (backgrounds,
+ * surfaces, text and outlines) rather than Material's role colours. The user's chosen
+ * accent is threaded through as `primary`, but everything else comes from the surface
+ * theme, so the app never has to rely on Material's colour scheme.
+ */
+@Immutable
+data class BaseUIColorScheme(
+ val surfaceTheme: SurfaceStyle,
+ val isLight: Boolean,
+ /** Full-bleed page background for the current surface theme. */
+ val background: Brush,
+ val surface: Color,
+ val surfaceVariant: Color,
+ val surfaceContainer: Color,
+ val surfaceContainerHigh: Color,
+ val surfaceContainerHighest: Color,
+ val onBackground: Color,
+ val onSurface: Color,
+ val onSurfaceVariant: Color,
+ val primary: Color,
+ val onPrimary: Color,
+ val primaryContainer: Color,
+ val onPrimaryContainer: Color,
+ val secondaryContainer: Color,
+ val onSecondaryContainer: Color,
+ val outline: Color,
+ val outlineVariant: Color,
+ val error: Color,
+ val onError: Color,
+ val scrim: Color,
+)
+
+val LocalBaseUIColors = staticCompositionLocalOf {
+ error("No BaseUIColorScheme provided. Wrap your UI in a BaseUITheme provider.")
+}
+
+/**
+ * Builds the [BaseUIColorScheme] for the selected [surfaceTheme] and accent.
+ *
+ * Light/dark is read from the surrounding [MaterialTheme] (which the app already drives
+ * from the user's theme setting), and the accent comes from the user's chosen accent
+ * colour. Everything else is derived from the surface theme palette.
+ */
+@Composable
+fun rememberBaseUIColorScheme(
+ surfaceTheme: SurfaceStyle = SurfaceStyle.Glass,
+ accentColor: Color? = null,
+): BaseUIColorScheme {
+ val isLight = MaterialTheme.colorScheme.surface.luminance() > 0.5f
+ val accent = accentColor ?: MaterialTheme.colorScheme.primary
+ return remember(surfaceTheme, isLight, accent) {
+ baseUIColorScheme(surfaceTheme, isLight, accent)
+ }
+}
+
+fun baseUIColorScheme(
+ surfaceTheme: SurfaceStyle,
+ isLight: Boolean,
+ accent: Color,
+): BaseUIColorScheme {
+ val palette = surfacePalette(surfaceTheme, isLight)
+ val onPrimary = if (accent.luminance() > 0.5f) Color(0xFF10161C) else Color.White
+ val primaryContainer = lerp(palette.surface, accent, if (isLight) 0.16f else 0.32f)
+ val onPrimaryContainer = if (isLight) {
+ lerp(accent, Color.Black, 0.55f)
+ } else {
+ lerp(accent, Color.White, 0.7f)
+ }
+ val secondaryContainer = lerp(palette.surface, accent, if (isLight) 0.22f else 0.34f)
+
+ return BaseUIColorScheme(
+ surfaceTheme = surfaceTheme,
+ isLight = isLight,
+ background = Brush.verticalGradient(
+ listOf(palette.backgroundTop, palette.backgroundBottom),
+ ),
+ surface = palette.surface,
+ surfaceVariant = palette.surfaceVariant,
+ surfaceContainer = palette.surfaceContainer,
+ surfaceContainerHigh = palette.surfaceContainerHigh,
+ surfaceContainerHighest = palette.surfaceContainerHighest,
+ onBackground = palette.onBackground,
+ onSurface = palette.onSurface,
+ onSurfaceVariant = palette.onSurfaceVariant,
+ primary = accent,
+ onPrimary = onPrimary,
+ primaryContainer = primaryContainer,
+ onPrimaryContainer = onPrimaryContainer,
+ secondaryContainer = secondaryContainer,
+ onSecondaryContainer = palette.onSurface,
+ outline = palette.outline,
+ outlineVariant = palette.outlineVariant,
+ error = if (isLight) Color(0xFFC0392B) else Color(0xFFE57373),
+ onError = Color.White,
+ scrim = Color.Black.copy(alpha = 0.42f),
+ )
+}
+
+private class SurfacePalette(
+ val backgroundTop: Color,
+ val backgroundBottom: Color,
+ val surface: Color,
+ val surfaceVariant: Color,
+ val surfaceContainer: Color,
+ val surfaceContainerHigh: Color,
+ val surfaceContainerHighest: Color,
+ val onBackground: Color,
+ val onSurface: Color,
+ val onSurfaceVariant: Color,
+ val outline: Color,
+ val outlineVariant: Color,
+)
+
+private fun surfacePalette(style: SurfaceStyle, isLight: Boolean): SurfacePalette {
+ return if (isLight) {
+ when (style) {
+ SurfaceStyle.Glass -> SurfacePalette(
+ backgroundTop = Color(0xFFEAF0F7),
+ backgroundBottom = Color(0xFFD8E3F1),
+ surface = Color(0xFFF8FBFF),
+ surfaceVariant = Color(0xFFE4ECF5),
+ surfaceContainer = Color(0xFFEEF3F9),
+ surfaceContainerHigh = Color(0xFFF3F7FC),
+ surfaceContainerHighest = Color(0xFFE1E9F3),
+ onBackground = Color(0xFF1B2733),
+ onSurface = Color(0xFF1B2733),
+ onSurfaceVariant = Color(0xFF56677A),
+ outline = Color(0xFFA9B8C8),
+ outlineVariant = Color(0xFFD3DDE8),
+ )
+
+ SurfaceStyle.Acrylic -> SurfacePalette(
+ backgroundTop = Color(0xFFE6EAF0),
+ backgroundBottom = Color(0xFFD6DCE5),
+ surface = Color(0xFFF4F7FA),
+ surfaceVariant = Color(0xFFE1E7EE),
+ surfaceContainer = Color(0xFFEAEEF4),
+ surfaceContainerHigh = Color(0xFFF1F4F8),
+ surfaceContainerHighest = Color(0xFFDCE2E9),
+ onBackground = Color(0xFF1C222B),
+ onSurface = Color(0xFF1C222B),
+ onSurfaceVariant = Color(0xFF545E6C),
+ outline = Color(0xFFA6AEBC),
+ outlineVariant = Color(0xFFD2D8E0),
+ )
+
+ SurfaceStyle.Watery -> SurfacePalette(
+ backgroundTop = Color(0xFFD6F0FF),
+ backgroundBottom = Color(0xFFB4DFFA),
+ surface = Color(0xFFF0FAFF),
+ surfaceVariant = Color(0xFFDCF0FC),
+ surfaceContainer = Color(0xFFE7F5FE),
+ surfaceContainerHigh = Color(0xFFEFF9FF),
+ surfaceContainerHighest = Color(0xFFCFE9F8),
+ onBackground = Color(0xFF08324A),
+ onSurface = Color(0xFF08324A),
+ onSurfaceVariant = Color(0xFF3A6884),
+ outline = Color(0xFF8FBBD4),
+ outlineVariant = Color(0xFFC6E2F2),
+ )
+
+ SurfaceStyle.Icy -> SurfacePalette(
+ backgroundTop = Color(0xFFE8F7FF),
+ backgroundBottom = Color(0xFFC9E8FB),
+ surface = Color(0xFFF6FCFF),
+ surfaceVariant = Color(0xFFE0F1FC),
+ surfaceContainer = Color(0xFFEBF7FE),
+ surfaceContainerHigh = Color(0xFFF3FBFF),
+ surfaceContainerHighest = Color(0xFFD4EBFA),
+ onBackground = Color(0xFF0B3A55),
+ onSurface = Color(0xFF0B3A55),
+ onSurfaceVariant = Color(0xFF3D6C88),
+ outline = Color(0xFF93C0D9),
+ outlineVariant = Color(0xFFC9E5F4),
+ )
+
+ SurfaceStyle.Chrome -> SurfacePalette(
+ backgroundTop = Color(0xFFE4E8ED),
+ backgroundBottom = Color(0xFFD0D6DE),
+ surface = Color(0xFFF4F7FA),
+ surfaceVariant = Color(0xFFE0E5EB),
+ surfaceContainer = Color(0xFFE9EDF2),
+ surfaceContainerHigh = Color(0xFFF1F4F7),
+ surfaceContainerHighest = Color(0xFFDBE0E7),
+ onBackground = Color(0xFF1B2733),
+ onSurface = Color(0xFF1B2733),
+ onSurfaceVariant = Color(0xFF525C6A),
+ outline = Color(0xFFA2AAB6),
+ outlineVariant = Color(0xFFD0D5DC),
+ )
+ }
+ } else {
+ when (style) {
+ SurfaceStyle.Glass -> SurfacePalette(
+ backgroundTop = Color(0xFF0E1217),
+ backgroundBottom = Color(0xFF161C24),
+ surface = Color(0xFF1E242C),
+ surfaceVariant = Color(0xFF262E38),
+ surfaceContainer = Color(0xFF1A2027),
+ surfaceContainerHigh = Color(0xFF232A33),
+ surfaceContainerHighest = Color(0xFF2C343E),
+ onBackground = Color(0xFFE6EDF5),
+ onSurface = Color(0xFFE6EDF5),
+ onSurfaceVariant = Color(0xFF9AA7B6),
+ outline = Color(0xFF46525F),
+ outlineVariant = Color(0xFF2C343E),
+ )
+
+ SurfaceStyle.Acrylic -> SurfacePalette(
+ backgroundTop = Color(0xFF101317),
+ backgroundBottom = Color(0xFF181C22),
+ surface = Color(0xFF20262E),
+ surfaceVariant = Color(0xFF282F39),
+ surfaceContainer = Color(0xFF1C2229),
+ surfaceContainerHigh = Color(0xFF252C35),
+ surfaceContainerHighest = Color(0xFF2E3640),
+ onBackground = Color(0xFFE4EAF1),
+ onSurface = Color(0xFFE4EAF1),
+ onSurfaceVariant = Color(0xFF98A2B0),
+ outline = Color(0xFF48525E),
+ outlineVariant = Color(0xFF2E3640),
+ )
+
+ SurfaceStyle.Watery -> SurfacePalette(
+ backgroundTop = Color(0xFF06121E),
+ backgroundBottom = Color(0xFF0B1F31),
+ surface = Color(0xFF0E2233),
+ surfaceVariant = Color(0xFF153148),
+ surfaceContainer = Color(0xFF0B1D2C),
+ surfaceContainerHigh = Color(0xFF12293C),
+ surfaceContainerHighest = Color(0xFF183449),
+ onBackground = Color(0xFFD6EAF7),
+ onSurface = Color(0xFFD6EAF7),
+ onSurfaceVariant = Color(0xFF8FB0C6),
+ outline = Color(0xFF3C5A72),
+ outlineVariant = Color(0xFF183449),
+ )
+
+ SurfaceStyle.Icy -> SurfacePalette(
+ backgroundTop = Color(0xFF0C1620),
+ backgroundBottom = Color(0xFF132331),
+ surface = Color(0xFF16283A),
+ surfaceVariant = Color(0xFF1E3448),
+ surfaceContainer = Color(0xFF122130),
+ surfaceContainerHigh = Color(0xFF1A2E42),
+ surfaceContainerHighest = Color(0xFF213A50),
+ onBackground = Color(0xFFDCEBF7),
+ onSurface = Color(0xFFDCEBF7),
+ onSurfaceVariant = Color(0xFF92B3CA),
+ outline = Color(0xFF40607A),
+ outlineVariant = Color(0xFF213A50),
+ )
+
+ SurfaceStyle.Chrome -> SurfacePalette(
+ backgroundTop = Color(0xFF101317),
+ backgroundBottom = Color(0xFF181C22),
+ surface = Color(0xFF232830),
+ surfaceVariant = Color(0xFF2B313A),
+ surfaceContainer = Color(0xFF1E232A),
+ surfaceContainerHigh = Color(0xFF272D36),
+ surfaceContainerHighest = Color(0xFF303742),
+ onBackground = Color(0xFFE7ECF2),
+ onSurface = Color(0xFFE7ECF2),
+ onSurfaceVariant = Color(0xFF9BA5B2),
+ outline = Color(0xFF4B5561),
+ outlineVariant = Color(0xFF303742),
+ )
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/BaseUIPreviews.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/BaseUIPreviews.kt
index 1a5cc11c..7ce9a6f2 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/BaseUIPreviews.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/BaseUIPreviews.kt
@@ -48,15 +48,20 @@ import androidx.compose.ui.tooling.preview.Preview
@Composable
internal fun BaseUIPreview(
modifier: Modifier = Modifier,
+ surfaceTheme: SurfaceStyle = SurfaceStyle.Glass,
content: @Composable BoxScope.() -> Unit,
) {
MaterialTheme(
colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme(),
) {
- CompositionLocalProvider(LocalBaseUITheme provides rememberBaseUITheme()) {
+ val theme = rememberBaseUITheme(surfaceTheme = surfaceTheme)
+ CompositionLocalProvider(
+ LocalBaseUITheme provides theme,
+ LocalBaseUIColors provides theme.colors,
+ ) {
Surface(
modifier = modifier.fillMaxSize(),
- color = MaterialTheme.colorScheme.background,
+ color = theme.colors.surface,
) {
Box(modifier = Modifier.fillMaxSize(), content = content)
}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Scaffold.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Scaffold.kt
new file mode 100644
index 00000000..59a6ea61
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Scaffold.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package dev.krtirtho.spotube.core.ui.base
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.ScaffoldDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+
+/**
+ * A theme-aware replacement for [Scaffold].
+ *
+ * Material's [Scaffold] paints Material's own surface colour, which hides the selected
+ * [SurfaceStyle] (Watery/Icy/Chrome/Acrylic/Glass). [BaseScaffold] instead:
+ *
+ * - is **transparent by default**, so the app-wide themed backdrop shows through, or
+ * - paints a chosen [surface] material locally, or
+ * - uses an explicit [containerColor] when one is given.
+ *
+ * It keeps the same slot structure as [Scaffold] (top/bottom bars, snackbar host,
+ * floating action button) and hands the resolved [PaddingValues] to [content].
+ */
+@Composable
+fun BaseScaffold(
+ modifier: Modifier = Modifier,
+ topBar: @Composable () -> Unit = {},
+ bottomBar: @Composable () -> Unit = {},
+ snackbarHost: @Composable () -> Unit = {},
+ floatingActionButton: @Composable () -> Unit = {},
+ surface: SurfaceStyle? = null,
+ containerColor: Color? = null,
+ contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
+ content: @Composable (PaddingValues) -> Unit,
+) {
+ Scaffold(
+ modifier = modifier,
+ topBar = topBar,
+ bottomBar = bottomBar,
+ snackbarHost = snackbarHost,
+ floatingActionButton = floatingActionButton,
+ containerColor = Color.Transparent,
+ contentWindowInsets = contentWindowInsets,
+ ) { innerPadding ->
+ val backgroundModifier = when {
+ containerColor != null -> Modifier.background(containerColor)
+ surface != null -> {
+ val isLight = LocalBaseUIColors.current.isLight
+ Modifier.baseSurfaceBackdrop(surface, isLight)
+ }
+
+ else -> Modifier
+ }
+ Box(modifier = Modifier.fillMaxSize().then(backgroundModifier)) {
+ content(innerPadding)
+ }
+ }
+}
+
+@BaseUIPhonePreview
+@BaseUIDesktopPreview
+@Composable
+private fun BaseScaffoldPreview() {
+ BaseUIPreview {
+ val style = LocalBaseUITheme.current.surfaceTheme
+ BaseScaffold(
+ surface = style,
+ topBar = {
+ Text(
+ text = "Title",
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.SemiBold,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ )
+ },
+ bottomBar = {
+ Text(
+ text = "Bottom bar",
+ style = MaterialTheme.typography.labelMedium,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ )
+ },
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ Text("Content")
+ Text("Sits on the selected surface theme.", style = MaterialTheme.typography.bodySmall)
+ }
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Surface.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Surface.kt
index b3ad145f..f10ae714 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Surface.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Surface.kt
@@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -52,6 +53,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlin.random.Random
+import kotlinx.serialization.Serializable
/**
* The Frutiger-Aero material families available to [BaseSurface].
@@ -61,7 +63,10 @@ import kotlin.random.Random
* - [Chrome]: brushed metal with a hard specular reflection.
* - [Acrylic]: translucent tinted panel with fine grain.
* - [Glass]: clear, glossy glass with a strong sheen.
+ *
+ * Each style doubles as a full app theme (see [BaseUIColorScheme]).
*/
+@Serializable
enum class SurfaceStyle {
Watery,
Icy,
@@ -101,7 +106,7 @@ fun BaseSurface(
val resolvedContentColor = contentColor ?: when (style) {
SurfaceStyle.Watery -> Color.White
SurfaceStyle.Icy -> if (isLight) Color(0xFF0B3A55) else Color(0xFFDCEBF7)
- SurfaceStyle.Chrome -> Color(0xFF1B2733)
+ SurfaceStyle.Chrome -> if (isLight) Color(0xFF1B2733) else Color(0xFFE7ECF2)
SurfaceStyle.Acrylic, SurfaceStyle.Glass -> onSurface
}
val ambient = when (style) {
@@ -167,11 +172,25 @@ fun BaseSurface(
}
}
+/**
+ * Paints the full-bleed material of [style] as a page background: the material body plus
+ * its own sheen and reflections, but no panel bevel (a backdrop has no edges).
+ */
+fun Modifier.baseSurfaceBackdrop(style: SurfaceStyle, isLight: Boolean): Modifier =
+ drawWithCache {
+ onDrawBehind { drawSurfaceBase(style, isLight) }
+ }
+
private fun DrawScope.drawSurfaceMaterial(style: SurfaceStyle, isLight: Boolean) {
+ drawSurfaceBase(style, isLight)
+ drawInnerBevel(isLight)
+}
+
+private fun DrawScope.drawSurfaceBase(style: SurfaceStyle, isLight: Boolean) {
when (style) {
SurfaceStyle.Watery -> drawWatery(isLight)
SurfaceStyle.Icy -> drawIcy(isLight)
- SurfaceStyle.Chrome -> drawChrome()
+ SurfaceStyle.Chrome -> drawChrome(isLight)
SurfaceStyle.Acrylic -> drawAcrylic(isLight)
SurfaceStyle.Glass -> drawGlass(isLight)
}
@@ -191,7 +210,6 @@ private fun DrawScope.drawGlass(isLight: Boolean) {
)
drawGloss(if (isLight) 0.55f else 0.16f)
drawBottomReflection(if (isLight) 0.4f else 0.1f)
- drawInnerBevel(isLight)
drawBubbles(seed = 11, count = 3, alpha = if (isLight) 0.4f else 0.14f)
}
@@ -222,28 +240,48 @@ private fun DrawScope.drawAcrylic(isLight: Boolean) {
)
}
drawGloss(if (isLight) 0.4f else 0.12f, heightFraction = 0.45f)
- drawInnerBevel(isLight)
}
-private fun DrawScope.drawChrome() {
- drawRect(
- brush = Brush.verticalGradient(
- 0f to Color(0xFFF8FBFF),
- 0.16f to Color(0xFFD6DEE7),
- 0.4f to Color(0xFF8E99A8),
- 0.5f to Color(0xFFEDF2F7),
- 0.62f to Color(0xFF9AA5B4),
- 1f to Color(0xFF6C7787),
- ),
- )
- // Hard specular reflection near the top.
- drawRect(
- brush = Brush.verticalGradient(
- colors = listOf(Color.White.copy(alpha = 0.85f), Color.Transparent),
- startY = size.height * 0.05f,
- endY = size.height * 0.22f,
- ),
- )
+private fun DrawScope.drawChrome(isLight: Boolean) {
+ if (isLight) {
+ drawRect(
+ brush = Brush.verticalGradient(
+ 0f to Color(0xFFF8FBFF),
+ 0.16f to Color(0xFFD6DEE7),
+ 0.4f to Color(0xFF8E99A8),
+ 0.5f to Color(0xFFEDF2F7),
+ 0.62f to Color(0xFF9AA5B4),
+ 1f to Color(0xFF6C7787),
+ ),
+ )
+ // Hard specular reflection near the top.
+ drawRect(
+ brush = Brush.verticalGradient(
+ colors = listOf(Color.White.copy(alpha = 0.85f), Color.Transparent),
+ startY = size.height * 0.05f,
+ endY = size.height * 0.22f,
+ ),
+ )
+ } else {
+ // Dark gunmetal for the night variant.
+ drawRect(
+ brush = Brush.verticalGradient(
+ 0f to Color(0xFF3B424C),
+ 0.16f to Color(0xFF2A303A),
+ 0.4f to Color(0xFF1B2028),
+ 0.5f to Color(0xFF343B45),
+ 0.62f to Color(0xFF232830),
+ 1f to Color(0xFF141820),
+ ),
+ )
+ drawRect(
+ brush = Brush.verticalGradient(
+ colors = listOf(Color.White.copy(alpha = 0.18f), Color.Transparent),
+ startY = size.height * 0.05f,
+ endY = size.height * 0.22f,
+ ),
+ )
+ }
// Darkened lower edge for the metal bevel.
drawRect(
brush = Brush.verticalGradient(
@@ -463,3 +501,36 @@ private fun BaseSurfaceGridPreview() {
}
}
}
+
+@BaseUIPhonePreview
+@BaseUIDesktopPreview
+@Composable
+private fun SurfaceThemeBackdropPreview() {
+ val isLight = LocalBaseUIColors.current.isLight
+ val onSurface = LocalBaseUIColors.current.onSurface
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ SurfaceStyle.entries.forEach { style ->
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(66.dp)
+ .clip(RoundedCornerShape(14.dp))
+ .baseSurfaceBackdrop(style, isLight),
+ contentAlignment = Alignment.CenterStart,
+ ) {
+ Text(
+ text = style.name,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold,
+ color = if (style == SurfaceStyle.Watery) Color.White else onSurface,
+ modifier = Modifier.padding(horizontal = 16.dp),
+ )
+ }
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Theme.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Theme.kt
index 9e27fc39..826a3319 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Theme.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Theme.kt
@@ -42,6 +42,8 @@ import dev.krtirtho.spotube.core.ui.base.BaseUITheme.TextFieldTheme
@Stable
data class BaseUITheme(
+ val colors: BaseUIColorScheme,
+ val surfaceTheme: SurfaceStyle,
val buttons: ButtonVariants,
val iconButtons: ButtonVariants,
val textField: TextFieldTheme,
@@ -216,9 +218,12 @@ val LocalBaseUITheme = staticCompositionLocalOf {
}
@Composable
-fun rememberBaseUITheme(): BaseUITheme {
- val scheme = MaterialTheme.colorScheme
- val isLight = scheme.surface.luminance() > 0.5f
+fun rememberBaseUITheme(
+ surfaceTheme: SurfaceStyle = SurfaceStyle.Glass,
+ accentColor: Color? = null,
+): BaseUITheme {
+ val colors = rememberBaseUIColorScheme(surfaceTheme, accentColor)
+ val isLight = colors.isLight
// ------------------------------------------------------------------
// Frutiger-Aero glass palette.
@@ -230,28 +235,28 @@ fun rememberBaseUITheme(): BaseUITheme {
val glassTop = if (isLight) {
Color.White
} else {
- lerp(scheme.surfaceContainerHigh, Color.White, 0.10f)
+ lerp(colors.surfaceContainerHigh, Color.White, 0.10f)
}
val glassBottom = if (isLight) {
- lerp(scheme.surface, Color.White, 0.04f)
+ lerp(colors.surface, Color.White, 0.04f)
} else {
- scheme.surfaceContainer
+ colors.surfaceContainer
}
val glassPressed = if (isLight) {
- lerp(scheme.surfaceContainerHighest, Color.White, 0.06f)
+ lerp(colors.surfaceContainerHighest, Color.White, 0.06f)
} else {
- lerp(scheme.surfaceContainerHighest, Color.Black, 0.08f)
+ lerp(colors.surfaceContainerHighest, Color.Black, 0.08f)
}
val gloss = if (isLight) Color.White.copy(alpha = 0.75f) else Color.White.copy(alpha = 0.18f)
val glossSoft = if (isLight) Color.White.copy(alpha = 0.5f) else Color.White.copy(alpha = 0.12f)
val glassEdge = if (isLight) Color.White.copy(alpha = 0.95f) else Color.White.copy(alpha = 0.22f)
val glassBorder = if (isLight) {
- lerp(scheme.outlineVariant, Color.White, 0.55f)
+ lerp(colors.outlineVariant, Color.White, 0.55f)
} else {
- scheme.outlineVariant.copy(alpha = 0.5f)
+ colors.outlineVariant.copy(alpha = 0.5f)
}
- val shadowColor = scheme.onSurface
+ val shadowColor = colors.onSurface
fun glassBrush(top: Color, bottom: Color): Brush =
Brush.verticalGradient(listOf(top, bottom))
@@ -288,22 +293,22 @@ fun rememberBaseUITheme(): BaseUITheme {
val outlineColors = BaseUITheme.InteractionState(
normal = BaseUITheme.ButtonColors(
background = glassBrush(glassTop, glassBottom),
- foreground = scheme.onSurface,
+ foreground = colors.onSurface,
highlight = gloss,
),
hovered = BaseUITheme.ButtonColors(
background = glassBrush(glassTop, glassBottom),
- foreground = scheme.onSurface,
+ foreground = colors.onSurface,
highlight = Color.White.copy(alpha = if (isLight) 0.9f else 0.26f),
),
pressed = BaseUITheme.ButtonColors(
background = glassBrush(glassPressed, glassPressed),
- foreground = scheme.onSurface,
+ foreground = colors.onSurface,
highlight = glossSoft,
),
focused = BaseUITheme.ButtonColors(
background = glassBrush(glassTop, glassBottom),
- foreground = scheme.onSurface,
+ foreground = colors.onSurface,
highlight = gloss,
),
)
@@ -341,48 +346,48 @@ fun rememberBaseUITheme(): BaseUITheme {
)
// --- Aqua accent (primary) -------------------------------------------
- val primaryEdge = lerp(scheme.primary, Color.White, 0.5f)
+ val primaryEdge = lerp(colors.primary, Color.White, 0.5f)
val primaryColors = BaseUITheme.InteractionState(
- normal = BaseUITheme.ButtonColors(aqua(scheme.primary), scheme.onPrimary, gloss),
+ normal = BaseUITheme.ButtonColors(aqua(colors.primary), colors.onPrimary, gloss),
hovered = BaseUITheme.ButtonColors(
- aqua(scheme.primary),
- scheme.onPrimary,
+ aqua(colors.primary),
+ colors.onPrimary,
Color.White.copy(alpha = if (isLight) 0.9f else 0.3f),
),
pressed = BaseUITheme.ButtonColors(
glassBrush(
- lerp(scheme.primary, Color.Black, 0.16f),
- lerp(scheme.primary, Color.Black, 0.24f),
+ lerp(colors.primary, Color.Black, 0.16f),
+ lerp(colors.primary, Color.Black, 0.24f),
),
- scheme.onPrimary,
+ colors.onPrimary,
glossSoft,
),
- focused = BaseUITheme.ButtonColors(aqua(scheme.primary), scheme.onPrimary, gloss),
+ focused = BaseUITheme.ButtonColors(aqua(colors.primary), colors.onPrimary, gloss),
)
val primaryShadow = BaseUITheme.InteractionState(
normal = BaseUITheme.Shadow(
10.dp,
true,
- scheme.primary.copy(alpha = 0.35f),
- scheme.primary.copy(alpha = 0.42f),
+ colors.primary.copy(alpha = 0.35f),
+ colors.primary.copy(alpha = 0.42f),
),
hovered = BaseUITheme.Shadow(
16.dp,
true,
- scheme.primary.copy(alpha = 0.45f),
- scheme.primary.copy(alpha = 0.5f),
+ colors.primary.copy(alpha = 0.45f),
+ colors.primary.copy(alpha = 0.5f),
),
pressed = BaseUITheme.Shadow(
3.dp,
true,
- scheme.primary.copy(alpha = 0.22f),
- scheme.primary.copy(alpha = 0.28f),
+ colors.primary.copy(alpha = 0.22f),
+ colors.primary.copy(alpha = 0.28f),
),
focused = BaseUITheme.Shadow(
10.dp,
true,
- scheme.primary.copy(alpha = 0.35f),
- scheme.primary.copy(alpha = 0.42f),
+ colors.primary.copy(alpha = 0.35f),
+ colors.primary.copy(alpha = 0.42f),
),
)
val primaryBorder = BaseUITheme.InteractionState(
@@ -393,16 +398,16 @@ fun rememberBaseUITheme(): BaseUITheme {
)
// --- Secondary (soft aqua glass) -------------------------------------
- val secondaryBase = scheme.secondaryContainer
+ val secondaryBase = colors.secondaryContainer
val secondaryColors = BaseUITheme.InteractionState(
normal = BaseUITheme.ButtonColors(
glassBrush(lerp(secondaryBase, Color.White, 0.5f), secondaryBase),
- scheme.onSecondaryContainer,
+ colors.onSecondaryContainer,
gloss,
),
hovered = BaseUITheme.ButtonColors(
glassBrush(lerp(secondaryBase, Color.White, 0.55f), secondaryBase),
- scheme.onSecondaryContainer,
+ colors.onSecondaryContainer,
Color.White.copy(alpha = if (isLight) 0.85f else 0.24f),
),
pressed = BaseUITheme.ButtonColors(
@@ -410,12 +415,12 @@ fun rememberBaseUITheme(): BaseUITheme {
lerp(secondaryBase, Color.Black, 0.08f),
lerp(secondaryBase, Color.Black, 0.14f),
),
- scheme.onSecondaryContainer,
+ colors.onSecondaryContainer,
glossSoft,
),
focused = BaseUITheme.ButtonColors(
glassBrush(lerp(secondaryBase, Color.White, 0.5f), secondaryBase),
- scheme.onSecondaryContainer,
+ colors.onSecondaryContainer,
gloss,
),
)
@@ -456,25 +461,25 @@ fun rememberBaseUITheme(): BaseUITheme {
val ghostColors = BaseUITheme.InteractionState(
normal = BaseUITheme.ButtonColors(
background = glassBrush(Color.Transparent, Color.Transparent),
- foreground = scheme.onSurface,
+ foreground = colors.onSurface,
highlight = Color.Transparent,
),
hovered = BaseUITheme.ButtonColors(
background = glassBrush(
- lerp(scheme.surfaceContainerHigh, Color.White, 0.1f),
- scheme.surfaceContainer,
+ lerp(colors.surfaceContainerHigh, Color.White, 0.1f),
+ colors.surfaceContainer,
),
- foreground = scheme.onSurface,
+ foreground = colors.onSurface,
highlight = glossSoft,
),
pressed = BaseUITheme.ButtonColors(
background = glassBrush(glassPressed, glassPressed),
- foreground = scheme.onSurface,
+ foreground = colors.onSurface,
highlight = Color.Transparent,
),
focused = BaseUITheme.ButtonColors(
background = glassBrush(Color.Transparent, Color.Transparent),
- foreground = scheme.onSurface,
+ foreground = colors.onSurface,
highlight = Color.Transparent,
),
)
@@ -538,10 +543,10 @@ fun rememberBaseUITheme(): BaseUITheme {
)
val textFieldForeground = BaseUITheme.InteractionState(
- normal = scheme.onSurface,
- hovered = scheme.onSurface,
- pressed = scheme.onSurface,
- focused = scheme.onSurface,
+ normal = colors.onSurface,
+ hovered = colors.onSurface,
+ pressed = colors.onSurface,
+ focused = colors.onSurface,
)
val checkBoxShape = RoundedCornerShape(7.dp)
@@ -571,6 +576,8 @@ fun rememberBaseUITheme(): BaseUITheme {
)
return BaseUITheme(
+ colors = colors,
+ surfaceTheme = surfaceTheme,
buttons = BaseUITheme.ButtonVariants(
primary = primaryStyle,
secondary = secondaryStyle,
@@ -630,13 +637,13 @@ fun rememberBaseUITheme(): BaseUITheme {
focused = BaseUITheme.Shadow(
10.dp,
true,
- scheme.primary.copy(alpha = 0.22f),
- scheme.primary.copy(alpha = 0.28f)
+ colors.primary.copy(alpha = 0.22f),
+ colors.primary.copy(alpha = 0.28f)
),
),
padding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
textStyle = TextStyle.Default,
- cursor = SolidColor(scheme.primary),
+ cursor = SolidColor(colors.primary),
foreground = textFieldForeground,
),
autoCompleteMenuTheme = AutoCompleteMenuTheme(
@@ -648,10 +655,10 @@ fun rememberBaseUITheme(): BaseUITheme {
),
slider = SliderTheme(
trackActiveColor = BaseUITheme.InteractionState(
- normal = scheme.primary,
- hovered = scheme.primary,
- pressed = scheme.primary,
- focused = scheme.primary,
+ normal = colors.primary,
+ hovered = colors.primary,
+ pressed = colors.primary,
+ focused = colors.primary,
),
trackInactiveColor = BaseUITheme.InteractionState(
normal = glassBorder,
@@ -660,39 +667,39 @@ fun rememberBaseUITheme(): BaseUITheme {
focused = glassBorder,
),
thumbColor = BaseUITheme.InteractionState(
- normal = scheme.primary,
- hovered = scheme.primary,
- pressed = scheme.primary,
- focused = scheme.primary,
+ normal = colors.primary,
+ hovered = colors.primary,
+ pressed = colors.primary,
+ focused = colors.primary,
),
thumbShadow = BaseUITheme.InteractionState(
normal = BaseUITheme.Shadow(
5.dp,
true,
- scheme.primary.copy(alpha = 0.4f),
- scheme.primary.copy(alpha = 0.45f)
+ colors.primary.copy(alpha = 0.4f),
+ colors.primary.copy(alpha = 0.45f)
),
hovered = BaseUITheme.Shadow(
8.dp,
true,
- scheme.primary.copy(alpha = 0.45f),
- scheme.primary.copy(alpha = 0.5f)
+ colors.primary.copy(alpha = 0.45f),
+ colors.primary.copy(alpha = 0.5f)
),
pressed = BaseUITheme.Shadow(
2.dp,
true,
- scheme.primary.copy(alpha = 0.25f),
- scheme.primary.copy(alpha = 0.3f)
+ colors.primary.copy(alpha = 0.25f),
+ colors.primary.copy(alpha = 0.3f)
),
focused = BaseUITheme.Shadow(
5.dp,
true,
- scheme.primary.copy(alpha = 0.4f),
- scheme.primary.copy(alpha = 0.45f)
+ colors.primary.copy(alpha = 0.4f),
+ colors.primary.copy(alpha = 0.45f)
),
),
stepActiveColor = Color.White.copy(alpha = 0.7f),
- stepInactiveColor = scheme.onSurface.copy(alpha = 0.25f),
+ stepInactiveColor = colors.onSurface.copy(alpha = 0.25f),
),
checkBox = CheckBoxTheme(
selected = BaseUITheme.ButtonStyle(
@@ -709,7 +716,7 @@ fun rememberBaseUITheme(): BaseUITheme {
border = glassBorderState,
padding = noPadding,
),
- checkmarkColor = scheme.onPrimary,
+ checkmarkColor = colors.onPrimary,
),
chipTab = ChipTabTheme(
selected = BaseUITheme.ButtonStyle(
@@ -794,8 +801,8 @@ fun rememberBaseUITheme(): BaseUITheme {
pressed = glassBrush(glassPressed, glassPressed),
focused = glassBrush(glassTop, glassBottom),
selected = glassBrush(
- lerp(scheme.primaryContainer, Color.White, 0.4f),
- scheme.primaryContainer,
+ lerp(colors.primaryContainer, Color.White, 0.4f),
+ colors.primaryContainer,
),
disabled = glassBrush(
glassTop.copy(alpha = 0.5f),
@@ -841,12 +848,12 @@ fun rememberBaseUITheme(): BaseUITheme {
disabled = TextStyle.Default,
),
foreground = BaseUITheme.AdvancedInteractionState(
- normal = scheme.onSurface,
- hovered = scheme.onSurface,
- pressed = scheme.onSurface,
- focused = scheme.onSurface,
- selected = scheme.onSurface,
- disabled = scheme.onSurface.copy(alpha = 0.38f),
+ normal = colors.onSurface,
+ hovered = colors.onSurface,
+ pressed = colors.onSurface,
+ focused = colors.onSurface,
+ selected = colors.onSurface,
+ disabled = colors.onSurface.copy(alpha = 0.38f),
),
),
dialog = BaseUITheme.DialogTheme(
@@ -873,7 +880,7 @@ fun rememberBaseUITheme(): BaseUITheme {
shadowColor.copy(alpha = 0.28f)
),
itemHoverBackground = glassBrush(glassTop, glassBottom),
- itemForeground = scheme.onSurface,
+ itemForeground = colors.onSurface,
itemHighlight = gloss,
padding = PaddingValues(6.dp),
itemPadding = PaddingValues(horizontal = 14.dp, vertical = 11.dp),
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDropdownBottomSheet.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDropdownBottomSheet.kt
index cc1068aa..352f0282 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDropdownBottomSheet.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDropdownBottomSheet.kt
@@ -44,7 +44,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
-import androidx.compose.material3.Scaffold
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
@@ -327,7 +327,7 @@ private fun AdaptiveBottomSheetContent(
@Preview
@Composable
private fun AdaptiveDropdownBottomSheetPreview() {
- Scaffold {
+ BaseScaffold {
AdaptiveDropdownBottomSheet(
items = listOf(
AdaptiveMenuItem(
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionView.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionView.kt
index e4c9dd31..614999e6 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionView.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/CollectionView.kt
@@ -27,7 +27,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
@@ -103,7 +103,7 @@ fun CollectionView(
}
}
- Scaffold(
+ BaseScaffold(
topBar = {
ApplicationMainBar(
backButton = true,
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/ErrorDisplay.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/ErrorDisplay.kt
index 585ba1d5..7038d5b4 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/ErrorDisplay.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/ErrorDisplay.kt
@@ -37,7 +37,7 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -278,7 +278,7 @@ private fun ErrorDisplayPreview() {
@Preview
@Composable
private fun ErrorLogDialogPreview() {
- Scaffold {
+ BaseScaffold {
ErrorLogDialog(
errorMessage = "This is an error message",
onDismiss = {},
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackList.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackList.kt
index a985983a..c092a364 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackList.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackList.kt
@@ -48,7 +48,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -1035,7 +1035,7 @@ fun previewTracks(): List {
@Composable
@Preview
private fun TrackListPreview() {
- Scaffold {
+ BaseScaffold {
TrackList(
tracks = previewTracks(),
hasMore = true,
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/cards/PlayableCard.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/cards/PlayableCard.kt
index 14aee52b..89b9458c 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/cards/PlayableCard.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/cards/PlayableCard.kt
@@ -38,7 +38,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@@ -189,7 +189,7 @@ fun PlayableCard(
@PreviewLightDark
@Composable
private fun PlayableCardPreview() {
- Scaffold {
+ BaseScaffold {
PlayableCard(
title = "Sample Title",
subtitle = "Sample Subtitle",
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/webview/PlatformWebviewScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/webview/PlatformWebviewScreen.kt
index 8751e1ec..7d3373cd 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/webview/PlatformWebviewScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/webview/PlatformWebviewScreen.kt
@@ -34,7 +34,7 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -148,7 +148,7 @@ fun PlatformWebViewScreen(webViewController: WebViewController) {
}
}
- Scaffold(
+ BaseScaffold(
contentWindowInsets = WindowInsets.statusBars,
topBar = {
ApplicationMainBar(
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistScreen.kt
index ca2d0458..5d5afc72 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/artist/ArtistScreen.kt
@@ -42,7 +42,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PlainTooltip
-import androidx.compose.material3.Scaffold
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox
@@ -118,7 +118,7 @@ fun ArtistScreen(
val trackOptionsContext by viewModel.trackOptionsContext.collectAsStateWithLifecycle()
val showAddToPlaylistPicker by viewModel.showAddToPlaylistPicker.collectAsStateWithLifecycle()
- Scaffold(
+ BaseScaffold(
topBar = { ApplicationMainBar() }
) { innerPadding ->
when (val currentState = state) {
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/blacklist/BlacklistScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/blacklist/BlacklistScreen.kt
index 8aad2670..9d2d6261 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/blacklist/BlacklistScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/blacklist/BlacklistScreen.kt
@@ -33,7 +33,6 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -51,6 +50,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
import dev.krtirtho.spotube.core.ui.base.TextField
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
@@ -67,7 +67,7 @@ import org.koin.compose.viewmodel.koinViewModel
@Composable
fun BlacklistScreen() {
val viewModel = koinViewModel()
- Scaffold(
+ BaseScaffold(
topBar = {
ApplicationMainBar(
backButton = true,
@@ -83,7 +83,7 @@ fun BlacklistScreen() {
}
/**
- * The blacklist UI without its own [Scaffold] / app bar, so it can be embedded
+ * The blacklist UI without its own [BaseScaffold] / app bar, so it can be embedded
* as a tab in the large-screen settings layout as well as hosted by [BlacklistScreen].
*/
@Composable
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesScreen.kt
index 95366a4a..a6c06f0d 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/DevicesScreen.kt
@@ -33,7 +33,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
-import androidx.compose.material3.Scaffold
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -77,7 +77,7 @@ fun DevicesScreen(
}
}
- Scaffold(
+ BaseScaffold(
topBar = {
ApplicationMainBar(
backButton = true,
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt
index 31949017..b0b42d5b 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/devices/RemoteControlScreen.kt
@@ -45,7 +45,7 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -107,7 +107,7 @@ fun RemoteControlScreen(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.TopCenter,
) {
- Scaffold(
+ BaseScaffold(
modifier = Modifier
.fillMaxHeight(),
topBar = {
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/home/HomeScreen.kt
index 67924aaf..4b6782fe 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/home/HomeScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/home/HomeScreen.kt
@@ -35,7 +35,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
@@ -120,7 +120,7 @@ fun HomeScreen(viewModel: HomeScreenViewModel) {
}
}
- Scaffold(
+ BaseScaffold(
topBar = {
ApplicationMainBar(
backButton = false,
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/jam/JamScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/jam/JamScreen.kt
index b7f76b00..f05cbc50 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/jam/JamScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/jam/JamScreen.kt
@@ -32,7 +32,7 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Scaffold
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
@@ -71,7 +71,7 @@ fun JamScreen(
val state by viewModel.uiState.collectAsStateWithLifecycle()
val shellBottomInset = LocalAppShellBottomInset.current
- Scaffold(
+ BaseScaffold(
topBar = {
ApplicationMainBar(
backButton = true,
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/LibraryScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/LibraryScreen.kt
index c5d931ed..fdc33bfa 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/LibraryScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/library/LibraryScreen.kt
@@ -27,7 +27,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
-import androidx.compose.material3.Scaffold
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@@ -57,7 +57,7 @@ fun LibraryScreen(
val searchMode by libraryState.searchMode.collectAsState()
val isLargeScreen = appShellViewModel.useSidebar()
- Scaffold(
+ BaseScaffold(
topBar = {
Column {
ApplicationMainBar(
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/lyrics/LyricsScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/lyrics/LyricsScreen.kt
index b1a0cbd4..6797df65 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/lyrics/LyricsScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/lyrics/LyricsScreen.kt
@@ -36,7 +36,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -71,7 +71,7 @@ fun LyricsScreen(
val positionMillis by viewModel.positionMillisFlow.collectAsState()
val scope = rememberCoroutineScope()
- Scaffold(
+ BaseScaffold(
topBar = {
ApplicationMainBar(
backButton = onClose == null,
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginScreen.kt
index 8fcd5baf..d8cc2ca5 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/plugin/PluginScreen.kt
@@ -39,7 +39,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -62,6 +61,7 @@ import coil3.request.ImageRequest
import coil3.request.crossfade
import dev.krtirtho.spotube.PlatformType
import dev.krtirtho.spotube.core.extras.kebabToTitleCase
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import dev.krtirtho.spotube.core.ui.base.Card
import dev.krtirtho.spotube.core.ui.base.OutlineButton
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
@@ -145,13 +145,12 @@ fun PluginScreen(
viewModel: PluginViewModel = koinViewModel(),
onboarding: Boolean = false,
) {
- Scaffold(
+ BaseScaffold(
topBar = {
if (!onboarding) {
ApplicationMainBar(title = { Text(stringResource(Res.string.plugin_screen_title)) })
}
},
- containerColor = if (onboarding) Color.Transparent else MaterialTheme.colorScheme.surface,
) { innerPadding ->
PluginScreenContent(
viewModel = viewModel,
@@ -162,7 +161,7 @@ fun PluginScreen(
}
/**
- * The plugin management UI without its own [Scaffold] / app bar, so it can be embedded
+ * The plugin management UI without its own [BaseScaffold] / app bar, so it can be embedded
* as a tab in the large-screen settings layout as well as hosted by [PluginScreen].
*/
@Composable
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreen.kt
index bb7fd14b..ba3a837e 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreen.kt
@@ -53,7 +53,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -268,7 +268,7 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
remotePlaybackController.requestTracksPlayNext(tracks, "Search results")
}
- Scaffold(
+ BaseScaffold(
topBar = { ApplicationMainBar(backButton = false) }
) { innerPadding ->
Column(
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/JamSettingsScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/JamSettingsScreen.kt
index 6630b844..da022080 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/JamSettingsScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/JamSettingsScreen.kt
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.Composable
@@ -16,6 +15,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
import dev.krtirtho.spotube.modules.settings.components.SwitchSettingCard
@@ -59,7 +59,7 @@ import spotube.composeapp.generated.resources.settings_section_jam
fun JamSettingsScreen(
viewModel: JamSettingsViewModel = koinViewModel(),
) {
- Scaffold(
+ BaseScaffold(
topBar = {
ApplicationMainBar(
title = { Text(stringResource(Res.string.settings_section_jam)) },
@@ -74,7 +74,7 @@ fun JamSettingsScreen(
}
/**
- * The Group Jam settings UI without its own [Scaffold] / app bar, so it can be embedded
+ * The Group Jam settings UI without its own [BaseScaffold] / app bar, so it can be embedded
* as a tab in the large-screen settings layout as well as hosted by [JamSettingsScreen].
*/
@Composable
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsModels.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsModels.kt
index 55173afc..d51fb20e 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsModels.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsModels.kt
@@ -19,6 +19,7 @@ package dev.krtirtho.spotube.modules.settings
import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioFormat
import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioQuality
+import dev.krtirtho.spotube.core.ui.base.SurfaceStyle
import kotlinx.serialization.Serializable
@Serializable
@@ -35,6 +36,7 @@ data class UserSettings(
// Appearance
val theme: Theme = Theme.SYSTEM,
val accentColor: AccentColors = AccentColors.GREEN_GOBLIN,
+ val surfaceTheme: SurfaceStyle = SurfaceStyle.Glass,
// Playback
val streamingMusicFormat: AudioFormat = AudioFormat(
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsScreen.kt
index 4430015c..694c9d18 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/SettingsScreen.kt
@@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -39,6 +38,7 @@ import dev.krtirtho.spotube.getPlatform
import dev.krtirtho.spotube.core.discovery.rememberLocalNetworkPermissionRequester
import dev.krtirtho.spotube.core.navigation.NavigationCommands
import dev.krtirtho.spotube.core.navigation.Routes
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
import spotube.composeapp.generated.resources.*
import dev.krtirtho.spotube.modules.settings.sections.appearanceSection
@@ -91,7 +91,7 @@ private fun CompactSettingsScreen(settingsViewModel: SettingsViewModel) {
PaddingValues(top = 16.dp, bottom = 16.dp + shellBottomInset)
}
- Scaffold(
+ BaseScaffold(
topBar = {
ApplicationMainBar(
title = {
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/AppearanceSection.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/AppearanceSection.kt
index bc178108..54331e6f 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/AppearanceSection.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/settings/sections/AppearanceSection.kt
@@ -52,6 +52,7 @@ import dev.krtirtho.spotube.core.ui.base.ThemedDialog
import dev.krtirtho.spotube.core.ui.base.OutlineButton
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
import dev.krtirtho.spotube.core.ui.base.Radio
+import dev.krtirtho.spotube.core.ui.base.SurfaceStyle
import dev.krtirtho.spotube.modules.settings.AccentColors
import dev.krtirtho.spotube.modules.settings.SettingsViewModel
import dev.krtirtho.spotube.modules.settings.Theme
@@ -61,6 +62,7 @@ import dev.krtirtho.spotube.modules.settings.components.SettingCardItem
import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxColorSwatch
import dev.krtirtho.spotube.resources.iconsax.IconsaxColorsSquare
+import dev.krtirtho.spotube.resources.iconsax.IconsaxMagic
import org.jetbrains.compose.resources.stringResource
import spotube.composeapp.generated.resources.*
@@ -94,6 +96,29 @@ internal fun LazyListScope.appearanceSection(
}
)
},
+ {
+ SelectionSettingCard(
+ title = stringResource(Res.string.settings_surface_theme_title),
+ subtitle = stringResource(
+ Res.string.settings_surface_theme_subtitle_current,
+ settings.surfaceTheme.displayLabel()
+ ),
+ icon = {
+ SettingsItemIcon(
+ Iconsax.IconsaxMagic,
+ stringResource(Res.string.settings_surface_theme_title)
+ )
+ },
+ selectedOption = settings.surfaceTheme,
+ options = SurfaceStyle.entries,
+ optionLabel = { it.displayLabel() },
+ onOptionSelected = { surfaceTheme ->
+ settingsViewModel.updateSettings {
+ copy(surfaceTheme = surfaceTheme)
+ }
+ }
+ )
+ },
{
AccentColorSettingCard(
selectedAccent = settings.accentColor,
@@ -120,6 +145,17 @@ private fun Theme.displayLabel(): String {
}
}
+@Composable
+private fun SurfaceStyle.displayLabel(): String {
+ return when (this) {
+ SurfaceStyle.Watery -> stringResource(Res.string.settings_surface_watery)
+ SurfaceStyle.Icy -> stringResource(Res.string.settings_surface_icy)
+ SurfaceStyle.Chrome -> stringResource(Res.string.settings_surface_chrome)
+ SurfaceStyle.Acrylic -> stringResource(Res.string.settings_surface_acrylic)
+ SurfaceStyle.Glass -> stringResource(Res.string.settings_surface_glass)
+ }
+}
+
@Composable
private fun AccentColors.displayLabel(): String {
return when (this) {
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppExpandedPlayer.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppExpandedPlayer.kt
index 0016bc80..c43837fd 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppExpandedPlayer.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppExpandedPlayer.kt
@@ -47,7 +47,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
-import androidx.compose.material3.Scaffold
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
@@ -235,7 +235,7 @@ fun AppExpandedPlayer(
}
}
- Scaffold(
+ BaseScaffold(
modifier = modifier.fillMaxSize(),
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { innerPadding ->
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppShell.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppShell.kt
index 4d64ed63..6e79cfa3 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppShell.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppShell.kt
@@ -74,6 +74,8 @@ import dev.krtirtho.spotube.core.navigation.Navigator
import dev.krtirtho.spotube.core.navigation.Routes
import dev.krtirtho.spotube.core.remote.ConnectionRequestDialogHost
import dev.krtirtho.spotube.core.remote.RemotePlaybackController
+import dev.krtirtho.spotube.core.ui.base.LocalBaseUIColors
+import dev.krtirtho.spotube.core.ui.base.baseSurfaceBackdrop
import dev.krtirtho.spotube.modules.devices.PlayDestinationPickerHost
import dev.krtirtho.spotube.modules.lyrics.LyricsScreen
import dev.krtirtho.spotube.modules.shell.alternative_track.AlternativeTrackContent
@@ -100,6 +102,7 @@ fun AppShell(
) {
val navigatorCommands: NavigationCommands = koinInject()
val remotePlaybackController: RemotePlaybackController = koinInject()
+ val baseUIColors = LocalBaseUIColors.current
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(remotePlaybackController) {
remotePlaybackController.events.collect { message ->
@@ -156,7 +159,7 @@ fun AppShell(
Box(
modifier = Modifier
.fillMaxSize()
- .background(MaterialTheme.colorScheme.surface)
+ .baseSurfaceBackdrop(baseUIColors.surfaceTheme, baseUIColors.isLight)
.hazeSource(state = bgHazeState) // <-- Put it here!
)
Row(modifier = Modifier.fillMaxSize()) {
@@ -229,6 +232,11 @@ fun AppShell(
}
} else {
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .baseSurfaceBackdrop(baseUIColors.surfaceTheme, baseUIColors.isLight),
+ )
CompactPlayerOverlay(
navigator = navigator,
navigationState = navigationState,
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/welcome/WelcomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/welcome/WelcomeScreen.kt
index 94c2055e..61311d76 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/welcome/WelcomeScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/welcome/WelcomeScreen.kt
@@ -41,7 +41,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
+import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -79,7 +79,7 @@ fun WelcomeScreen(viewModel: WelcomeViewModel) {
WelcomeTheme {
Box(Modifier.fillMaxSize()) {
WelcomeBackground()
- Scaffold(
+ BaseScaffold(
containerColor = Color.Transparent,
topBar = {
ApplicationMainBar(
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/welcome/WelcomeVisuals.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/welcome/WelcomeVisuals.kt
index d5e50838..c06a5b98 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/welcome/WelcomeVisuals.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/welcome/WelcomeVisuals.kt
@@ -29,6 +29,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.TextUnit
+import dev.krtirtho.spotube.core.ui.base.LocalBaseUIColors
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
import dev.krtirtho.spotube.core.ui.base.rememberBaseUITheme
import org.jetbrains.compose.resources.Font
@@ -60,7 +61,11 @@ fun WelcomeTheme(content: @Composable () -> Unit) {
outlineVariant = Color(0xFFD2D0CC),
)
MaterialTheme(colorScheme = scheme) {
- CompositionLocalProvider(LocalBaseUITheme provides rememberBaseUITheme()) {
+ val baseTheme = rememberBaseUITheme()
+ CompositionLocalProvider(
+ LocalBaseUITheme provides baseTheme,
+ LocalBaseUIColors provides baseTheme.colors,
+ ) {
content()
}
}
diff --git a/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/ui/component/ApplicationMainBar.jvm.kt b/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/ui/component/ApplicationMainBar.jvm.kt
index fb1c63fe..a55f1c85 100644
--- a/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/ui/component/ApplicationMainBar.jvm.kt
+++ b/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/ui/component/ApplicationMainBar.jvm.kt
@@ -41,6 +41,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.WindowPlacement
import dev.krtirtho.spotube.core.systemtray.SystemTrayService
+import dev.krtirtho.spotube.core.ui.base.LocalBaseUIColors
import dev.krtirtho.spotube.modules.settings.SettingsProvider
import dev.krtirtho.spotube.resources.iconsax.FluentDismiss
import dev.krtirtho.spotube.resources.iconsax.FluentMaximize
@@ -63,6 +64,7 @@ actual fun ApplicationMainBar(
backButton: Boolean,
transparent: Boolean,
) {
+ val colors = LocalBaseUIColors.current
Box(
modifier = Modifier.fillMaxWidth()
.windowDragArea(doubleClickAction = WindowDoubleClickAction.ToggleMaximize)
@@ -82,9 +84,19 @@ actual fun ApplicationMainBar(
TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent,
+ titleContentColor = colors.onSurface,
+ actionIconContentColor = colors.onSurfaceVariant,
+ navigationIconContentColor = colors.onSurfaceVariant,
)
} else {
- TopAppBarDefaults.topAppBarColors()
+ // The app bar follows the selected surface theme rather than Material's.
+ TopAppBarDefaults.topAppBarColors(
+ containerColor = colors.surface,
+ scrolledContainerColor = colors.surfaceContainer,
+ titleContentColor = colors.onSurface,
+ actionIconContentColor = colors.onSurfaceVariant,
+ navigationIconContentColor = colors.onSurfaceVariant,
+ )
},
modifier = Modifier.padding(end = 125.dp) // To avoid overlap with window buttons
)
diff --git a/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/ui/component/VerticalScrollbar.jvm.kt b/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/ui/component/VerticalScrollbar.jvm.kt
index e38e6397..bc56ea24 100644
--- a/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/ui/component/VerticalScrollbar.jvm.kt
+++ b/composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/core/ui/component/VerticalScrollbar.jvm.kt
@@ -17,26 +17,43 @@
package dev.krtirtho.spotube.core.ui.component
+import androidx.compose.foundation.LocalScrollbarStyle
+import androidx.compose.foundation.ScrollbarStyle
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.rememberScrollbarAdapter
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
+import dev.krtirtho.spotube.core.ui.base.LocalBaseUIColors
@Composable
actual fun VerticalScrollbar(
listState: LazyListState,
modifier: Modifier
) {
- VerticalScrollbar(
- adapter = rememberScrollbarAdapter(listState),
- modifier = modifier.fillMaxHeight()
- .padding(end = 2.dp)
- .padding(vertical = 16.dp)
- .width(12.dp),
- )
+ val colors = LocalBaseUIColors.current
+ CompositionLocalProvider(
+ LocalScrollbarStyle provides ScrollbarStyle(
+ minimalHeight = 16.dp,
+ thickness = 8.dp,
+ shape = RoundedCornerShape(4.dp),
+ hoverDurationMillis = 300,
+ unhoverColor = colors.onSurface.copy(alpha = 0.18f),
+ hoverColor = colors.onSurface.copy(alpha = 0.4f),
+ ),
+ ) {
+ VerticalScrollbar(
+ adapter = rememberScrollbarAdapter(listState),
+ modifier = modifier.fillMaxHeight()
+ .padding(end = 2.dp)
+ .padding(vertical = 16.dp)
+ .width(12.dp),
+ )
+ }
}
\ No newline at end of file