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