feat(previews): add comprehensive previews for UI components including sliders, toggles, checkboxes, and text fields

This commit is contained in:
Kingkor Roy Tirtho 2026-10-07 10:39:26 +06:00
parent 71d7649db6
commit 5c0652b395
14 changed files with 1378 additions and 529 deletions

View File

@ -33,6 +33,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -43,6 +44,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -74,7 +76,6 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
@ -83,6 +84,7 @@ import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties import androidx.compose.ui.window.PopupProperties
import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
private val AutocompleteTextFieldMinHeight = 44.dp private val AutocompleteTextFieldMinHeight = 44.dp
private val AutocompleteMenuDefaultMaxHeight = 300.dp private val AutocompleteMenuDefaultMaxHeight = 300.dp
@ -373,13 +375,11 @@ fun <T> AutocompleteTextField(
isMenuOpen = false isMenuOpen = false
}, },
) { ) {
Box( AutocompleteMenuContainer(
theme = resolvedMenuTheme,
modifier = Modifier modifier = Modifier
.width(textFieldWidthDp) .width(textFieldWidthDp)
.heightIn(max = menuMaxHeight) .heightIn(max = menuMaxHeight),
.shadow(resolvedMenuTheme.shadowElevation, resolvedMenuTheme.shape)
.background(resolvedMenuTheme.background, resolvedMenuTheme.shape)
.clip(resolvedMenuTheme.shape),
) { ) {
LazyColumn( LazyColumn(
state = menuListState, state = menuListState,
@ -407,18 +407,29 @@ fun <T> AutocompleteTextField(
} }
} }
@Preview /**
* The visual container of the autocomplete suggestion menu, without the Popup window.
*
* Extracted so the menu can be rendered in previews, since popups are hosted in a
* separate window the preview canvas cannot show.
*/
@Composable @Composable
private fun AutocompleteTextFieldPreview() { internal fun AutocompleteMenuContainer(
MaterialTheme { theme: BaseUITheme.AutoCompleteMenuTheme,
val theme = rememberBaseUITheme() modifier: Modifier = Modifier,
CompositionLocalProvider(LocalBaseUITheme provides theme) { content: @Composable () -> Unit,
androidx.compose.material3.Surface(
color = MaterialTheme.colorScheme.background,
modifier = Modifier.padding(24.dp),
) { ) {
val artists = remember { Box(
listOf( modifier = modifier
.shadow(theme.shadowElevation, theme.shape)
.background(theme.background, theme.shape)
.clip(theme.shape),
) {
content()
}
}
private val AutocompletePreviewArtists = listOf(
"Twenty One Pilots", "Twenty One Pilots",
"The Beatles", "The Beatles",
"Adele", "Adele",
@ -427,29 +438,109 @@ private fun AutocompleteTextFieldPreview() {
"Arctic Monkeys", "Arctic Monkeys",
"Billie Eilish", "Billie Eilish",
) )
}
@BaseUIPhonePreview
@Composable
private fun AutocompleteTextFieldPreview() {
BaseUIPreview {
var value by remember { mutableStateOf("") } var value by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(20.dp),
) {
Text(
text = "Field states",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
AutocompleteTextField( AutocompleteTextField(
value = value, value = "",
onValueChange = { onValueChange = {},
value = it items = emptyList<String>(),
}, itemContent = { _, _ -> },
items = artists.filter {
it.contains(value, ignoreCase = true) && value.isNotEmpty()
},
onItemSelected = { selected ->
value = selected
},
placeholder = { Text("Search artists...") }, placeholder = { Text("Search artists...") },
leadingIcon = { leadingIcon = {
androidx.compose.material3.Icon( Icon(
imageVector = Iconsax.IconsaxSearchBroken, imageVector = Iconsax.IconsaxSearchBroken,
contentDescription = "Search", contentDescription = "Search",
) )
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
)
AutocompleteTextField(
value = value,
onValueChange = { value = it },
items = AutocompletePreviewArtists.filter {
it.contains(value, ignoreCase = true)
},
onItemSelected = { value = it },
placeholder = { Text("Search artists...") },
leadingIcon = {
Icon(
imageVector = Iconsax.IconsaxSearchBroken,
contentDescription = "Search",
)
},
trailingIcon = {
Icon(
imageVector = Iconsax.IconsaxTrash,
contentDescription = "Clear",
)
},
modifier = Modifier.fillMaxWidth(),
itemContent = { item, isSelected -> itemContent = { item, isSelected ->
AutocompletePreviewItem(item, isSelected)
},
)
}
}
}
@BaseUIPhonePreview
@Composable
private fun AutocompleteMenuPreview() {
BaseUIPreview {
var selectedIndex by remember { mutableStateOf(1) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(
text = "Suggestion menu",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
AutocompleteMenuContainer(
theme = LocalBaseUITheme.current.autoCompleteMenuTheme,
modifier = Modifier.width(320.dp),
) {
Column {
AutocompletePreviewArtists.take(5).forEachIndexed { index, artist ->
Box(
modifier = Modifier.clickable { selectedIndex = index },
) {
AutocompletePreviewItem(artist, isSelected = selectedIndex == index)
}
}
}
}
Text(
text = "Selected: ${AutocompletePreviewArtists[selectedIndex]}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
@Composable
private fun AutocompletePreviewItem(text: String, isSelected: Boolean) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -464,20 +555,15 @@ private fun AutocompleteTextFieldPreview() {
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),
) { ) {
androidx.compose.material3.Icon( Icon(
imageVector = Iconsax.IconsaxSearchBroken, imageVector = Iconsax.IconsaxSearchBroken,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant, tint = MaterialTheme.colorScheme.onSurfaceVariant,
) )
Text( Text(
text = item, text = text,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
) )
} }
},
)
}
}
}
} }

View File

@ -0,0 +1,89 @@
/*
* 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.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
/**
* Shared scaffold for every preview of a base UI component.
*
* It mirrors the runtime environment the components expect:
* - a [MaterialTheme] whose light/dark mode follows the preview's `uiMode`, so the
* `@PreviewLightDark`-style annotations actually change the rendered colors,
* - a [LocalBaseUITheme] built from that color scheme via [rememberBaseUITheme],
* - a [Surface] matching the real app background, so layered highlights and shadows
* render against the color they will actually sit on.
*
* Always render base components inside [BaseUIPreview]; several of them read
* [LocalBaseUITheme] directly and would otherwise throw at composition time.
*/
@Composable
internal fun BaseUIPreview(
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit,
) {
MaterialTheme(
colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme(),
) {
CompositionLocalProvider(LocalBaseUITheme provides rememberBaseUITheme()) {
Surface(
modifier = modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background,
) {
Box(modifier = Modifier.fillMaxSize(), content = content)
}
}
}
}
/**
* Renders a base UI component on a phone-sized canvas in both light and dark themes.
*/
@Preview(name = "Phone · Light", group = "Base UI devices", device = Devices.PHONE)
@Preview(
name = "Phone · Dark",
group = "Base UI devices",
device = Devices.PHONE,
uiMode = UI_MODE_NIGHT_YES,
)
internal annotation class BaseUIPhonePreview
/**
* Renders a base UI component on a desktop-sized canvas in both light and dark themes.
*/
@Preview(name = "Desktop · Light", group = "Base UI devices", device = Devices.DESKTOP)
@Preview(
name = "Desktop · Dark",
group = "Base UI devices",
device = Devices.DESKTOP,
uiMode = UI_MODE_NIGHT_YES,
)
internal annotation class BaseUIDesktopPreview

View File

@ -27,6 +27,7 @@ import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -46,7 +47,10 @@ import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
@ -57,7 +61,6 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import dev.krtirtho.spotube.resources.iconsax.ArrowLeft3 import dev.krtirtho.spotube.resources.iconsax.ArrowLeft3
@ -549,15 +552,20 @@ fun ButtonGroupDivider(
) )
} }
@Preview(showBackground = true) @BaseUIPhonePreview
@BaseUIDesktopPreview
@Composable @Composable
private fun ButtonsRow1Preview() { private fun ButtonsPreview() {
MaterialTheme { BaseUIPreview {
val theme = rememberBaseUITheme() var liked by remember { mutableStateOf(false) }
CompositionLocalProvider(LocalBaseUITheme provides theme) { var likeCount by remember { mutableIntStateOf(128) }
Surface( var following by remember { mutableStateOf(false) }
color = MaterialTheme.colorScheme.background,
modifier = Modifier.padding(24.dp), Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -569,20 +577,17 @@ private fun ButtonsRow1Preview() {
contentDescription = null, contentDescription = null,
modifier = Modifier.size(18.dp), modifier = Modifier.size(18.dp),
) )
Text( Text("Share", maxLines = 1, softWrap = false)
"Copy link",
maxLines = 1,
softWrap = false,
)
} }
OutlineButton(onClick = {}) { SecondaryButton(onClick = {}) {
Icon( Text("Secondary", maxLines = 1, softWrap = false)
imageVector = Iconsax.User,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
Text("Login", maxLines = 1, softWrap = false)
} }
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
PrimaryButton(onClick = {}) { PrimaryButton(onClick = {}) {
Icon( Icon(
imageVector = Iconsax.IconsaxAddSquare, imageVector = Iconsax.IconsaxAddSquare,
@ -590,7 +595,67 @@ private fun ButtonsRow1Preview() {
modifier = Modifier.size(18.dp), modifier = Modifier.size(18.dp),
) )
Text( Text(
"Sign Up", "Sign up",
maxLines = 1,
softWrap = false,
fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold,
)
}
GhostButton(onClick = {}) {
Text("Ghost", maxLines = 1, softWrap = false)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
OutlineButton(onClick = {}, enabled = false) {
Text("Disabled", maxLines = 1, softWrap = false)
}
SecondaryButton(onClick = {}, enabled = false) {
Text("Disabled", maxLines = 1, softWrap = false)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
PrimaryButton(onClick = {}, enabled = false) {
Text("Disabled", maxLines = 1, softWrap = false)
}
OutlineButton(onClick = {}) {
Text("Forward", maxLines = 1, softWrap = false)
Icon(
imageVector = Iconsax.IconsaxArrowSquareUp,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
OutlineButton(
onClick = {
liked = !liked
likeCount += if (liked) 1 else -1
},
) {
Icon(
imageVector = Iconsax.IconsaxHeart,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
Text(if (liked) "Liked" else "Like", maxLines = 1, softWrap = false)
ButtonBadge(count = likeCount)
}
PrimaryButton(onClick = { following = !following }) {
Text(
if (following) "Following" else "Follow",
maxLines = 1, maxLines = 1,
softWrap = false, softWrap = false,
fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold, fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold,
@ -600,21 +665,122 @@ private fun ButtonsRow1Preview() {
} }
} }
} }
@BaseUIPhonePreview
@Composable
private fun IconButtonsPreview() {
BaseUIPreview {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(
text = "Icon buttons",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
IconButton(onClick = {}) {
Icon(
imageVector = Iconsax.IconsaxMagic,
contentDescription = "Magic",
modifier = Modifier.size(18.dp),
)
}
GhostIconButton(onClick = {}) {
Icon(
imageVector = Iconsax.IconsaxHeart,
contentDescription = "Favorite",
modifier = Modifier.size(18.dp),
)
}
PrimaryIconButton(onClick = {}) {
Icon(
imageVector = Iconsax.IconsaxAddSquare,
contentDescription = "Add",
modifier = Modifier.size(18.dp),
)
}
SecondaryIconButton(onClick = {}) {
Icon(
imageVector = Iconsax.IconsaxShare,
contentDescription = "Share",
modifier = Modifier.size(18.dp),
)
}
} }
@Preview(showBackground = true) Text(
@Composable text = "Variable icon button",
private fun ButtonsRow2Preview() { style = MaterialTheme.typography.labelLarge,
MaterialTheme { color = MaterialTheme.colorScheme.onSurfaceVariant,
val theme = rememberBaseUITheme() )
CompositionLocalProvider(LocalBaseUITheme provides theme) { Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Surface( VariableIconButton(
color = MaterialTheme.colorScheme.background, variant = VariableIconButtonVariant.Outline,
modifier = Modifier.padding(24.dp), onClick = {},
) { ) {
Icon(
imageVector = Iconsax.IconsaxDocumentText,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
}
VariableIconButton(
variant = VariableIconButtonVariant.Ghost,
onClick = {},
) {
Icon(
imageVector = Iconsax.Iconsax3DotsMore,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
}
VariableIconButton(
variant = VariableIconButtonVariant.Primary,
onClick = {},
) {
Icon(
imageVector = Iconsax.IconsaxMagic,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
}
VariableIconButton(
variant = VariableIconButtonVariant.Secondary,
onClick = {},
) {
Icon(
imageVector = Iconsax.User,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
}
}
}
}
}
@BaseUIDesktopPreview
@Composable
private fun ButtonGroupPreview() {
BaseUIPreview {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
Text(
text = "Button groups",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
ButtonGroup { ButtonGroup {
GroupButton(onClick = {}) { GroupButton(onClick = {}) {
@ -660,133 +826,7 @@ private fun ButtonsRow2Preview() {
) )
} }
} }
} ButtonBadge(count = 8)
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun ButtonsRow3Preview() {
MaterialTheme {
val theme = rememberBaseUITheme()
CompositionLocalProvider(LocalBaseUITheme provides theme) {
Surface(
color = MaterialTheme.colorScheme.background,
modifier = Modifier.padding(24.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
OutlineButton(onClick = {}) {
Text("Cancel", maxLines = 1, softWrap = false)
}
PrimaryButton(onClick = {}) {
Text(
"Done",
maxLines = 1,
softWrap = false,
fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold,
)
}
IconButton(onClick = {}) {
Icon(
imageVector = Iconsax.IconsaxMagic,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
}
OutlineButton(onClick = {}) {
Icon(
imageVector = Iconsax.IconsaxHeart,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
Text("Like", maxLines = 1, softWrap = false)
ButtonBadge(count = 2)
}
}
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun ButtonsRow4Preview() {
MaterialTheme {
val theme = rememberBaseUITheme()
CompositionLocalProvider(LocalBaseUITheme provides theme) {
Surface(
color = MaterialTheme.colorScheme.background,
modifier = Modifier.padding(24.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
ButtonGroup {
GroupIconButton(onClick = {}) {
Icon(
imageVector = Iconsax.ArrowLeft3,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
}
ButtonGroupDivider()
GroupIconButton(onClick = {}) {
Icon(
imageVector = Iconsax.IconsaxNext,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
}
}
OutlineButton(onClick = {}) {
Text("Forward", maxLines = 1, softWrap = false)
Icon(
imageVector = Iconsax.IconsaxArrowSquareUp,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
}
}
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun ButtonStylesPreview() {
MaterialTheme {
val theme = rememberBaseUITheme()
CompositionLocalProvider(LocalBaseUITheme provides theme) {
Surface(
color = MaterialTheme.colorScheme.background,
modifier = Modifier.padding(24.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
OutlineButton(onClick = {}) {
Text("Outline", maxLines = 1, softWrap = false)
}
SecondaryButton(onClick = {}) {
Text("Secondary", maxLines = 1, softWrap = false)
}
PrimaryButton(onClick = {}) {
Text(
"Primary",
maxLines = 1,
softWrap = false,
fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold,
)
}
}
} }
} }
} }

View File

@ -20,18 +20,38 @@ package dev.krtirtho.spotube.core.ui.base
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusic
import dev.krtirtho.spotube.resources.iconsax.IconsaxPause
import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay
@Composable @Composable
fun Card( fun Card(
@ -62,18 +82,90 @@ fun Card(
} }
} }
@BaseUIPhonePreview
@Composable @Composable
@androidx.compose.ui.tooling.preview.Preview
private fun CardPreview() { private fun CardPreview() {
MaterialTheme { BaseUIPreview {
val theme = rememberBaseUITheme() var connected by remember { mutableStateOf(false) }
CompositionLocalProvider(LocalBaseUITheme provides theme) { var playing by remember { mutableStateOf(false) }
Surface(
color = MaterialTheme.colorScheme.background, Column(
modifier = Modifier.padding(24.dp), modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
Card { Card {
Box(modifier = Modifier.padding(16.dp)) Column(modifier = Modifier.padding(16.dp)) {
Text(
text = if (connected) "Spotify connected" else "Connect a music source",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
)
Spacer(Modifier.height(6.dp))
Text(
text = if (connected) {
"Streaming, downloads and scrobbling are ready."
} else {
"Sign in to stream, download and scrobble from your account."
},
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
OutlineButton(onClick = { connected = false }) {
Text("Disconnect")
}
PrimaryButton(onClick = { connected = !connected }) {
Text(if (connected) "Connected" else "Connect")
}
}
}
}
Card {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.size(44.dp)
.clip(RoundedCornerShape(12.dp))
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Iconsax.IconsaxMusic,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.primary,
)
}
Spacer(Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Midnight City",
style = MaterialTheme.typography.titleSmall,
maxLines = 1,
)
Text(
text = "M83 · Hurry Up, We're Dreaming",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
)
}
PrimaryIconButton(onClick = { playing = !playing }) {
Icon(
imageVector = if (playing) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
contentDescription = if (playing) "Pause" else "Play",
modifier = Modifier.size(18.dp),
)
}
} }
} }
} }

View File

@ -31,17 +31,20 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -56,7 +59,6 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
enum class CheckBoxState { enum class CheckBoxState {
@ -207,47 +209,72 @@ fun CheckBox(
} }
} }
@Preview @BaseUIPhonePreview
@Composable @Composable
fun CheckBoxPreview() { private fun CheckBoxPreview() {
MaterialTheme { BaseUIPreview {
val theme = rememberBaseUITheme() var wifi by remember { mutableStateOf(true) }
CompositionLocalProvider(LocalBaseUITheme provides theme) { var bluetooth by remember { mutableStateOf(false) }
Surface( var downloads by remember { mutableStateOf(true) }
color = MaterialTheme.colorScheme.background,
modifier = Modifier.padding(24.dp), val allEnabled = wifi && bluetooth && downloads
val anyEnabled = wifi || bluetooth || downloads
val allState = when {
allEnabled -> CheckBoxState.SELECTED
anyEnabled -> CheckBoxState.INDETERMINATE
else -> CheckBoxState.UNSELECTED
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
CheckBoxPreviewRow(
state = allState,
label = "All playback sources",
onClick = {
val target = !allEnabled
wifi = target
bluetooth = target
downloads = target
},
)
Spacer(Modifier.height(4.dp))
CheckBoxPreviewRow(
state = if (wifi) CheckBoxState.SELECTED else CheckBoxState.UNSELECTED,
label = "Wi-Fi",
onClick = { wifi = !wifi },
)
CheckBoxPreviewRow(
state = if (bluetooth) CheckBoxState.SELECTED else CheckBoxState.UNSELECTED,
label = "Bluetooth",
onClick = { bluetooth = !bluetooth },
)
CheckBoxPreviewRow(
state = if (downloads) CheckBoxState.SELECTED else CheckBoxState.UNSELECTED,
label = "Download over cellular",
onClick = { downloads = !downloads },
)
CheckBoxPreviewRow(CheckBoxState.SELECTED, "Disabled selected", enabled = false)
CheckBoxPreviewRow(CheckBoxState.UNSELECTED, "Disabled unselected", enabled = false)
}
}
}
@Composable
private fun CheckBoxPreviewRow(
state: CheckBoxState,
label: String,
enabled: Boolean = true,
onClick: () -> Unit = {},
) { ) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
CheckBox(state = CheckBoxState.SELECTED, onClick = {}) CheckBox(state = state, onClick = onClick, enabled = enabled)
Text("Selected", style = MaterialTheme.typography.bodyMedium) Text(label, style = MaterialTheme.typography.bodyMedium)
}
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
CheckBox(state = CheckBoxState.UNSELECTED, onClick = {})
Text("Unselected", style = MaterialTheme.typography.bodyMedium)
}
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
CheckBox(state = CheckBoxState.INDETERMINATE, onClick = {})
Text("Indeterminate", style = MaterialTheme.typography.bodyMedium)
}
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
CheckBox(state = CheckBoxState.SELECTED, onClick = {}, enabled = false)
Text("Disabled", style = MaterialTheme.typography.bodyMedium)
}
}
}
}
} }
} }

View File

@ -27,10 +27,14 @@ import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -38,7 +42,9 @@ import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -50,6 +56,9 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxDocumentDownload
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicLibrary
private val ChipTabMinHeight = 36.dp private val ChipTabMinHeight = 36.dp
@ -169,3 +178,62 @@ fun ChipTab(
) )
} }
} }
@BaseUIPhonePreview
@Composable
private fun ChipTabsPreview() {
BaseUIPreview {
val tabs = listOf("All", "Songs", "Albums", "Playlists")
var selected by remember { mutableStateOf(0) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
tabs.forEachIndexed { index, tab ->
ChipTab(
text = tab,
selected = selected == index,
onClick = { selected = index },
)
}
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
ChipTab(
text = "Library",
selected = true,
onClick = {},
leadingIcon = {
Icon(
imageVector = Iconsax.IconsaxMusicLibrary,
contentDescription = null,
modifier = Modifier.size(16.dp),
)
},
)
ChipTab(
text = "Downloads",
selected = false,
onClick = {},
leadingIcon = {
Icon(
imageVector = Iconsax.IconsaxDocumentDownload,
contentDescription = null,
modifier = Modifier.size(16.dp),
)
},
)
ChipTab(
text = "Disabled",
selected = false,
onClick = {},
enabled = false,
)
}
}
}
}

View File

@ -26,11 +26,14 @@ import androidx.compose.animation.scaleOut
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
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
@ -42,7 +45,6 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -52,7 +54,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview 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 androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
@ -70,8 +72,6 @@ fun ThemedDialog(
properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false), properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false),
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val dialogTheme = theme ?: LocalBaseUITheme.current.dialog
Dialog( Dialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
properties = properties, properties = properties,
@ -93,8 +93,35 @@ fun ThemedDialog(
.padding(24.dp), .padding(24.dp),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
ThemedDialogSurface(
theme = theme,
title = title,
actions = actions,
content = content,
)
}
}
}
}
/**
* The visual chrome of [ThemedDialog] without the platform [Dialog] window.
*
* Extracted so the dialog can be rendered inside previews (and reused elsewhere),
* since the platform dialog lives in a separate window the preview canvas cannot show.
*/
@Composable
internal fun ThemedDialogSurface(
modifier: Modifier = Modifier,
theme: BaseUITheme.DialogTheme? = null,
title: @Composable (() -> Unit)? = null,
actions: @Composable (() -> Unit)? = null,
content: @Composable () -> Unit,
) {
val dialogTheme = theme ?: LocalBaseUITheme.current.dialog
Box( Box(
modifier = Modifier modifier = modifier
.widthIn(max = DefaultDialogMaxWidth) .widthIn(max = DefaultDialogMaxWidth)
.shadow( .shadow(
elevation = dialogTheme.shadow.elevation, elevation = dialogTheme.shadow.elevation,
@ -154,29 +181,53 @@ fun ThemedDialog(
} }
} }
} }
}
}
}
@Preview @BaseUIPhonePreview
@Composable @Composable
private fun ThemedDialogPreview() { private fun ThemedDialogPreview() {
MaterialTheme { BaseUIPreview {
val theme = rememberBaseUITheme() var visible by remember { mutableStateOf(true) }
CompositionLocalProvider(LocalBaseUITheme provides theme) {
var show by remember { mutableStateOf(true) } Box(modifier = Modifier.fillMaxSize()) {
androidx.compose.material3.Surface( PrimaryButton(
color = MaterialTheme.colorScheme.background, onClick = { visible = true },
modifier = Modifier
.align(Alignment.TopStart)
.padding(24.dp),
) { ) {
PrimaryButton(onClick = { show = true }) { Text("Open dialog")
Text("Open Dialog")
} }
if (show) {
ThemedDialog(onDismissRequest = { show = false }) { if (visible) {
Box(
modifier = Modifier
.fillMaxSize()
.background(LocalBaseUITheme.current.dialog.scrim)
.clickable { visible = false },
contentAlignment = Alignment.Center,
) {
ThemedDialogSurface(
modifier = Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {},
),
title = {
Text( Text(
"This is a themed dialog with glass-like gradient, shadow, and scale animation.", text = "Sign out of Spotube?",
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.SemiBold,
)
},
actions = {
OutlineButton(onClick = { visible = false }) { Text("Cancel") }
PrimaryButton(onClick = { visible = false }) { Text("Sign out") }
},
) {
Text(
text = "You can sign back in at any time. Your downloaded tracks stay on this device.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} }
} }

View File

@ -26,8 +26,10 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
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
@ -38,7 +40,9 @@ import androidx.compose.material3.MaterialTheme
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
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -47,6 +51,12 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart
import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay
import dev.krtirtho.spotube.resources.iconsax.IconsaxShare
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
@Composable @Composable
fun DropdownMenu( fun DropdownMenu(
@ -63,25 +73,32 @@ fun DropdownMenu(
expanded = expanded, expanded = expanded,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
offset = offset, offset = offset,
modifier = modifier modifier = modifier.dropdownMenuContainer(menuTheme),
.widthIn(min = 180.dp)
.shadow(
elevation = menuTheme.shadow.elevation,
shape = menuTheme.shape,
ambientColor = menuTheme.shadow.ambientColor,
spotColor = menuTheme.shadow.spotColor,
)
.clip(menuTheme.shape)
.background(menuTheme.background, menuTheme.shape)
.border(
BorderStroke(menuTheme.border.width, menuTheme.border.color),
menuTheme.shape,
),
) { ) {
content() content()
} }
} }
/**
* The container styling of [DropdownMenu], split out so it can be reused by the
* material3 popup and by previews, which cannot render the popup window itself.
*/
internal fun Modifier.dropdownMenuContainer(theme: BaseUITheme.DropdownMenuTheme): Modifier =
this
.widthIn(min = 180.dp)
.shadow(
elevation = theme.shadow.elevation,
shape = theme.shape,
ambientColor = theme.shadow.ambientColor,
spotColor = theme.shadow.spotColor,
)
.clip(theme.shape)
.background(theme.background, theme.shape)
.border(
BorderStroke(theme.border.width, theme.border.color),
theme.shape,
)
@Composable @Composable
fun DropdownMenuItem( fun DropdownMenuItem(
text: String, text: String,
@ -170,3 +187,78 @@ fun DropdownMenuDivider(
) )
} }
} }
@BaseUIPhonePreview
@Composable
private fun DropdownMenuPreview() {
BaseUIPreview {
var expanded by remember { mutableStateOf(true) }
var lastAction by remember { mutableStateOf("None") }
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
OutlineButton(onClick = { expanded = !expanded }) {
Text(if (expanded) "Hide menu" else "Show menu")
}
if (expanded) {
Column(
modifier = Modifier.dropdownMenuContainer(
LocalBaseUITheme.current.dropdownMenu,
),
) {
DropdownMenuItem(
text = "Play next",
leadingIcon = Iconsax.IconsaxPlay,
onClick = {
lastAction = "Play next"
expanded = false
},
)
DropdownMenuItem(
text = "Add to queue",
leadingIcon = Iconsax.IconsaxAddSquare,
onClick = {
lastAction = "Add to queue"
expanded = false
},
)
DropdownMenuItem(
text = "Add to favorites",
leadingIcon = Iconsax.IconsaxHeart,
selected = lastAction == "Add to favorites",
onClick = {
lastAction = "Add to favorites"
expanded = false
},
)
DropdownMenuDivider()
DropdownMenuItem(
text = "Share",
leadingIcon = Iconsax.IconsaxShare,
onClick = {
lastAction = "Share"
expanded = false
},
)
DropdownMenuItem(
text = "Remove from playlist",
leadingIcon = Iconsax.IconsaxTrash,
enabled = false,
onClick = {},
)
}
}
Text(
text = "Last action: $lastAction",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}

View File

@ -25,20 +25,31 @@ import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusic
@Composable @Composable
fun ListRowTile( fun ListRowTile(
@ -88,7 +99,13 @@ fun ListRowTile(
val mod = modifier val mod = modifier
.fillMaxWidth() .fillMaxWidth()
.shadow(shadowDef.elevation, shape, shadowDef.clip, shadowDef.ambientColor, shadowDef.spotColor) .shadow(
shadowDef.elevation,
shape,
shadowDef.clip,
shadowDef.ambientColor,
shadowDef.spotColor
)
.clip(shape) .clip(shape)
.border(borderDef.width, borderDef.color, shape) .border(borderDef.width, borderDef.color, shape)
.background(background, shape) .background(background, shape)
@ -130,3 +147,94 @@ fun ListRowTile(
} }
} }
} }
private data class ListRowPreviewTrack(
val title: String,
val subtitle: String,
val duration: String,
)
private val ListRowPreviewTracks = listOf(
ListRowPreviewTrack("Midnight City", "M83 · Hurry Up, We're Dreaming", "4:03"),
ListRowPreviewTrack("Odoriko", "Vaundy · strobo", "3:37"),
ListRowPreviewTrack("Instant Crush", "Daft Punk · Random Access Memories", "5:37"),
)
@BaseUIPhonePreview
@Composable
private fun ListRowTilePreview() {
BaseUIPreview {
var selectedIndex by remember { mutableStateOf(0) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
ListRowPreviewTracks.forEachIndexed { index, track ->
ListRowTile(
selected = selectedIndex == index,
onClick = { selectedIndex = index },
leading = { ListRowTilePreviewArtwork() },
title = {
Text(
text = track.title,
style = MaterialTheme.typography.bodyMedium,
)
},
subtitle = {
Text(
text = track.subtitle,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
},
trailing = {
Text(
text = track.duration,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
},
)
}
ListRowTile(
enabled = false,
title = {
Text(
text = "Add to queue",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
)
},
subtitle = {
Text(
text = "Unavailable offline",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f),
)
},
)
}
}
}
@Composable
private fun ListRowTilePreviewArtwork() {
Box(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Iconsax.IconsaxMusic,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.primary,
)
}
}

View File

@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@ -38,9 +39,10 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -48,7 +50,6 @@ import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
private val RadioSize = 22.dp private val RadioSize = 22.dp
@ -145,32 +146,37 @@ fun Radio(
} }
} }
@Preview @BaseUIPhonePreview
@Composable @Composable
private fun RadioPreview() { private fun RadioPreview() {
MaterialTheme { BaseUIPreview {
val theme = rememberBaseUITheme() val options = listOf("Automatic", "High", "Lossless", "Data saver")
CompositionLocalProvider(LocalBaseUITheme provides theme) { var selected by remember { mutableStateOf(0) }
androidx.compose.material3.Surface(
color = MaterialTheme.colorScheme.background, Column(
modifier = Modifier.padding(24.dp), modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { options.forEachIndexed { index, label ->
Row( Row(
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Radio(selected = true, onClick = {}) Radio(
Text("Selected", style = MaterialTheme.typography.bodyMedium) selected = selected == index,
onClick = { selected = index },
)
Text(label, style = MaterialTheme.typography.bodyMedium)
}
} }
Row( Row(
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Radio(selected = false, onClick = {}) Radio(selected = true, onClick = {}, enabled = false)
Text("Unselected", style = MaterialTheme.typography.bodyMedium) Text("Disabled", style = MaterialTheme.typography.bodyMedium)
}
}
} }
} }
} }

View File

@ -30,6 +30,8 @@ import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.heightIn import androidx.compose.foundation.layout.heightIn
@ -40,10 +42,8 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
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
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
@ -61,7 +61,6 @@ import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -311,3 +310,60 @@ fun Slider(
) )
} }
} }
@BaseUIPhonePreview
@Composable
private fun SliderPreview() {
BaseUIPreview {
var volume by remember { mutableFloatStateOf(0.65f) }
var progress by remember { mutableFloatStateOf(0.3f) }
var quality by remember { mutableFloatStateOf(2f) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(28.dp),
) {
SliderPreviewField(title = "Volume", value = "${(volume * 100).toInt()}%") {
Slider(value = volume, onValueChange = { volume = it })
}
SliderPreviewField(title = "Progress", value = "${(progress * 100).toInt()}%") {
Slider(value = progress, onValueChange = { progress = it })
}
SliderPreviewField(title = "Quality", value = "${quality.toInt()} / 4") {
Slider(
value = quality,
onValueChange = { quality = it },
valueRange = 0f..4f,
steps = 3,
)
}
SliderPreviewField(title = "Disabled", value = "40%") {
Slider(value = 0.4f, onValueChange = {}, enabled = false)
}
}
}
}
@Composable
private fun SliderPreviewField(
title: String,
value: String,
content: @Composable () -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(title, style = MaterialTheme.typography.bodyMedium)
Text(
text = value,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
content()
}
}

View File

@ -31,19 +31,26 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
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.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -55,11 +62,11 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
import dev.krtirtho.spotube.resources.iconsax.User
private val TextFieldMinHeight = 44.dp private val TextFieldMinHeight = 44.dp
@ -201,43 +208,84 @@ fun TextField(
} }
} }
@Preview @BaseUIPhonePreview
@Composable @Composable
fun TextFieldPreview() { private fun TextFieldPreview() {
MaterialTheme { BaseUIPreview {
val theme = rememberBaseUITheme() var email by remember { mutableStateOf("") }
CompositionLocalProvider(LocalBaseUITheme provides theme) { var query by remember { mutableStateOf("Twenty One Pilots") }
androidx.compose.material3.Surface(
color = MaterialTheme.colorScheme.background, Column(
modifier = Modifier.padding(24.dp), modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(20.dp),
) { ) {
TextField( TextField(
modifier = Modifier.padding(top = 4.dp), value = email,
value = "Twenty One Pilots", onValueChange = { email = it },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("you@example.com") },
label = { TextFieldPreviewLabel("Email address") },
leadingIcon = {
Icon(
imageVector = Iconsax.User,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
},
singleLine = true,
)
TextField(
value = query,
onValueChange = { query = it },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Search songs, artists, albums...") },
leadingIcon = {
Icon(
imageVector = Iconsax.IconsaxSearchBroken,
contentDescription = "Search",
modifier = Modifier.size(18.dp),
)
},
trailingIcon = {
Icon(
imageVector = Iconsax.IconsaxTrash,
contentDescription = "Clear",
modifier = Modifier.size(18.dp),
)
},
singleLine = true,
)
TextField(
value = "not-an-email",
onValueChange = {}, onValueChange = {},
placeholder = { Text("Placeholder") }, modifier = Modifier.fillMaxWidth(),
label = { label = { TextFieldPreviewLabel("Error state") },
isError = true,
singleLine = true,
)
TextField(
value = "Read only value",
onValueChange = {},
modifier = Modifier.fillMaxWidth(),
label = { TextFieldPreviewLabel("Disabled") },
enabled = false,
singleLine = true,
)
}
}
}
@Composable
private fun TextFieldPreviewLabel(text: String) {
Text( Text(
"Search Field", text = text,
modifier = Modifier.padding(bottom = 6.dp), modifier = Modifier.padding(bottom = 6.dp),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
) )
},
leadingIcon = {
androidx.compose.material3.Icon(
imageVector = Iconsax.IconsaxSearchBroken,
contentDescription = "Search Icon",
)
},
trailingIcon = {
androidx.compose.material3.Icon(
imageVector = Iconsax.IconsaxTrash,
contentDescription = "Clear Icon",
)
},
)
}
}
}
} }

View File

@ -31,6 +31,9 @@ import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -38,11 +41,12 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -53,7 +57,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max import androidx.compose.ui.unit.max
@ -239,26 +242,56 @@ fun Toggle(
} }
} }
@Preview(showBackground = true) @BaseUIPhonePreview
@Composable @Composable
private fun TogglePreview() { private fun TogglePreview() {
MaterialTheme { BaseUIPreview {
val theme = rememberBaseUITheme() var scrobbling by remember { mutableStateOf(true) }
CompositionLocalProvider(LocalBaseUITheme provides theme) { var offlineMode by remember { mutableStateOf(false) }
Surface(
color = MaterialTheme.colorScheme.background,
modifier = Modifier.padding(24.dp),
) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier
horizontalAlignment = Alignment.CenterHorizontally, .fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(20.dp),
) { ) {
Toggle(checked = true, onCheckedChange = {}) TogglePreviewRow(
Toggle(checked = false, onCheckedChange = {}) label = "Scrobble listened tracks",
Toggle(checked = true, onCheckedChange = null) checked = scrobbling,
Toggle(checked = false, onCheckedChange = null, enabled = false) onCheckedChange = { scrobbling = it },
)
TogglePreviewRow(
label = "Offline mode",
checked = offlineMode,
onCheckedChange = { offlineMode = it },
)
TogglePreviewRow(
label = "Discord rich presence",
checked = true,
enabled = false,
onCheckedChange = {},
)
} }
} }
} }
@Composable
private fun TogglePreviewRow(
label: String,
checked: Boolean,
enabled: Boolean = true,
onCheckedChange: (Boolean) -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(label, style = MaterialTheme.typography.bodyMedium)
Toggle(
checked = checked,
onCheckedChange = onCheckedChange,
enabled = enabled,
)
} }
} }

View File

@ -1,9 +1,26 @@
package dev.krtirtho.spotube.core.ui.base package dev.krtirtho.spotube.core.ui.base
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
fun Modifier.highlight(color: Color): Modifier = fun Modifier.highlight(color: Color): Modifier =
drawWithCache { drawWithCache {
@ -21,3 +38,39 @@ fun Modifier.highlight(color: Color): Modifier =
) )
} }
} }
@BaseUIPhonePreview
@Composable
private fun HighlightModifierPreview() {
BaseUIPreview {
var highlighted by remember { mutableStateOf(true) }
Row(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Box(
modifier = Modifier
.size(96.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.primaryContainer)
.then(
if (highlighted) {
Modifier.highlight(Color.White.copy(alpha = 0.45f))
} else {
Modifier
},
)
.clickable { highlighted = !highlighted },
)
Box(
modifier = Modifier
.size(96.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.primaryContainer),
)
}
}
}