feat(ui): replace Scaffold with BaseScaffold for consistent surface theming across screens

This commit is contained in:
Kingkor Roy Tirtho 2026-10-07 18:45:22 +06:00
parent 4da5b25a5c
commit 9f96b07ebd
33 changed files with 761 additions and 157 deletions

View File

@ -44,6 +44,14 @@
<string name="settings_theme_dark">Dark</string> <string name="settings_theme_dark">Dark</string>
<string name="settings_theme_system">System</string> <string name="settings_theme_system">System</string>
<string name="settings_surface_theme_title">Surface</string>
<string name="settings_surface_theme_subtitle_current">Background material of the app. Current: %1$s</string>
<string name="settings_surface_watery">Watery</string>
<string name="settings_surface_icy">Icy</string>
<string name="settings_surface_chrome">Chrome</string>
<string name="settings_surface_acrylic">Acrylic</string>
<string name="settings_surface_glass">Glass</string>
<string name="settings_accent_title">Accent color</string> <string name="settings_accent_title">Accent color</string>
<string name="settings_accent_subtitle_current">Current palette: %1$s</string> <string name="settings_accent_subtitle_current">Current palette: %1$s</string>
<string name="settings_accent_dialog_title">Accent color</string> <string name="settings_accent_dialog_title">Accent color</string>

View File

@ -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.TOP_LEVEL_ROUTES
import dev.krtirtho.spotube.core.navigation.rememberNavigationState import dev.krtirtho.spotube.core.navigation.rememberNavigationState
import dev.krtirtho.spotube.core.navigation.toEntries 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.LocalBaseUITheme
import dev.krtirtho.spotube.core.ui.base.rememberBaseUITheme import dev.krtirtho.spotube.core.ui.base.rememberBaseUITheme
import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope
@ -156,8 +157,11 @@ fun App(
} }
SpotubeTheme(settings = userSettings) { SpotubeTheme(settings = userSettings) {
val baseUITheme = rememberBaseUITheme() val baseUITheme = rememberBaseUITheme(surfaceTheme = userSettings.surfaceTheme)
CompositionLocalProvider(LocalBaseUITheme provides baseUITheme) { CompositionLocalProvider(
LocalBaseUITheme provides baseUITheme,
LocalBaseUIColors provides baseUITheme.colors,
) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
val currentRoute = val currentRoute =
navigationState.backStacks[navigationState.topLevelRoute]?.last() navigationState.backStacks[navigationState.topLevelRoute]?.last()

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<BaseUIColorScheme> {
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),
)
}
}
}

View File

@ -48,15 +48,20 @@ import androidx.compose.ui.tooling.preview.Preview
@Composable @Composable
internal fun BaseUIPreview( internal fun BaseUIPreview(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
surfaceTheme: SurfaceStyle = SurfaceStyle.Glass,
content: @Composable BoxScope.() -> Unit, content: @Composable BoxScope.() -> Unit,
) { ) {
MaterialTheme( MaterialTheme(
colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme(), colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme(),
) { ) {
CompositionLocalProvider(LocalBaseUITheme provides rememberBaseUITheme()) { val theme = rememberBaseUITheme(surfaceTheme = surfaceTheme)
CompositionLocalProvider(
LocalBaseUITheme provides theme,
LocalBaseUIColors provides theme.colors,
) {
Surface( Surface(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background, color = theme.colors.surface,
) { ) {
Box(modifier = Modifier.fillMaxSize(), content = content) Box(modifier = Modifier.fillMaxSize(), content = content)
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}
}
}

View File

@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text 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 androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlin.random.Random import kotlin.random.Random
import kotlinx.serialization.Serializable
/** /**
* The Frutiger-Aero material families available to [BaseSurface]. * The Frutiger-Aero material families available to [BaseSurface].
@ -61,7 +63,10 @@ import kotlin.random.Random
* - [Chrome]: brushed metal with a hard specular reflection. * - [Chrome]: brushed metal with a hard specular reflection.
* - [Acrylic]: translucent tinted panel with fine grain. * - [Acrylic]: translucent tinted panel with fine grain.
* - [Glass]: clear, glossy glass with a strong sheen. * - [Glass]: clear, glossy glass with a strong sheen.
*
* Each style doubles as a full app theme (see [BaseUIColorScheme]).
*/ */
@Serializable
enum class SurfaceStyle { enum class SurfaceStyle {
Watery, Watery,
Icy, Icy,
@ -101,7 +106,7 @@ fun BaseSurface(
val resolvedContentColor = contentColor ?: when (style) { val resolvedContentColor = contentColor ?: when (style) {
SurfaceStyle.Watery -> Color.White SurfaceStyle.Watery -> Color.White
SurfaceStyle.Icy -> if (isLight) Color(0xFF0B3A55) else Color(0xFFDCEBF7) 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 SurfaceStyle.Acrylic, SurfaceStyle.Glass -> onSurface
} }
val ambient = when (style) { 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) { private fun DrawScope.drawSurfaceMaterial(style: SurfaceStyle, isLight: Boolean) {
drawSurfaceBase(style, isLight)
drawInnerBevel(isLight)
}
private fun DrawScope.drawSurfaceBase(style: SurfaceStyle, isLight: Boolean) {
when (style) { when (style) {
SurfaceStyle.Watery -> drawWatery(isLight) SurfaceStyle.Watery -> drawWatery(isLight)
SurfaceStyle.Icy -> drawIcy(isLight) SurfaceStyle.Icy -> drawIcy(isLight)
SurfaceStyle.Chrome -> drawChrome() SurfaceStyle.Chrome -> drawChrome(isLight)
SurfaceStyle.Acrylic -> drawAcrylic(isLight) SurfaceStyle.Acrylic -> drawAcrylic(isLight)
SurfaceStyle.Glass -> drawGlass(isLight) SurfaceStyle.Glass -> drawGlass(isLight)
} }
@ -191,7 +210,6 @@ private fun DrawScope.drawGlass(isLight: Boolean) {
) )
drawGloss(if (isLight) 0.55f else 0.16f) drawGloss(if (isLight) 0.55f else 0.16f)
drawBottomReflection(if (isLight) 0.4f else 0.1f) drawBottomReflection(if (isLight) 0.4f else 0.1f)
drawInnerBevel(isLight)
drawBubbles(seed = 11, count = 3, alpha = if (isLight) 0.4f else 0.14f) drawBubbles(seed = 11, count = 3, alpha = if (isLight) 0.4f else 0.14f)
} }
@ -222,10 +240,10 @@ private fun DrawScope.drawAcrylic(isLight: Boolean) {
) )
} }
drawGloss(if (isLight) 0.4f else 0.12f, heightFraction = 0.45f) drawGloss(if (isLight) 0.4f else 0.12f, heightFraction = 0.45f)
drawInnerBevel(isLight)
} }
private fun DrawScope.drawChrome() { private fun DrawScope.drawChrome(isLight: Boolean) {
if (isLight) {
drawRect( drawRect(
brush = Brush.verticalGradient( brush = Brush.verticalGradient(
0f to Color(0xFFF8FBFF), 0f to Color(0xFFF8FBFF),
@ -244,6 +262,26 @@ private fun DrawScope.drawChrome() {
endY = size.height * 0.22f, 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. // Darkened lower edge for the metal bevel.
drawRect( drawRect(
brush = Brush.verticalGradient( 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),
)
}
}
}
}

View File

@ -42,6 +42,8 @@ import dev.krtirtho.spotube.core.ui.base.BaseUITheme.TextFieldTheme
@Stable @Stable
data class BaseUITheme( data class BaseUITheme(
val colors: BaseUIColorScheme,
val surfaceTheme: SurfaceStyle,
val buttons: ButtonVariants, val buttons: ButtonVariants,
val iconButtons: ButtonVariants, val iconButtons: ButtonVariants,
val textField: TextFieldTheme, val textField: TextFieldTheme,
@ -216,9 +218,12 @@ val LocalBaseUITheme = staticCompositionLocalOf<BaseUITheme> {
} }
@Composable @Composable
fun rememberBaseUITheme(): BaseUITheme { fun rememberBaseUITheme(
val scheme = MaterialTheme.colorScheme surfaceTheme: SurfaceStyle = SurfaceStyle.Glass,
val isLight = scheme.surface.luminance() > 0.5f accentColor: Color? = null,
): BaseUITheme {
val colors = rememberBaseUIColorScheme(surfaceTheme, accentColor)
val isLight = colors.isLight
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// Frutiger-Aero glass palette. // Frutiger-Aero glass palette.
@ -230,28 +235,28 @@ fun rememberBaseUITheme(): BaseUITheme {
val glassTop = if (isLight) { val glassTop = if (isLight) {
Color.White Color.White
} else { } else {
lerp(scheme.surfaceContainerHigh, Color.White, 0.10f) lerp(colors.surfaceContainerHigh, Color.White, 0.10f)
} }
val glassBottom = if (isLight) { val glassBottom = if (isLight) {
lerp(scheme.surface, Color.White, 0.04f) lerp(colors.surface, Color.White, 0.04f)
} else { } else {
scheme.surfaceContainer colors.surfaceContainer
} }
val glassPressed = if (isLight) { val glassPressed = if (isLight) {
lerp(scheme.surfaceContainerHighest, Color.White, 0.06f) lerp(colors.surfaceContainerHighest, Color.White, 0.06f)
} else { } 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 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 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 glassEdge = if (isLight) Color.White.copy(alpha = 0.95f) else Color.White.copy(alpha = 0.22f)
val glassBorder = if (isLight) { val glassBorder = if (isLight) {
lerp(scheme.outlineVariant, Color.White, 0.55f) lerp(colors.outlineVariant, Color.White, 0.55f)
} else { } 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 = fun glassBrush(top: Color, bottom: Color): Brush =
Brush.verticalGradient(listOf(top, bottom)) Brush.verticalGradient(listOf(top, bottom))
@ -288,22 +293,22 @@ fun rememberBaseUITheme(): BaseUITheme {
val outlineColors = BaseUITheme.InteractionState( val outlineColors = BaseUITheme.InteractionState(
normal = BaseUITheme.ButtonColors( normal = BaseUITheme.ButtonColors(
background = glassBrush(glassTop, glassBottom), background = glassBrush(glassTop, glassBottom),
foreground = scheme.onSurface, foreground = colors.onSurface,
highlight = gloss, highlight = gloss,
), ),
hovered = BaseUITheme.ButtonColors( hovered = BaseUITheme.ButtonColors(
background = glassBrush(glassTop, glassBottom), background = glassBrush(glassTop, glassBottom),
foreground = scheme.onSurface, foreground = colors.onSurface,
highlight = Color.White.copy(alpha = if (isLight) 0.9f else 0.26f), highlight = Color.White.copy(alpha = if (isLight) 0.9f else 0.26f),
), ),
pressed = BaseUITheme.ButtonColors( pressed = BaseUITheme.ButtonColors(
background = glassBrush(glassPressed, glassPressed), background = glassBrush(glassPressed, glassPressed),
foreground = scheme.onSurface, foreground = colors.onSurface,
highlight = glossSoft, highlight = glossSoft,
), ),
focused = BaseUITheme.ButtonColors( focused = BaseUITheme.ButtonColors(
background = glassBrush(glassTop, glassBottom), background = glassBrush(glassTop, glassBottom),
foreground = scheme.onSurface, foreground = colors.onSurface,
highlight = gloss, highlight = gloss,
), ),
) )
@ -341,48 +346,48 @@ fun rememberBaseUITheme(): BaseUITheme {
) )
// --- Aqua accent (primary) ------------------------------------------- // --- 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( 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( hovered = BaseUITheme.ButtonColors(
aqua(scheme.primary), aqua(colors.primary),
scheme.onPrimary, colors.onPrimary,
Color.White.copy(alpha = if (isLight) 0.9f else 0.3f), Color.White.copy(alpha = if (isLight) 0.9f else 0.3f),
), ),
pressed = BaseUITheme.ButtonColors( pressed = BaseUITheme.ButtonColors(
glassBrush( glassBrush(
lerp(scheme.primary, Color.Black, 0.16f), lerp(colors.primary, Color.Black, 0.16f),
lerp(scheme.primary, Color.Black, 0.24f), lerp(colors.primary, Color.Black, 0.24f),
), ),
scheme.onPrimary, colors.onPrimary,
glossSoft, glossSoft,
), ),
focused = BaseUITheme.ButtonColors(aqua(scheme.primary), scheme.onPrimary, gloss), focused = BaseUITheme.ButtonColors(aqua(colors.primary), colors.onPrimary, gloss),
) )
val primaryShadow = BaseUITheme.InteractionState( val primaryShadow = BaseUITheme.InteractionState(
normal = BaseUITheme.Shadow( normal = BaseUITheme.Shadow(
10.dp, 10.dp,
true, true,
scheme.primary.copy(alpha = 0.35f), colors.primary.copy(alpha = 0.35f),
scheme.primary.copy(alpha = 0.42f), colors.primary.copy(alpha = 0.42f),
), ),
hovered = BaseUITheme.Shadow( hovered = BaseUITheme.Shadow(
16.dp, 16.dp,
true, true,
scheme.primary.copy(alpha = 0.45f), colors.primary.copy(alpha = 0.45f),
scheme.primary.copy(alpha = 0.5f), colors.primary.copy(alpha = 0.5f),
), ),
pressed = BaseUITheme.Shadow( pressed = BaseUITheme.Shadow(
3.dp, 3.dp,
true, true,
scheme.primary.copy(alpha = 0.22f), colors.primary.copy(alpha = 0.22f),
scheme.primary.copy(alpha = 0.28f), colors.primary.copy(alpha = 0.28f),
), ),
focused = BaseUITheme.Shadow( focused = BaseUITheme.Shadow(
10.dp, 10.dp,
true, true,
scheme.primary.copy(alpha = 0.35f), colors.primary.copy(alpha = 0.35f),
scheme.primary.copy(alpha = 0.42f), colors.primary.copy(alpha = 0.42f),
), ),
) )
val primaryBorder = BaseUITheme.InteractionState( val primaryBorder = BaseUITheme.InteractionState(
@ -393,16 +398,16 @@ fun rememberBaseUITheme(): BaseUITheme {
) )
// --- Secondary (soft aqua glass) ------------------------------------- // --- Secondary (soft aqua glass) -------------------------------------
val secondaryBase = scheme.secondaryContainer val secondaryBase = colors.secondaryContainer
val secondaryColors = BaseUITheme.InteractionState( val secondaryColors = BaseUITheme.InteractionState(
normal = BaseUITheme.ButtonColors( normal = BaseUITheme.ButtonColors(
glassBrush(lerp(secondaryBase, Color.White, 0.5f), secondaryBase), glassBrush(lerp(secondaryBase, Color.White, 0.5f), secondaryBase),
scheme.onSecondaryContainer, colors.onSecondaryContainer,
gloss, gloss,
), ),
hovered = BaseUITheme.ButtonColors( hovered = BaseUITheme.ButtonColors(
glassBrush(lerp(secondaryBase, Color.White, 0.55f), secondaryBase), glassBrush(lerp(secondaryBase, Color.White, 0.55f), secondaryBase),
scheme.onSecondaryContainer, colors.onSecondaryContainer,
Color.White.copy(alpha = if (isLight) 0.85f else 0.24f), Color.White.copy(alpha = if (isLight) 0.85f else 0.24f),
), ),
pressed = BaseUITheme.ButtonColors( pressed = BaseUITheme.ButtonColors(
@ -410,12 +415,12 @@ fun rememberBaseUITheme(): BaseUITheme {
lerp(secondaryBase, Color.Black, 0.08f), lerp(secondaryBase, Color.Black, 0.08f),
lerp(secondaryBase, Color.Black, 0.14f), lerp(secondaryBase, Color.Black, 0.14f),
), ),
scheme.onSecondaryContainer, colors.onSecondaryContainer,
glossSoft, glossSoft,
), ),
focused = BaseUITheme.ButtonColors( focused = BaseUITheme.ButtonColors(
glassBrush(lerp(secondaryBase, Color.White, 0.5f), secondaryBase), glassBrush(lerp(secondaryBase, Color.White, 0.5f), secondaryBase),
scheme.onSecondaryContainer, colors.onSecondaryContainer,
gloss, gloss,
), ),
) )
@ -456,25 +461,25 @@ fun rememberBaseUITheme(): BaseUITheme {
val ghostColors = BaseUITheme.InteractionState( val ghostColors = BaseUITheme.InteractionState(
normal = BaseUITheme.ButtonColors( normal = BaseUITheme.ButtonColors(
background = glassBrush(Color.Transparent, Color.Transparent), background = glassBrush(Color.Transparent, Color.Transparent),
foreground = scheme.onSurface, foreground = colors.onSurface,
highlight = Color.Transparent, highlight = Color.Transparent,
), ),
hovered = BaseUITheme.ButtonColors( hovered = BaseUITheme.ButtonColors(
background = glassBrush( background = glassBrush(
lerp(scheme.surfaceContainerHigh, Color.White, 0.1f), lerp(colors.surfaceContainerHigh, Color.White, 0.1f),
scheme.surfaceContainer, colors.surfaceContainer,
), ),
foreground = scheme.onSurface, foreground = colors.onSurface,
highlight = glossSoft, highlight = glossSoft,
), ),
pressed = BaseUITheme.ButtonColors( pressed = BaseUITheme.ButtonColors(
background = glassBrush(glassPressed, glassPressed), background = glassBrush(glassPressed, glassPressed),
foreground = scheme.onSurface, foreground = colors.onSurface,
highlight = Color.Transparent, highlight = Color.Transparent,
), ),
focused = BaseUITheme.ButtonColors( focused = BaseUITheme.ButtonColors(
background = glassBrush(Color.Transparent, Color.Transparent), background = glassBrush(Color.Transparent, Color.Transparent),
foreground = scheme.onSurface, foreground = colors.onSurface,
highlight = Color.Transparent, highlight = Color.Transparent,
), ),
) )
@ -538,10 +543,10 @@ fun rememberBaseUITheme(): BaseUITheme {
) )
val textFieldForeground = BaseUITheme.InteractionState( val textFieldForeground = BaseUITheme.InteractionState(
normal = scheme.onSurface, normal = colors.onSurface,
hovered = scheme.onSurface, hovered = colors.onSurface,
pressed = scheme.onSurface, pressed = colors.onSurface,
focused = scheme.onSurface, focused = colors.onSurface,
) )
val checkBoxShape = RoundedCornerShape(7.dp) val checkBoxShape = RoundedCornerShape(7.dp)
@ -571,6 +576,8 @@ fun rememberBaseUITheme(): BaseUITheme {
) )
return BaseUITheme( return BaseUITheme(
colors = colors,
surfaceTheme = surfaceTheme,
buttons = BaseUITheme.ButtonVariants( buttons = BaseUITheme.ButtonVariants(
primary = primaryStyle, primary = primaryStyle,
secondary = secondaryStyle, secondary = secondaryStyle,
@ -630,13 +637,13 @@ fun rememberBaseUITheme(): BaseUITheme {
focused = BaseUITheme.Shadow( focused = BaseUITheme.Shadow(
10.dp, 10.dp,
true, true,
scheme.primary.copy(alpha = 0.22f), colors.primary.copy(alpha = 0.22f),
scheme.primary.copy(alpha = 0.28f) colors.primary.copy(alpha = 0.28f)
), ),
), ),
padding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), padding = PaddingValues(horizontal = 16.dp, vertical = 12.dp),
textStyle = TextStyle.Default, textStyle = TextStyle.Default,
cursor = SolidColor(scheme.primary), cursor = SolidColor(colors.primary),
foreground = textFieldForeground, foreground = textFieldForeground,
), ),
autoCompleteMenuTheme = AutoCompleteMenuTheme( autoCompleteMenuTheme = AutoCompleteMenuTheme(
@ -648,10 +655,10 @@ fun rememberBaseUITheme(): BaseUITheme {
), ),
slider = SliderTheme( slider = SliderTheme(
trackActiveColor = BaseUITheme.InteractionState( trackActiveColor = BaseUITheme.InteractionState(
normal = scheme.primary, normal = colors.primary,
hovered = scheme.primary, hovered = colors.primary,
pressed = scheme.primary, pressed = colors.primary,
focused = scheme.primary, focused = colors.primary,
), ),
trackInactiveColor = BaseUITheme.InteractionState( trackInactiveColor = BaseUITheme.InteractionState(
normal = glassBorder, normal = glassBorder,
@ -660,39 +667,39 @@ fun rememberBaseUITheme(): BaseUITheme {
focused = glassBorder, focused = glassBorder,
), ),
thumbColor = BaseUITheme.InteractionState( thumbColor = BaseUITheme.InteractionState(
normal = scheme.primary, normal = colors.primary,
hovered = scheme.primary, hovered = colors.primary,
pressed = scheme.primary, pressed = colors.primary,
focused = scheme.primary, focused = colors.primary,
), ),
thumbShadow = BaseUITheme.InteractionState( thumbShadow = BaseUITheme.InteractionState(
normal = BaseUITheme.Shadow( normal = BaseUITheme.Shadow(
5.dp, 5.dp,
true, true,
scheme.primary.copy(alpha = 0.4f), colors.primary.copy(alpha = 0.4f),
scheme.primary.copy(alpha = 0.45f) colors.primary.copy(alpha = 0.45f)
), ),
hovered = BaseUITheme.Shadow( hovered = BaseUITheme.Shadow(
8.dp, 8.dp,
true, true,
scheme.primary.copy(alpha = 0.45f), colors.primary.copy(alpha = 0.45f),
scheme.primary.copy(alpha = 0.5f) colors.primary.copy(alpha = 0.5f)
), ),
pressed = BaseUITheme.Shadow( pressed = BaseUITheme.Shadow(
2.dp, 2.dp,
true, true,
scheme.primary.copy(alpha = 0.25f), colors.primary.copy(alpha = 0.25f),
scheme.primary.copy(alpha = 0.3f) colors.primary.copy(alpha = 0.3f)
), ),
focused = BaseUITheme.Shadow( focused = BaseUITheme.Shadow(
5.dp, 5.dp,
true, true,
scheme.primary.copy(alpha = 0.4f), colors.primary.copy(alpha = 0.4f),
scheme.primary.copy(alpha = 0.45f) colors.primary.copy(alpha = 0.45f)
), ),
), ),
stepActiveColor = Color.White.copy(alpha = 0.7f), stepActiveColor = Color.White.copy(alpha = 0.7f),
stepInactiveColor = scheme.onSurface.copy(alpha = 0.25f), stepInactiveColor = colors.onSurface.copy(alpha = 0.25f),
), ),
checkBox = CheckBoxTheme( checkBox = CheckBoxTheme(
selected = BaseUITheme.ButtonStyle( selected = BaseUITheme.ButtonStyle(
@ -709,7 +716,7 @@ fun rememberBaseUITheme(): BaseUITheme {
border = glassBorderState, border = glassBorderState,
padding = noPadding, padding = noPadding,
), ),
checkmarkColor = scheme.onPrimary, checkmarkColor = colors.onPrimary,
), ),
chipTab = ChipTabTheme( chipTab = ChipTabTheme(
selected = BaseUITheme.ButtonStyle( selected = BaseUITheme.ButtonStyle(
@ -794,8 +801,8 @@ fun rememberBaseUITheme(): BaseUITheme {
pressed = glassBrush(glassPressed, glassPressed), pressed = glassBrush(glassPressed, glassPressed),
focused = glassBrush(glassTop, glassBottom), focused = glassBrush(glassTop, glassBottom),
selected = glassBrush( selected = glassBrush(
lerp(scheme.primaryContainer, Color.White, 0.4f), lerp(colors.primaryContainer, Color.White, 0.4f),
scheme.primaryContainer, colors.primaryContainer,
), ),
disabled = glassBrush( disabled = glassBrush(
glassTop.copy(alpha = 0.5f), glassTop.copy(alpha = 0.5f),
@ -841,12 +848,12 @@ fun rememberBaseUITheme(): BaseUITheme {
disabled = TextStyle.Default, disabled = TextStyle.Default,
), ),
foreground = BaseUITheme.AdvancedInteractionState( foreground = BaseUITheme.AdvancedInteractionState(
normal = scheme.onSurface, normal = colors.onSurface,
hovered = scheme.onSurface, hovered = colors.onSurface,
pressed = scheme.onSurface, pressed = colors.onSurface,
focused = scheme.onSurface, focused = colors.onSurface,
selected = scheme.onSurface, selected = colors.onSurface,
disabled = scheme.onSurface.copy(alpha = 0.38f), disabled = colors.onSurface.copy(alpha = 0.38f),
), ),
), ),
dialog = BaseUITheme.DialogTheme( dialog = BaseUITheme.DialogTheme(
@ -873,7 +880,7 @@ fun rememberBaseUITheme(): BaseUITheme {
shadowColor.copy(alpha = 0.28f) shadowColor.copy(alpha = 0.28f)
), ),
itemHoverBackground = glassBrush(glassTop, glassBottom), itemHoverBackground = glassBrush(glassTop, glassBottom),
itemForeground = scheme.onSurface, itemForeground = colors.onSurface,
itemHighlight = gloss, itemHighlight = gloss,
padding = PaddingValues(6.dp), padding = PaddingValues(6.dp),
itemPadding = PaddingValues(horizontal = 14.dp, vertical = 11.dp), itemPadding = PaddingValues(horizontal = 14.dp, vertical = 11.dp),

View File

@ -44,7 +44,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet 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.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -327,7 +327,7 @@ private fun AdaptiveBottomSheetContent(
@Preview @Preview
@Composable @Composable
private fun AdaptiveDropdownBottomSheetPreview() { private fun AdaptiveDropdownBottomSheetPreview() {
Scaffold { BaseScaffold {
AdaptiveDropdownBottomSheet( AdaptiveDropdownBottomSheet(
items = listOf( items = listOf(
AdaptiveMenuItem( AdaptiveMenuItem(

View File

@ -27,7 +27,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme 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.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@ -103,7 +103,7 @@ fun CollectionView(
} }
} }
Scaffold( BaseScaffold(
topBar = { topBar = {
ApplicationMainBar( ApplicationMainBar(
backButton = true, backButton = true,

View File

@ -37,7 +37,7 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme 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.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -278,7 +278,7 @@ private fun ErrorDisplayPreview() {
@Preview @Preview
@Composable @Composable
private fun ErrorLogDialogPreview() { private fun ErrorLogDialogPreview() {
Scaffold { BaseScaffold {
ErrorLogDialog( ErrorLogDialog(
errorMessage = "This is an error message", errorMessage = "This is an error message",
onDismiss = {}, onDismiss = {},

View File

@ -48,7 +48,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme 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.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -1035,7 +1035,7 @@ fun previewTracks(): List<MetadataTrack> {
@Composable @Composable
@Preview @Preview
private fun TrackListPreview() { private fun TrackListPreview() {
Scaffold { BaseScaffold {
TrackList( TrackList(
tracks = previewTracks(), tracks = previewTracks(),
hasMore = true, hasMore = true,

View File

@ -38,7 +38,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme 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.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -189,7 +189,7 @@ fun PlayableCard(
@PreviewLightDark @PreviewLightDark
@Composable @Composable
private fun PlayableCardPreview() { private fun PlayableCardPreview() {
Scaffold { BaseScaffold {
PlayableCard( PlayableCard(
title = "Sample Title", title = "Sample Title",
subtitle = "Sample Subtitle", subtitle = "Sample Subtitle",

View File

@ -34,7 +34,7 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme 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.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -148,7 +148,7 @@ fun PlatformWebViewScreen(webViewController: WebViewController) {
} }
} }
Scaffold( BaseScaffold(
contentWindowInsets = WindowInsets.statusBars, contentWindowInsets = WindowInsets.statusBars,
topBar = { topBar = {
ApplicationMainBar( ApplicationMainBar(

View File

@ -42,7 +42,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PlainTooltip 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.Text
import androidx.compose.material3.TooltipAnchorPosition import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox import androidx.compose.material3.TooltipBox
@ -118,7 +118,7 @@ fun ArtistScreen(
val trackOptionsContext by viewModel.trackOptionsContext.collectAsStateWithLifecycle() val trackOptionsContext by viewModel.trackOptionsContext.collectAsStateWithLifecycle()
val showAddToPlaylistPicker by viewModel.showAddToPlaylistPicker.collectAsStateWithLifecycle() val showAddToPlaylistPicker by viewModel.showAddToPlaylistPicker.collectAsStateWithLifecycle()
Scaffold( BaseScaffold(
topBar = { ApplicationMainBar() } topBar = { ApplicationMainBar() }
) { innerPadding -> ) { innerPadding ->
when (val currentState = state) { when (val currentState = state) {

View File

@ -33,7 +33,6 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -51,6 +50,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack 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.GhostIconButton
import dev.krtirtho.spotube.core.ui.base.TextField import dev.krtirtho.spotube.core.ui.base.TextField
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
@ -67,7 +67,7 @@ import org.koin.compose.viewmodel.koinViewModel
@Composable @Composable
fun BlacklistScreen() { fun BlacklistScreen() {
val viewModel = koinViewModel<BlacklistViewModel>() val viewModel = koinViewModel<BlacklistViewModel>()
Scaffold( BaseScaffold(
topBar = { topBar = {
ApplicationMainBar( ApplicationMainBar(
backButton = true, 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]. * as a tab in the large-screen settings layout as well as hosted by [BlacklistScreen].
*/ */
@Composable @Composable

View File

@ -33,7 +33,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton 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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@ -77,7 +77,7 @@ fun DevicesScreen(
} }
} }
Scaffold( BaseScaffold(
topBar = { topBar = {
ApplicationMainBar( ApplicationMainBar(
backButton = true, backButton = true,

View File

@ -45,7 +45,7 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme 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.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -107,7 +107,7 @@ fun RemoteControlScreen(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.TopCenter, contentAlignment = Alignment.TopCenter,
) { ) {
Scaffold( BaseScaffold(
modifier = Modifier modifier = Modifier
.fillMaxHeight(), .fillMaxHeight(),
topBar = { topBar = {

View File

@ -35,7 +35,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme 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.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -120,7 +120,7 @@ fun HomeScreen(viewModel: HomeScreenViewModel) {
} }
} }
Scaffold( BaseScaffold(
topBar = { topBar = {
ApplicationMainBar( ApplicationMainBar(
backButton = false, backButton = false,

View File

@ -32,7 +32,7 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField 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.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SingleChoiceSegmentedButtonRow
@ -71,7 +71,7 @@ fun JamScreen(
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
val shellBottomInset = LocalAppShellBottomInset.current val shellBottomInset = LocalAppShellBottomInset.current
Scaffold( BaseScaffold(
topBar = { topBar = {
ApplicationMainBar( ApplicationMainBar(
backButton = true, backButton = true,

View File

@ -27,7 +27,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton 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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -57,7 +57,7 @@ fun LibraryScreen(
val searchMode by libraryState.searchMode.collectAsState() val searchMode by libraryState.searchMode.collectAsState()
val isLargeScreen = appShellViewModel.useSidebar() val isLargeScreen = appShellViewModel.useSidebar()
Scaffold( BaseScaffold(
topBar = { topBar = {
Column { Column {
ApplicationMainBar( ApplicationMainBar(

View File

@ -36,7 +36,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme 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.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -71,7 +71,7 @@ fun LyricsScreen(
val positionMillis by viewModel.positionMillisFlow.collectAsState() val positionMillis by viewModel.positionMillisFlow.collectAsState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
Scaffold( BaseScaffold(
topBar = { topBar = {
ApplicationMainBar( ApplicationMainBar(
backButton = onClose == null, backButton = onClose == null,

View File

@ -39,7 +39,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -62,6 +61,7 @@ import coil3.request.ImageRequest
import coil3.request.crossfade import coil3.request.crossfade
import dev.krtirtho.spotube.PlatformType import dev.krtirtho.spotube.PlatformType
import dev.krtirtho.spotube.core.extras.kebabToTitleCase 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.Card
import dev.krtirtho.spotube.core.ui.base.OutlineButton import dev.krtirtho.spotube.core.ui.base.OutlineButton
import dev.krtirtho.spotube.core.ui.base.PrimaryButton import dev.krtirtho.spotube.core.ui.base.PrimaryButton
@ -145,13 +145,12 @@ fun PluginScreen(
viewModel: PluginViewModel = koinViewModel(), viewModel: PluginViewModel = koinViewModel(),
onboarding: Boolean = false, onboarding: Boolean = false,
) { ) {
Scaffold( BaseScaffold(
topBar = { topBar = {
if (!onboarding) { if (!onboarding) {
ApplicationMainBar(title = { Text(stringResource(Res.string.plugin_screen_title)) }) ApplicationMainBar(title = { Text(stringResource(Res.string.plugin_screen_title)) })
} }
}, },
containerColor = if (onboarding) Color.Transparent else MaterialTheme.colorScheme.surface,
) { innerPadding -> ) { innerPadding ->
PluginScreenContent( PluginScreenContent(
viewModel = viewModel, 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]. * as a tab in the large-screen settings layout as well as hosted by [PluginScreen].
*/ */
@Composable @Composable

View File

@ -53,7 +53,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme 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.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -268,7 +268,7 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
remotePlaybackController.requestTracksPlayNext(tracks, "Search results") remotePlaybackController.requestTracksPlayNext(tracks, "Search results")
} }
Scaffold( BaseScaffold(
topBar = { ApplicationMainBar(backButton = false) } topBar = { ApplicationMainBar(backButton = false) }
) { innerPadding -> ) { innerPadding ->
Column( Column(

View File

@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -16,6 +15,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.base.PrimaryButton
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
import dev.krtirtho.spotube.modules.settings.components.SwitchSettingCard import dev.krtirtho.spotube.modules.settings.components.SwitchSettingCard
@ -59,7 +59,7 @@ import spotube.composeapp.generated.resources.settings_section_jam
fun JamSettingsScreen( fun JamSettingsScreen(
viewModel: JamSettingsViewModel = koinViewModel(), viewModel: JamSettingsViewModel = koinViewModel(),
) { ) {
Scaffold( BaseScaffold(
topBar = { topBar = {
ApplicationMainBar( ApplicationMainBar(
title = { Text(stringResource(Res.string.settings_section_jam)) }, 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]. * as a tab in the large-screen settings layout as well as hosted by [JamSettingsScreen].
*/ */
@Composable @Composable

View File

@ -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.AudioFormat
import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioQuality import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioQuality
import dev.krtirtho.spotube.core.ui.base.SurfaceStyle
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@ -35,6 +36,7 @@ data class UserSettings(
// Appearance // Appearance
val theme: Theme = Theme.SYSTEM, val theme: Theme = Theme.SYSTEM,
val accentColor: AccentColors = AccentColors.GREEN_GOBLIN, val accentColor: AccentColors = AccentColors.GREEN_GOBLIN,
val surfaceTheme: SurfaceStyle = SurfaceStyle.Glass,
// Playback // Playback
val streamingMusicFormat: AudioFormat = AudioFormat( val streamingMusicFormat: AudioFormat = AudioFormat(

View File

@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue 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.discovery.rememberLocalNetworkPermissionRequester
import dev.krtirtho.spotube.core.navigation.NavigationCommands import dev.krtirtho.spotube.core.navigation.NavigationCommands
import dev.krtirtho.spotube.core.navigation.Routes import dev.krtirtho.spotube.core.navigation.Routes
import dev.krtirtho.spotube.core.ui.base.BaseScaffold
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
import spotube.composeapp.generated.resources.* import spotube.composeapp.generated.resources.*
import dev.krtirtho.spotube.modules.settings.sections.appearanceSection 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) PaddingValues(top = 16.dp, bottom = 16.dp + shellBottomInset)
} }
Scaffold( BaseScaffold(
topBar = { topBar = {
ApplicationMainBar( ApplicationMainBar(
title = { title = {

View File

@ -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.OutlineButton
import dev.krtirtho.spotube.core.ui.base.PrimaryButton import dev.krtirtho.spotube.core.ui.base.PrimaryButton
import dev.krtirtho.spotube.core.ui.base.Radio 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.AccentColors
import dev.krtirtho.spotube.modules.settings.SettingsViewModel import dev.krtirtho.spotube.modules.settings.SettingsViewModel
import dev.krtirtho.spotube.modules.settings.Theme 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.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxColorSwatch import dev.krtirtho.spotube.resources.iconsax.IconsaxColorSwatch
import dev.krtirtho.spotube.resources.iconsax.IconsaxColorsSquare import dev.krtirtho.spotube.resources.iconsax.IconsaxColorsSquare
import dev.krtirtho.spotube.resources.iconsax.IconsaxMagic
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import spotube.composeapp.generated.resources.* 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( AccentColorSettingCard(
selectedAccent = settings.accentColor, 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 @Composable
private fun AccentColors.displayLabel(): String { private fun AccentColors.displayLabel(): String {
return when (this) { return when (this) {

View File

@ -47,7 +47,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet 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.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@ -235,7 +235,7 @@ fun AppExpandedPlayer(
} }
} }
Scaffold( BaseScaffold(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
) { innerPadding -> ) { innerPadding ->

View File

@ -74,6 +74,8 @@ import dev.krtirtho.spotube.core.navigation.Navigator
import dev.krtirtho.spotube.core.navigation.Routes import dev.krtirtho.spotube.core.navigation.Routes
import dev.krtirtho.spotube.core.remote.ConnectionRequestDialogHost import dev.krtirtho.spotube.core.remote.ConnectionRequestDialogHost
import dev.krtirtho.spotube.core.remote.RemotePlaybackController 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.devices.PlayDestinationPickerHost
import dev.krtirtho.spotube.modules.lyrics.LyricsScreen import dev.krtirtho.spotube.modules.lyrics.LyricsScreen
import dev.krtirtho.spotube.modules.shell.alternative_track.AlternativeTrackContent import dev.krtirtho.spotube.modules.shell.alternative_track.AlternativeTrackContent
@ -100,6 +102,7 @@ fun AppShell(
) { ) {
val navigatorCommands: NavigationCommands = koinInject() val navigatorCommands: NavigationCommands = koinInject()
val remotePlaybackController: RemotePlaybackController = koinInject() val remotePlaybackController: RemotePlaybackController = koinInject()
val baseUIColors = LocalBaseUIColors.current
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(remotePlaybackController) { LaunchedEffect(remotePlaybackController) {
remotePlaybackController.events.collect { message -> remotePlaybackController.events.collect { message ->
@ -156,7 +159,7 @@ fun AppShell(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.surface) .baseSurfaceBackdrop(baseUIColors.surfaceTheme, baseUIColors.isLight)
.hazeSource(state = bgHazeState) // <-- Put it here! .hazeSource(state = bgHazeState) // <-- Put it here!
) )
Row(modifier = Modifier.fillMaxSize()) { Row(modifier = Modifier.fillMaxSize()) {
@ -229,6 +232,11 @@ fun AppShell(
} }
} else { } else {
BoxWithConstraints(modifier = Modifier.fillMaxSize()) { BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.fillMaxSize()
.baseSurfaceBackdrop(baseUIColors.surfaceTheme, baseUIColors.isLight),
)
CompactPlayerOverlay( CompactPlayerOverlay(
navigator = navigator, navigator = navigator,
navigationState = navigationState, navigationState = navigationState,

View File

@ -41,7 +41,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme 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.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -79,7 +79,7 @@ fun WelcomeScreen(viewModel: WelcomeViewModel) {
WelcomeTheme { WelcomeTheme {
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
WelcomeBackground() WelcomeBackground()
Scaffold( BaseScaffold(
containerColor = Color.Transparent, containerColor = Color.Transparent,
topBar = { topBar = {
ApplicationMainBar( ApplicationMainBar(

View File

@ -29,6 +29,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.TextUnit 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.LocalBaseUITheme
import dev.krtirtho.spotube.core.ui.base.rememberBaseUITheme import dev.krtirtho.spotube.core.ui.base.rememberBaseUITheme
import org.jetbrains.compose.resources.Font import org.jetbrains.compose.resources.Font
@ -60,7 +61,11 @@ fun WelcomeTheme(content: @Composable () -> Unit) {
outlineVariant = Color(0xFFD2D0CC), outlineVariant = Color(0xFFD2D0CC),
) )
MaterialTheme(colorScheme = scheme) { MaterialTheme(colorScheme = scheme) {
CompositionLocalProvider(LocalBaseUITheme provides rememberBaseUITheme()) { val baseTheme = rememberBaseUITheme()
CompositionLocalProvider(
LocalBaseUITheme provides baseTheme,
LocalBaseUIColors provides baseTheme.colors,
) {
content() content()
} }
} }

View File

@ -41,6 +41,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.WindowPlacement
import dev.krtirtho.spotube.core.systemtray.SystemTrayService 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.modules.settings.SettingsProvider
import dev.krtirtho.spotube.resources.iconsax.FluentDismiss import dev.krtirtho.spotube.resources.iconsax.FluentDismiss
import dev.krtirtho.spotube.resources.iconsax.FluentMaximize import dev.krtirtho.spotube.resources.iconsax.FluentMaximize
@ -63,6 +64,7 @@ actual fun ApplicationMainBar(
backButton: Boolean, backButton: Boolean,
transparent: Boolean, transparent: Boolean,
) { ) {
val colors = LocalBaseUIColors.current
Box( Box(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
.windowDragArea(doubleClickAction = WindowDoubleClickAction.ToggleMaximize) .windowDragArea(doubleClickAction = WindowDoubleClickAction.ToggleMaximize)
@ -82,9 +84,19 @@ actual fun ApplicationMainBar(
TopAppBarDefaults.topAppBarColors( TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent, containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent, scrolledContainerColor = Color.Transparent,
titleContentColor = colors.onSurface,
actionIconContentColor = colors.onSurfaceVariant,
navigationIconContentColor = colors.onSurfaceVariant,
) )
} else { } 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 modifier = Modifier.padding(end = 125.dp) // To avoid overlap with window buttons
) )

View File

@ -17,20 +17,36 @@
package dev.krtirtho.spotube.core.ui.component 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.VerticalScrollbar
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.krtirtho.spotube.core.ui.base.LocalBaseUIColors
@Composable @Composable
actual fun VerticalScrollbar( actual fun VerticalScrollbar(
listState: LazyListState, listState: LazyListState,
modifier: Modifier modifier: Modifier
) {
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( VerticalScrollbar(
adapter = rememberScrollbarAdapter(listState), adapter = rememberScrollbarAdapter(listState),
@ -40,3 +56,4 @@ actual fun VerticalScrollbar(
.width(12.dp), .width(12.dp),
) )
} }
}