ref: design tokens

This commit is contained in:
tapframe 2026-06-08 01:38:47 +05:30
parent 09cb8f8277
commit 8ecb392c74
28 changed files with 1395 additions and 754 deletions

View file

@ -188,6 +188,17 @@ val iosDistributionSourceDir = if (iosDistribution == "full") {
val iosFrameworkBundleId = "com.nuvio.media"
val fullCommonSourceDir = project.file("src/fullCommonMain/kotlin")
val generatedRuntimeConfigDir = layout.buildDirectory.dir("generated/runtime-config/kotlin")
val requestedGradleTasks = gradle.startParameter.taskNames.map { taskName ->
taskName.substringAfterLast(':').lowercase()
}
val isAndroidAppBundleBuild = requestedGradleTasks.any { taskName ->
taskName == "bundle" ||
taskName == "bundlerelease" ||
taskName == "bundledebug" ||
taskName.startsWith("bundleplaystore") ||
taskName.startsWith("bundlefull") ||
taskName.endsWith("bundle")
}
val generateRuntimeConfigs = tasks.register<GenerateRuntimeConfigsTask>("generateRuntimeConfigs") {
outputDir.set(generatedRuntimeConfigDir)
@ -352,7 +363,7 @@ android {
}
splits {
abi {
isEnable = true
isEnable = !isAndroidAppBundleBuild
reset()
include("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
isUniversalApk = false

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="nuvio_background">#020404</color>
<color name="nuvio_background">#0D0D0D</color>
</resources>

View file

@ -96,11 +96,13 @@ import com.nuvio.app.core.ui.NuvioToastController
import com.nuvio.app.core.ui.NuvioFloatingPrompt
import com.nuvio.app.core.ui.TraktListPickerDialog
import com.nuvio.app.core.ui.NuvioTheme
import com.nuvio.app.core.ui.NuvioTokens
import com.nuvio.app.core.ui.LocalNuvioBottomNavigationOverlayPadding
import com.nuvio.app.core.ui.NativeNavigationTab
import com.nuvio.app.core.ui.NativeTabBridge
import com.nuvio.app.core.ui.isLiquidGlassNativeTabBarSupported
import com.nuvio.app.core.ui.localizedContinueWatchingSubtitle
import com.nuvio.app.core.ui.nuvio
import com.nuvio.app.features.auth.AuthScreen
import com.nuvio.app.features.addons.AddonRepository
import com.nuvio.app.features.catalog.CatalogRepository
@ -547,10 +549,10 @@ fun App() {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
.background(MaterialTheme.nuvio.colors.background),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
CircularProgressIndicator(color = MaterialTheme.nuvio.colors.accent)
}
}
AppGateScreen.Auth.name -> {
@ -1298,7 +1300,7 @@ private fun MainAppContent(
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
.background(MaterialTheme.nuvio.colors.background),
) {
SharedTransitionLayout {
NavHost(
@ -2042,7 +2044,7 @@ private fun MainAppContent(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
CircularProgressIndicator(color = MaterialTheme.nuvio.colors.accent)
}
return@composable
}
@ -2235,17 +2237,17 @@ private fun MainAppContent(
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.82f)),
.background(MaterialTheme.nuvio.colors.overlayScrim.copy(alpha = MaterialTheme.nuvio.opacity.overlayHeavy)),
contentAlignment = Alignment.Center,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.nuvio.spacing.cardPadding),
) {
CircularProgressIndicator(color = Color.White)
CircularProgressIndicator(color = MaterialTheme.nuvio.colors.playerControlsForeground)
Text(
text = stringResource(Res.string.streams_finding_source),
color = Color.White.copy(alpha = 0.82f),
color = MaterialTheme.nuvio.colors.playerControlsForeground.copy(alpha = MaterialTheme.nuvio.opacity.overlayHeavy),
style = MaterialTheme.typography.bodyMedium,
)
}
@ -2907,23 +2909,24 @@ private fun TabletFloatingTopBar(
onAddProfileRequested: () -> Unit,
modifier: Modifier = Modifier,
) {
val tokens = MaterialTheme.nuvio
val statusBarPadding = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
Box(
modifier = modifier
.fillMaxWidth()
.padding(top = statusBarPadding + 10.dp, bottom = 8.dp),
.padding(top = statusBarPadding + NuvioTokens.Space.s10, bottom = tokens.spacing.controlGap),
contentAlignment = Alignment.TopCenter,
) {
Surface(
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.96f),
shape = RoundedCornerShape(999.dp),
tonalElevation = 4.dp,
shadowElevation = 10.dp,
color = tokens.colors.surface.copy(alpha = tokens.opacity.visible - tokens.opacity.subtle),
shape = tokens.shapes.chip,
tonalElevation = tokens.elevation.playerControls,
shadowElevation = tokens.elevation.overlay,
) {
Row(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(horizontal = NuvioTokens.Space.s10, vertical = tokens.spacing.controlGap),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap),
verticalAlignment = Alignment.CenterVertically,
) {
TabletTopPillItem(
@ -2934,11 +2937,11 @@ private fun TabletFloatingTopBar(
Icon(
imageVector = Icons.Filled.Home,
contentDescription = stringResource(Res.string.compose_nav_home),
modifier = Modifier.size(18.dp),
modifier = Modifier.size(NuvioTokens.Space.s18),
tint = if (selectedTab == AppScreenTab.Home) {
MaterialTheme.colorScheme.onPrimaryContainer
tokens.colors.textPrimary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
tokens.colors.textMuted
},
)
},
@ -2951,11 +2954,11 @@ private fun TabletFloatingTopBar(
Icon(
painter = painterResource(Res.drawable.sidebar_search),
contentDescription = stringResource(Res.string.compose_nav_search),
modifier = Modifier.size(18.dp),
modifier = Modifier.size(NuvioTokens.Space.s18),
tint = if (selectedTab == AppScreenTab.Search) {
MaterialTheme.colorScheme.onPrimaryContainer
tokens.colors.textPrimary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
tokens.colors.textMuted
},
)
},
@ -2968,26 +2971,26 @@ private fun TabletFloatingTopBar(
Icon(
painter = painterResource(Res.drawable.sidebar_library),
contentDescription = stringResource(Res.string.compose_nav_library),
modifier = Modifier.size(18.dp),
modifier = Modifier.size(NuvioTokens.Space.s18),
tint = if (selectedTab == AppScreenTab.Library) {
MaterialTheme.colorScheme.onPrimaryContainer
tokens.colors.textPrimary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
tokens.colors.textMuted
},
)
},
)
Surface(
color = if (selectedTab == AppScreenTab.Settings) {
MaterialTheme.colorScheme.primaryContainer
tokens.colors.overlaySelected
} else {
MaterialTheme.colorScheme.surface
tokens.colors.surface
},
shape = RoundedCornerShape(999.dp),
shape = tokens.shapes.chip,
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(horizontal = tokens.spacing.listGap, vertical = tokens.spacing.controlGap),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap),
verticalAlignment = Alignment.CenterVertically,
) {
ProfileSwitcherTab(
@ -3001,9 +3004,9 @@ private fun TabletFloatingTopBar(
modifier = Modifier.clickable { onTabSelected(AppScreenTab.Settings) },
style = MaterialTheme.typography.labelLarge,
color = if (selectedTab == AppScreenTab.Settings) {
MaterialTheme.colorScheme.onPrimaryContainer
tokens.colors.textPrimary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
tokens.colors.textMuted
},
)
}
@ -3023,15 +3026,16 @@ private fun TabletTopPillItem(
onClick: () -> Unit,
icon: @Composable () -> Unit,
) {
val tokens = MaterialTheme.nuvio
Surface(
color = if (selected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(999.dp),
tonalElevation = if (selected) 2.dp else 0.dp,
color = if (selected) tokens.colors.overlaySelected else tokens.colors.surface,
shape = tokens.shapes.chip,
tonalElevation = if (selected) tokens.elevation.raised else tokens.elevation.flat,
modifier = Modifier.clickable(onClick = onClick),
) {
Row(
modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(horizontal = tokens.components.chipHorizontalPadding, vertical = NuvioTokens.Space.s10),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap),
verticalAlignment = Alignment.CenterVertically,
) {
icon()
@ -3039,9 +3043,9 @@ private fun TabletTopPillItem(
text = label,
style = MaterialTheme.typography.labelLarge,
color = if (selected) {
MaterialTheme.colorScheme.onPrimaryContainer
tokens.colors.textPrimary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
tokens.colors.textMuted
},
)
}
@ -3052,10 +3056,11 @@ private fun TabletTopPillItem(
private fun AppLaunchOverlay(
modifier: Modifier = Modifier,
) {
val tokens = MaterialTheme.nuvio
Box(
modifier = modifier
.background(MaterialTheme.colorScheme.background)
.zIndex(10f),
.background(tokens.colors.background)
.zIndex(NuvioTokens.Z.dialog),
contentAlignment = Alignment.Center,
) {
Column(
@ -3069,8 +3074,8 @@ private fun AppLaunchOverlay(
.height(44.dp),
contentScale = ContentScale.Fit,
)
Spacer(modifier = Modifier.height(24.dp))
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
Spacer(modifier = Modifier.height(tokens.spacing.sectionGap))
CircularProgressIndicator(color = tokens.colors.accent)
}
}
}

View file

@ -26,7 +26,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -34,9 +33,9 @@ fun NuvioModalBottomSheet(
onDismissRequest: () -> Unit,
sheetState: SheetState,
modifier: Modifier = Modifier,
containerColor: Color = MaterialTheme.colorScheme.surface,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
shape: Shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp),
containerColor: Color = MaterialTheme.nuvio.colors.surfaceSheet,
contentColor: Color = MaterialTheme.nuvio.colors.textPrimary,
shape: Shape = RoundedCornerShape(topStart = NuvioTokens.Space.s28, topEnd = NuvioTokens.Space.s28),
showDragHandle: Boolean = true,
content: @Composable ColumnScope.() -> Unit,
) {
@ -62,7 +61,7 @@ fun NuvioBottomSheetDivider(
) {
HorizontalDivider(
modifier = modifier,
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f),
color = MaterialTheme.nuvio.colors.borderSubtle,
)
}
@ -74,27 +73,28 @@ fun NuvioBottomSheetActionRow(
icon: ImageVector? = null,
trailingContent: (@Composable RowScope.() -> Unit)? = null,
) {
val tokens = MaterialTheme.nuvio
Row(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(14.dp),
.padding(horizontal = tokens.spacing.screenHorizontal, vertical = tokens.spacing.screenHorizontal),
horizontalArrangement = Arrangement.spacedBy(NuvioTokens.Space.s14),
verticalAlignment = Alignment.CenterVertically,
) {
if (icon != null) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(22.dp),
tint = tokens.colors.accent,
modifier = Modifier.size(NuvioTokens.Icon.md),
)
}
Text(
text = title,
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
@ -115,11 +115,12 @@ suspend fun dismissNuvioBottomSheet(
@Composable
private fun NuvioBottomSheetDragHandle() {
val tokens = MaterialTheme.nuvio
Box(
modifier = Modifier
.padding(top = 10.dp, bottom = 6.dp)
.size(width = 54.dp, height = 5.dp)
.clip(RoundedCornerShape(999.dp))
.background(MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.65f)),
.padding(top = NuvioTokens.Space.s10, bottom = NuvioTokens.Space.s6)
.size(width = NuvioTokens.Space.s56 - NuvioTokens.Space.s2, height = NuvioTokens.Space.s5)
.clip(tokens.shapes.chip)
.background(tokens.colors.borderDefault),
)
}
}

View file

@ -31,7 +31,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Button
@ -78,24 +77,25 @@ import kotlinx.coroutines.flow.asStateFlow
@Composable
fun NuvioScreen(
modifier: Modifier = Modifier,
horizontalPadding: Dp = 16.dp,
horizontalPadding: Dp = MaterialTheme.nuvio.spacing.screenHorizontal,
topPadding: Dp? = null,
listState: LazyListState = rememberLazyListState(),
content: LazyListScope.() -> Unit,
) {
val tokens = MaterialTheme.nuvio
val statusBarTop = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
LazyColumn(
state = listState,
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
.background(tokens.colors.background),
contentPadding = PaddingValues(
start = horizontalPadding,
top = topPadding ?: 10.dp + statusBarTop + nuvioPlatformExtraTopPadding,
top = topPadding ?: tokens.spacing.screenTop + statusBarTop + nuvioPlatformExtraTopPadding,
end = horizontalPadding,
bottom = nuvioSafeBottomPadding(18.dp),
bottom = nuvioSafeBottomPadding(tokens.spacing.screenBottom),
),
verticalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(tokens.spacing.listGap),
content = content,
)
}
@ -117,15 +117,16 @@ fun NuvioSurfaceCard(
tonalElevation: Int = 0,
content: @Composable ColumnScope.() -> Unit,
) {
val tokens = MaterialTheme.nuvio
Surface(
modifier = modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(24.dp),
color = tokens.colors.surface,
shape = tokens.shapes.card,
tonalElevation = tonalElevation.dp,
shadowElevation = 0.dp,
shadowElevation = tokens.elevation.flat,
) {
Column(
modifier = Modifier.padding(horizontal = 18.dp, vertical = 18.dp),
modifier = Modifier.padding(tokens.spacing.cardPadding),
content = content,
)
}
@ -140,27 +141,28 @@ fun NuvioScreenHeader(
onBack: (() -> Unit)? = null,
actions: @Composable RowScope.() -> Unit = {},
) {
val tokens = MaterialTheme.nuvio
val statusBarTop = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
val resolvedTopPadding = topPadding ?: if (includeStatusBarPadding) statusBarTop else 0.dp
val resolvedTopPadding = topPadding ?: if (includeStatusBarPadding) statusBarTop else NuvioTokens.Space.none
Row(
modifier = modifier
.fillMaxWidth()
.nuvioBlockPointerPassthrough()
.background(MaterialTheme.colorScheme.background)
.padding(top = resolvedTopPadding, bottom = 4.dp),
.background(tokens.colors.background)
.padding(top = resolvedTopPadding, bottom = NuvioTokens.Space.s4),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap),
) {
if (onBack != null) {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = stringResource(Res.string.action_back),
tint = MaterialTheme.colorScheme.onBackground,
tint = tokens.colors.textPrimary,
)
}
}
@ -172,12 +174,12 @@ fun NuvioScreenHeader(
Text(
text = currentTitle,
style = MaterialTheme.typography.displayLarge,
color = MaterialTheme.colorScheme.onBackground,
color = tokens.colors.textPrimary,
)
}
}
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp),
horizontalArrangement = Arrangement.spacedBy(NuvioTokens.Space.s2),
verticalAlignment = Alignment.CenterVertically,
content = actions,
)
@ -193,7 +195,7 @@ fun NuvioSectionLabel(
text = text,
modifier = modifier,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = MaterialTheme.nuvio.colors.textMuted,
fontWeight = FontWeight.Bold,
)
}
@ -214,7 +216,7 @@ fun NuvioActionLabel(
}
),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
color = MaterialTheme.nuvio.colors.accent,
)
}
@ -223,14 +225,15 @@ fun NuvioIconActionButton(
icon: ImageVector,
contentDescription: String,
modifier: Modifier = Modifier,
tint: Color = MaterialTheme.colorScheme.onSurface,
tint: Color = MaterialTheme.nuvio.colors.textPrimary,
onClick: () -> Unit = {},
) {
val tokens = MaterialTheme.nuvio
IconButton(
modifier = modifier
.background(
color = MaterialTheme.colorScheme.background.copy(alpha = 0.001f),
shape = CircleShape,
color = tokens.colors.background.copy(alpha = 0.001f),
shape = tokens.shapes.avatar,
),
onClick = onClick,
) {
@ -246,11 +249,11 @@ fun NuvioIconActionButton(
fun NuvioBackButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
shape: Shape = CircleShape,
containerColor: Color = MaterialTheme.colorScheme.surface,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
buttonSize: Dp = 40.dp,
iconSize: Dp = 22.dp,
shape: Shape = MaterialTheme.nuvio.shapes.avatar,
containerColor: Color = MaterialTheme.nuvio.colors.surface,
contentColor: Color = MaterialTheme.nuvio.colors.textPrimary,
buttonSize: Dp = NuvioTokens.Space.s40,
iconSize: Dp = NuvioTokens.Icon.md,
contentDescription: String = stringResource(Res.string.action_back),
) {
Box(
@ -277,18 +280,19 @@ fun NuvioPrimaryButton(
enabled: Boolean = true,
onClick: () -> Unit = {},
) {
val tokens = MaterialTheme.nuvio
Button(
onClick = onClick,
modifier = modifier
.fillMaxWidth()
.height(52.dp),
.height(NuvioTokens.Space.s48 + NuvioTokens.Space.s4),
enabled = enabled,
shape = RoundedCornerShape(16.dp),
shape = tokens.shapes.button,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
disabledContainerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.65f),
disabledContentColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.65f),
containerColor = tokens.colors.accent,
contentColor = tokens.colors.onAccent,
disabledContainerColor = tokens.colors.accent.copy(alpha = tokens.opacity.disabled),
disabledContentColor = tokens.colors.onAccent.copy(alpha = tokens.opacity.disabled),
),
) {
AnimatedContent(
@ -313,27 +317,28 @@ fun NuvioInputField(
modifier: Modifier = Modifier,
trailingContent: (@Composable (() -> Unit))? = null,
) {
val tokens = MaterialTheme.nuvio
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier.fillMaxWidth(),
singleLine = true,
shape = RoundedCornerShape(14.dp),
shape = RoundedCornerShape(NuvioTokens.Radius.lg),
placeholder = {
Text(
text = placeholder,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
style = MaterialTheme.typography.bodyLarge,
)
},
textStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
textStyle = MaterialTheme.typography.bodyLarge.copy(color = tokens.colors.textPrimary),
trailingIcon = trailingContent,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.outline,
unfocusedBorderColor = MaterialTheme.colorScheme.outline,
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
cursorColor = MaterialTheme.colorScheme.primary,
focusedBorderColor = tokens.colors.borderFocus,
unfocusedBorderColor = tokens.colors.borderDefault,
focusedContainerColor = tokens.colors.surfaceCard,
unfocusedContainerColor = tokens.colors.surfaceCard,
cursorColor = tokens.colors.accent,
),
)
}
@ -343,18 +348,19 @@ fun NuvioInfoBadge(
text: String,
modifier: Modifier = Modifier,
) {
val tokens = MaterialTheme.nuvio
Box(
modifier = modifier
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(999.dp),
color = tokens.colors.surfaceCard,
shape = tokens.shapes.chip,
)
.padding(horizontal = 10.dp, vertical = 6.dp),
.padding(horizontal = NuvioTokens.Space.s10, vertical = NuvioTokens.Space.s6),
) {
Text(
text = text,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
@ -374,13 +380,13 @@ fun NuvioInlineMetadata(
Text(
text = title,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = MaterialTheme.nuvio.colors.textMuted,
)
Spacer(modifier = Modifier.width(6.dp))
Spacer(modifier = Modifier.width(NuvioTokens.Space.s6))
Text(
text = value,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
color = MaterialTheme.nuvio.colors.textPrimary,
)
}
}
@ -399,6 +405,7 @@ fun NuvioStatusModal(
onDismiss: (() -> Unit)? = null,
) {
if (!isVisible) return
val tokens = MaterialTheme.nuvio
BasicAlertDialog(
onDismissRequest = {
@ -409,31 +416,31 @@ fun NuvioStatusModal(
) {
Surface(
modifier = modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(24.dp),
color = tokens.colors.surfaceDialog,
shape = tokens.shapes.dialog,
) {
Column(
modifier = Modifier.padding(20.dp),
modifier = Modifier.padding(tokens.spacing.dialogPadding),
) {
if (isBusy) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.primary,
strokeWidth = 2.5.dp,
color = tokens.colors.accent,
strokeWidth = NuvioTokens.Border.medium + NuvioTokens.Space.hairline,
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(NuvioTokens.Space.s16))
}
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(tokens.spacing.controlGap))
Text(
text = message,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
)
Spacer(modifier = Modifier.height(18.dp))
Spacer(modifier = Modifier.height(NuvioTokens.Space.s18))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
@ -441,20 +448,20 @@ fun NuvioStatusModal(
if (!isBusy && dismissText != null && onDismiss != null) {
Button(
onClick = onDismiss,
shape = RoundedCornerShape(16.dp),
shape = tokens.shapes.button,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurface,
containerColor = tokens.colors.surfaceCard,
contentColor = tokens.colors.textPrimary,
),
) {
Text(dismissText)
}
Spacer(modifier = Modifier.width(10.dp))
Spacer(modifier = Modifier.width(NuvioTokens.Space.s10))
}
Button(
onClick = onConfirm,
enabled = !isBusy,
shape = RoundedCornerShape(16.dp),
shape = tokens.shapes.button,
) {
Text(confirmText)
}
@ -468,6 +475,7 @@ fun NuvioStatusModal(
fun NuvioToastHost(
modifier: Modifier = Modifier,
) {
val tokens = MaterialTheme.nuvio
val toast by NuvioToastController.currentToast.collectAsState()
val statusBarTop = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
val visibilityState = remember { MutableTransitionState(false) }
@ -505,21 +513,21 @@ fun NuvioToastHost(
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = statusBarTop + 12.dp)
.padding(horizontal = 16.dp),
.padding(top = statusBarTop + tokens.spacing.listGap)
.padding(horizontal = tokens.spacing.screenHorizontal),
contentAlignment = Alignment.TopCenter,
) {
Surface(
shape = RoundedCornerShape(18.dp),
color = MaterialTheme.colorScheme.surfaceContainerHigh,
tonalElevation = 6.dp,
shadowElevation = 10.dp,
shape = RoundedCornerShape(NuvioTokens.Radius.xl),
color = tokens.colors.surfacePopover,
tonalElevation = tokens.elevation.raised,
shadowElevation = tokens.elevation.overlay,
) {
Text(
text = currentToast.message,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
modifier = Modifier.padding(horizontal = NuvioTokens.Space.s16, vertical = NuvioTokens.Space.s12),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
)
}
}

View file

@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DeleteOutline
import androidx.compose.material.icons.filled.Info
@ -52,6 +51,7 @@ fun NuvioContinueWatchingActionSheet(
onRemove: () -> Unit,
) {
if (item == null) return
val tokens = MaterialTheme.nuvio
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val coroutineScope = rememberCoroutineScope()
@ -73,7 +73,7 @@ fun NuvioContinueWatchingActionSheet(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = nuvioSafeBottomPadding(16.dp)),
.padding(bottom = nuvioSafeBottomPadding(tokens.spacing.screenHorizontal)),
) {
ContinueWatchingSheetHeader(item = item)
if (showDetailsOption) {
@ -115,19 +115,20 @@ private fun ContinueWatchingSheetHeader(
item: ContinueWatchingItem,
) {
val posterCardStyle = rememberPosterCardStyleUiState()
val tokens = MaterialTheme.nuvio
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 14.dp),
horizontalArrangement = Arrangement.spacedBy(14.dp),
.padding(horizontal = tokens.spacing.screenHorizontal, vertical = NuvioTokens.Space.s14),
horizontalArrangement = Arrangement.spacedBy(NuvioTokens.Space.s14),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.size(width = 64.dp, height = 92.dp)
.clip(RoundedCornerShape(posterCardStyle.cornerRadiusDp.dp))
.background(MaterialTheme.colorScheme.surfaceVariant),
.size(width = NuvioTokens.Space.s64, height = NuvioTokens.Space.s80 + NuvioTokens.Space.s12)
.clip(androidx.compose.foundation.shape.RoundedCornerShape(posterCardStyle.cornerRadiusDp.dp))
.background(tokens.colors.surfaceCard),
contentAlignment = Alignment.Center,
) {
val artwork = item.poster ?: item.imageUrl
@ -141,9 +142,9 @@ private fun ContinueWatchingSheetHeader(
} else {
Text(
text = item.title,
modifier = Modifier.padding(12.dp),
modifier = Modifier.padding(tokens.spacing.listGap),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)
@ -152,12 +153,12 @@ private fun ContinueWatchingSheetHeader(
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(NuvioTokens.Space.s4),
) {
Text(
text = item.title,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
fontWeight = FontWeight.SemiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
@ -165,7 +166,7 @@ private fun ContinueWatchingSheetHeader(
Text(
text = localizedContinueWatchingSubtitle(item),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)

View file

@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.KeyboardArrowDown
@ -31,7 +30,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
data class NuvioDropdownOption(
@ -50,14 +48,15 @@ fun NuvioDropdownChip(
onSelected: (NuvioDropdownOption) -> Unit,
modifier: Modifier = Modifier,
) {
val tokens = MaterialTheme.nuvio
var isSheetVisible by remember { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val coroutineScope = rememberCoroutineScope()
Row(
modifier = modifier
.clip(RoundedCornerShape(12.dp))
.background(MaterialTheme.colorScheme.surface)
.clip(tokens.shapes.compactCard)
.background(tokens.colors.surface)
.then(
if (enabled) {
Modifier.clickable { isSheetVisible = true }
@ -65,22 +64,22 @@ fun NuvioDropdownChip(
Modifier
},
)
.padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
.padding(horizontal = NuvioTokens.Space.s12, vertical = tokens.components.chipVerticalPadding),
horizontalArrangement = Arrangement.spacedBy(NuvioTokens.Space.s6),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = label,
style = MaterialTheme.typography.labelLarge,
color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
color = if (enabled) tokens.colors.textPrimary else tokens.colors.textDisabled,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = if (enabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.outline,
modifier = Modifier.size(NuvioTokens.Icon.sm + NuvioTokens.Space.s2),
tint = if (enabled) tokens.colors.textMuted else tokens.colors.borderDefault,
)
}
@ -121,6 +120,7 @@ private fun NuvioDropdownOptionsSheet(
onDismiss: () -> Unit,
onSelected: (NuvioDropdownOption) -> Unit,
) {
val tokens = MaterialTheme.nuvio
NuvioModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
@ -128,19 +128,19 @@ private fun NuvioDropdownOptionsSheet(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = nuvioSafeBottomPadding(16.dp)),
.padding(bottom = nuvioSafeBottomPadding(tokens.spacing.screenHorizontal)),
) {
Text(
text = title,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
modifier = Modifier.padding(horizontal = tokens.spacing.screenHorizontal, vertical = NuvioTokens.Space.s14),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
)
NuvioBottomSheetDivider()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 420.dp),
.heightIn(max = tokens.breakpoints.largePhone),
) {
itemsIndexed(options) { index, option ->
NuvioBottomSheetActionRow(
@ -151,8 +151,8 @@ private fun NuvioDropdownOptionsSheet(
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp),
tint = tokens.colors.accent,
modifier = Modifier.size(tokens.icons.md),
)
}
},

View file

@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.PlayArrow
@ -73,6 +72,7 @@ fun NuvioFloatingPrompt(
modifier: Modifier = Modifier,
autoDismissMs: Long = AutoDismissDelayMs,
) {
val tokens = MaterialTheme.nuvio
val visibilityState = remember { MutableTransitionState(false) }
val coroutineScope = rememberCoroutineScope()
val density = LocalDensity.current
@ -98,12 +98,12 @@ fun NuvioFloatingPrompt(
LaunchedEffect(Unit) {
delay(autoDismissMs)
val dismissDistance = maxOf(
promptHeightPx.toFloat() + with(density) { 24.dp.toPx() },
with(density) { 160.dp.toPx() },
promptHeightPx.toFloat() + with(density) { tokens.spacing.sectionGap.toPx() },
with(density) { (NuvioTokens.Space.s80 + NuvioTokens.Space.s80).toPx() },
)
dragOffsetY.animateTo(
targetValue = dismissDistance,
animationSpec = tween(durationMillis = 240),
animationSpec = tween(durationMillis = tokens.motion.normalMillis),
)
onDismiss()
}
@ -116,14 +116,14 @@ fun NuvioFloatingPrompt(
AnimatedVisibility(
visibleState = visibilityState,
modifier = modifier,
enter = fadeIn(tween(400)) + slideInVertically(tween(400)) { it },
exit = fadeOut(tween(300)) + slideOutVertically(tween(300)) { it },
enter = fadeIn(tween(tokens.motion.slowMillis)) + slideInVertically(tween(tokens.motion.slowMillis)) { it },
exit = fadeOut(tween(tokens.motion.sheetEnterMillis)) + slideOutVertically(tween(tokens.motion.sheetEnterMillis)) { it },
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = navBarBottom + 72.dp)
.padding(horizontal = 16.dp)
.padding(bottom = navBarBottom + NuvioTokens.Space.s72)
.padding(horizontal = tokens.spacing.screenHorizontal)
.offset { IntOffset(0, dragOffsetY.value.roundToInt().coerceAtLeast(0)) }
.pointerInput(Unit) {
detectVerticalDragGestures(
@ -133,18 +133,18 @@ fun NuvioFloatingPrompt(
if (shouldDismiss) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
val dismissDistance = maxOf(
promptHeightPx.toFloat() + with(density) { 24.dp.toPx() },
with(density) { 160.dp.toPx() },
promptHeightPx.toFloat() + with(density) { tokens.spacing.sectionGap.toPx() },
with(density) { (NuvioTokens.Space.s80 + NuvioTokens.Space.s80).toPx() },
)
dragOffsetY.animateTo(
targetValue = dismissDistance,
animationSpec = tween(durationMillis = 220),
animationSpec = tween(durationMillis = tokens.motion.normalMillis),
)
onDismiss()
} else {
dragOffsetY.animateTo(
targetValue = 0f,
animationSpec = tween(durationMillis = 180),
animationSpec = tween(durationMillis = tokens.motion.fastMillis),
)
}
}
@ -153,7 +153,7 @@ fun NuvioFloatingPrompt(
coroutineScope.launch {
dragOffsetY.animateTo(
targetValue = 0f,
animationSpec = tween(durationMillis = 180),
animationSpec = tween(durationMillis = tokens.motion.fastMillis),
)
}
},
@ -167,27 +167,27 @@ fun NuvioFloatingPrompt(
) {
Surface(
modifier = Modifier.onSizeChanged { promptHeightPx = it.height },
shape = RoundedCornerShape(20.dp),
color = MaterialTheme.colorScheme.surfaceContainerHigh,
tonalElevation = 4.dp,
shadowElevation = 8.dp,
shape = tokens.shapes.card,
color = tokens.colors.surfacePopover,
tonalElevation = tokens.elevation.playerControls,
shadowElevation = tokens.elevation.modal,
) {
Column(
modifier = Modifier.padding(horizontal = 14.dp, vertical = 14.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.padding(tokens.spacing.cardPaddingCompact),
verticalArrangement = Arrangement.spacedBy(tokens.spacing.listGap),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 2.dp),
.padding(start = NuvioTokens.Space.s2),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.listGap),
) {
Box(
modifier = Modifier
.size(width = 54.dp, height = 78.dp)
.clip(RoundedCornerShape(12.dp))
.background(MaterialTheme.colorScheme.surfaceVariant),
.size(width = NuvioTokens.Space.s56 - NuvioTokens.Space.s2, height = NuvioTokens.Space.s80 - NuvioTokens.Space.s2)
.clip(tokens.shapes.compactCard)
.background(tokens.colors.surfaceCard),
contentAlignment = Alignment.Center,
) {
if (imageUrl != null) {
@ -207,37 +207,37 @@ fun NuvioFloatingPrompt(
Text(
text = stringResource(Res.string.floating_prompt_continue_where_left_off),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
)
Text(
text = title,
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.SemiBold),
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
Box(
modifier = Modifier.size(42.dp),
modifier = Modifier.size(NuvioTokens.Space.s40 + NuvioTokens.Space.s2),
contentAlignment = Alignment.Center,
) {
FilledIconButton(
onClick = actionWithHaptic,
modifier = Modifier.size(42.dp),
shape = CircleShape,
modifier = Modifier.size(NuvioTokens.Space.s40 + NuvioTokens.Space.s2),
shape = tokens.shapes.avatar,
) {
Icon(
imageVector = Icons.Rounded.PlayArrow,
contentDescription = actionLabel,
modifier = Modifier.size(22.dp),
modifier = Modifier.size(tokens.icons.md),
)
}
}
@ -246,21 +246,21 @@ fun NuvioFloatingPrompt(
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(999.dp))
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.55f))
.height(8.dp)
.padding(1.dp),
.clip(tokens.shapes.chip)
.background(tokens.colors.playerTimelineTrack)
.height(NuvioTokens.Space.s8)
.padding(tokens.borders.thin),
) {
Box(
modifier = Modifier
.fillMaxWidth(progressFraction.coerceIn(0f, 1f))
.height(6.dp)
.clip(RoundedCornerShape(999.dp)),
.height(NuvioTokens.Space.s6)
.clip(tokens.shapes.chip),
) {
Box(
modifier = Modifier
.matchParentSize()
.background(MaterialTheme.colorScheme.primary),
.background(tokens.colors.playerTimelineFill),
)
}
}

View file

@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -22,7 +21,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.painterResource
@ -31,17 +29,18 @@ fun NuvioNavigationBar(
modifier: Modifier = Modifier,
content: @Composable NuvioNavigationBarScope.() -> Unit,
) {
val tokens = MaterialTheme.nuvio
Column(modifier.fillMaxWidth()) {
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.outlineVariant,
thickness = tokens.borders.hairline,
color = tokens.colors.borderDefault,
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(nuvioBottomNavigationBarInsets().asPaddingValues())
.padding(horizontal = 4.dp, vertical = nuvioBottomNavigationExtraVerticalPadding),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
.padding(horizontal = NuvioTokens.Space.s4, vertical = nuvioBottomNavigationExtraVerticalPadding),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap, Alignment.CenterHorizontally),
) {
NuvioNavigationBarScopeImpl(this).content()
}
@ -88,25 +87,25 @@ private class NuvioNavigationBarScopeImpl(
contentDescription: String?,
modifier: Modifier,
) {
val tokens = MaterialTheme.nuvio
val iconColor by animateColorAsState(
targetValue = if (selected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant,
targetValue = if (selected) tokens.colors.accent else tokens.colors.textMuted,
)
with(rowScope) {
Icon(
modifier = modifier
.widthIn(max = 150.dp)
.widthIn(max = tokens.components.navItemMaxWidth)
.fillMaxWidth()
.weight(1f, fill = false)
.clip(RoundedCornerShape(16.dp))
.clip(tokens.components.navItemShape)
.selectable(
selected = selected,
enabled = true,
role = Role.Tab,
onClick = onClick,
)
.padding(10.dp)
.size(28.dp),
.padding(NuvioTokens.Space.s10)
.size(tokens.components.navIconSize),
imageVector = icon,
contentDescription = contentDescription,
tint = iconColor,
@ -122,25 +121,25 @@ private class NuvioNavigationBarScopeImpl(
contentDescription: String?,
modifier: Modifier,
) {
val tokens = MaterialTheme.nuvio
val iconColor by animateColorAsState(
targetValue = if (selected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant,
targetValue = if (selected) tokens.colors.accent else tokens.colors.textMuted,
)
with(rowScope) {
Icon(
modifier = modifier
.widthIn(max = 150.dp)
.widthIn(max = tokens.components.navItemMaxWidth)
.fillMaxWidth()
.weight(1f, fill = false)
.clip(RoundedCornerShape(16.dp))
.clip(tokens.components.navItemShape)
.selectable(
selected = selected,
enabled = true,
role = Role.Tab,
onClick = onClick,
)
.padding(10.dp)
.size(28.dp),
.padding(NuvioTokens.Space.s10)
.size(tokens.components.navIconSize),
painter = painterResource(icon),
contentDescription = contentDescription,
tint = iconColor,
@ -155,20 +154,21 @@ private class NuvioNavigationBarScopeImpl(
modifier: Modifier,
content: @Composable () -> Unit,
) {
val tokens = MaterialTheme.nuvio
with(rowScope) {
Box(
modifier = modifier
.widthIn(max = 150.dp)
.widthIn(max = tokens.components.navItemMaxWidth)
.fillMaxWidth()
.weight(1f, fill = false)
.clip(RoundedCornerShape(16.dp))
.clip(tokens.components.navItemShape)
.selectable(
selected = selected,
enabled = true,
role = Role.Tab,
onClick = onClick,
)
.padding(10.dp),
.padding(NuvioTokens.Space.s10),
contentAlignment = Alignment.Center,
) {
content()

View file

@ -6,7 +6,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.nuvio.app.core.network.NetworkCondition
import com.nuvio.app.core.network.messageForEmptyState
import com.nuvio.app.core.network.titleForEmptyState
@ -20,20 +19,21 @@ fun NuvioNetworkOfflineCard(
modifier: Modifier = Modifier,
onRetry: (() -> Unit)? = null,
) {
val tokens = MaterialTheme.nuvio
NuvioSurfaceCard(modifier = modifier) {
Text(
text = condition.titleForEmptyState(),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(tokens.spacing.controlGap))
Text(
text = condition.messageForEmptyState(),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
)
if (onRetry != null) {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(tokens.spacing.screenHorizontal))
NuvioPrimaryButton(
text = stringResource(Res.string.action_retry),
onClick = onRetry,

View file

@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
@ -57,6 +56,7 @@ fun NuvioPosterActionSheet(
onToggleWatched: () -> Unit,
) {
if (item == null) return
val tokens = MaterialTheme.nuvio
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val coroutineScope = rememberCoroutineScope()
@ -74,7 +74,7 @@ fun NuvioPosterActionSheet(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = nuvioSafeBottomPadding(16.dp)),
.padding(bottom = nuvioSafeBottomPadding(tokens.spacing.screenHorizontal)),
) {
PosterSheetHeader(item = item)
NuvioBottomSheetDivider()
@ -121,18 +121,19 @@ fun NuvioPosterActionSheet(
fun NuvioWatchedBadge(
modifier: Modifier = Modifier,
) {
val tokens = MaterialTheme.nuvio
Box(
modifier = modifier
.size(22.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary),
.size(NuvioTokens.Icon.md)
.clip(tokens.shapes.avatar)
.background(tokens.colors.accent),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = stringResource(Res.string.episodes_cd_watched),
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(12.dp),
tint = tokens.colors.onAccent,
modifier = Modifier.size(NuvioTokens.Icon.xs),
)
}
}
@ -156,7 +157,7 @@ fun NuvioAnimatedWatchedBadge(
fun BoxScope.NuvioPosterWatchedOverlay(
isWatched: Boolean,
modifier: Modifier = Modifier,
padding: Dp = 6.dp,
padding: Dp = NuvioTokens.Space.s6,
) {
NuvioAnimatedWatchedBadge(
isVisible = isWatched,
@ -171,19 +172,20 @@ private fun PosterSheetHeader(
item: MetaPreview,
) {
val posterCardStyle = rememberPosterCardStyleUiState()
val tokens = MaterialTheme.nuvio
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 14.dp),
horizontalArrangement = Arrangement.spacedBy(14.dp),
.padding(horizontal = tokens.spacing.screenHorizontal, vertical = NuvioTokens.Space.s14),
horizontalArrangement = Arrangement.spacedBy(NuvioTokens.Space.s14),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.size(width = 64.dp, height = 92.dp)
.size(width = NuvioTokens.Space.s64, height = NuvioTokens.Space.s80 + NuvioTokens.Space.s12)
.clip(RoundedCornerShape(posterCardStyle.cornerRadiusDp.dp))
.background(MaterialTheme.colorScheme.surfaceVariant),
.background(tokens.colors.surfaceCard),
contentAlignment = Alignment.Center,
) {
if (item.poster != null) {
@ -196,9 +198,9 @@ private fun PosterSheetHeader(
} else {
Text(
text = item.name,
modifier = Modifier.padding(12.dp),
modifier = Modifier.padding(tokens.spacing.listGap),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)
@ -207,12 +209,12 @@ private fun PosterSheetHeader(
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(NuvioTokens.Space.s4),
) {
Text(
text = item.name,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
fontWeight = FontWeight.SemiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
@ -221,9 +223,9 @@ private fun PosterSheetHeader(
text = item.releaseInfo?.takeIf { it.isNotBlank() }?.let { formatReleaseDateForDisplay(it) }
?: item.type.replaceFirstChar { char ->
if (char.isLowerCase()) char.titlecase() else char.toString()
},
},
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)

View file

@ -5,37 +5,36 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun NuvioProgressBar(
progress: Float,
modifier: Modifier = Modifier,
height: Dp = 4.dp,
trackColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
fillColor: Color = MaterialTheme.colorScheme.primary,
height: Dp = NuvioTokens.Space.s4,
trackColor: Color = MaterialTheme.nuvio.colors.playerTimelineTrack,
fillColor: Color = MaterialTheme.nuvio.colors.playerTimelineFill,
) {
val tokens = MaterialTheme.nuvio
val clampedProgress = progress.coerceIn(0f, 1f)
Box(
modifier = modifier
.fillMaxWidth()
.height(height)
.clip(RoundedCornerShape(percent = 50))
.clip(tokens.shapes.chip)
.background(trackColor),
) {
Box(
modifier = Modifier
.fillMaxWidth(clampedProgress)
.width(0.dp)
.width(NuvioTokens.Space.none)
.height(height)
.clip(RoundedCornerShape(percent = 50))
.clip(tokens.shapes.chip)
.background(fillColor),
)
}

View file

@ -63,9 +63,10 @@ fun <T> NuvioShelfSection(
key: ((T) -> Any)? = null,
itemContent: @Composable (T) -> Unit,
) {
val tokens = MaterialTheme.nuvio
Column(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp),
verticalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap + NuvioTokens.Space.s2),
) {
if (title.isNotBlank()) {
NuvioShelfSectionHeader(
@ -111,6 +112,7 @@ fun NuvioPosterCard(
onLongClick: (() -> Unit)? = null,
) {
val posterCardStyle = rememberPosterCardStyleUiState()
val tokens = MaterialTheme.nuvio
val cardWidth = shape.cardWidth(basePosterWidthDp = posterCardStyle.widthDp)
val cardShape = RoundedCornerShape(posterCardStyle.cornerRadiusDp.dp)
val catalogLogoOverlaySize = catalogLogoOverlaySize(
@ -121,14 +123,14 @@ fun NuvioPosterCard(
Column(
modifier = modifier.width(cardWidth),
verticalArrangement = Arrangement.spacedBy(6.dp),
verticalArrangement = Arrangement.spacedBy(NuvioTokens.Space.s6),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(shape.aspectRatio)
.clip(cardShape)
.background(MaterialTheme.colorScheme.surface)
.background(tokens.colors.surface)
.posterCardClickable(onClick = onClick, onLongClick = onLongClick),
contentAlignment = Alignment.Center,
) {
@ -142,9 +144,9 @@ fun NuvioPosterCard(
} else {
Text(
text = title,
modifier = Modifier.padding(horizontal = 14.dp),
modifier = Modifier.padding(horizontal = NuvioTokens.Space.s14),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
textAlign = TextAlign.Center,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
@ -155,7 +157,7 @@ fun NuvioPosterCard(
Box(
modifier = Modifier
.align(Alignment.BottomStart)
.padding(horizontal = 10.dp, vertical = 10.dp),
.padding(horizontal = NuvioTokens.Space.s10, vertical = NuvioTokens.Space.s10),
) {
if (!bottomLeftLogoUrl.isNullOrBlank()) {
AsyncImage(
@ -170,7 +172,7 @@ fun NuvioPosterCard(
Text(
text = bottomLeftText.orEmpty(),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onPrimary,
color = tokens.colors.textPrimary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.widthIn(max = catalogLogoOverlaySize.textMaxWidth),
@ -185,7 +187,7 @@ fun NuvioPosterCard(
Text(
text = title,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
@ -193,15 +195,15 @@ fun NuvioPosterCard(
Text(
text = detailLine,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
} else {
Box(modifier = Modifier.height(0.dp))
Box(modifier = Modifier.height(NuvioTokens.Space.none))
}
} else {
Box(modifier = Modifier.height(0.dp))
Box(modifier = Modifier.height(NuvioTokens.Space.none))
}
}
}
@ -214,6 +216,7 @@ private fun NuvioShelfSectionHeader(
onViewAllClick: (() -> Unit)? = null,
viewAllPillSize: NuvioViewAllPillSize = NuvioViewAllPillSize.Default,
) {
val tokens = MaterialTheme.nuvio
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
@ -225,19 +228,19 @@ private fun NuvioShelfSectionHeader(
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onBackground,
color = tokens.colors.textPrimary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (showAccent) {
Box(
modifier = Modifier
.padding(top = 6.dp)
.width(60.dp)
.height(4.dp)
.padding(top = NuvioTokens.Space.s6)
.width(NuvioTokens.Space.s64 - NuvioTokens.Space.s4)
.height(NuvioTokens.Space.s4)
.background(
color = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(999.dp),
color = tokens.colors.accent,
shape = tokens.shapes.chip,
),
)
}
@ -256,22 +259,21 @@ private fun NuvioViewAllPill(
onClick: (() -> Unit)?,
size: NuvioViewAllPillSize,
) {
val colorScheme = MaterialTheme.colorScheme
val isAmoled = colorScheme.background == androidx.compose.ui.graphics.Color.Black && colorScheme.surface == androidx.compose.ui.graphics.Color(0xFF050505)
val horizontalPadding = if (size == NuvioViewAllPillSize.Compact) 12.dp else 18.dp
val verticalPadding = if (size == NuvioViewAllPillSize.Compact) 9.dp else 14.dp
val tokens = MaterialTheme.nuvio
val horizontalPadding = if (size == NuvioViewAllPillSize.Compact) NuvioTokens.Space.s12 else NuvioTokens.Space.s18
val verticalPadding = if (size == NuvioViewAllPillSize.Compact) NuvioTokens.Space.s8 + NuvioTokens.Space.s1 else NuvioTokens.Space.s14
val textStyle = if (size == NuvioViewAllPillSize.Compact) {
MaterialTheme.typography.labelLarge
} else {
MaterialTheme.typography.titleMedium
}
val iconSpacing = if (size == NuvioViewAllPillSize.Compact) 2.dp else 4.dp
val iconSpacing = if (size == NuvioViewAllPillSize.Compact) NuvioTokens.Space.s2 else NuvioTokens.Space.s4
Row(
modifier = Modifier
.background(
color = if (isAmoled) androidx.compose.ui.graphics.Color(0xFF0D0D0D) else colorScheme.surface,
shape = RoundedCornerShape(20.dp),
color = tokens.colors.surface,
shape = RoundedCornerShape(NuvioTokens.Radius.xl),
)
.then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier)
.padding(horizontal = horizontalPadding, vertical = verticalPadding),
@ -281,13 +283,13 @@ private fun NuvioViewAllPill(
Text(
text = stringResource(Res.string.home_view_all),
style = textStyle,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
)
Icon(
imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.height(if (size == NuvioViewAllPillSize.Compact) 16.dp else 20.dp),
tint = tokens.colors.textMuted,
modifier = Modifier.height(if (size == NuvioViewAllPillSize.Compact) NuvioTokens.Icon.sm else tokens.icons.md),
)
}
}

View file

@ -18,7 +18,6 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.jetbrains_sans_bold
import nuvio.composeapp.generated.resources.jetbrains_sans_regular
@ -53,24 +52,6 @@ private fun buildColorScheme(palette: ThemeColorPalette, amoled: Boolean = false
onError = Color(0xFFFCE5EC),
)
private val NuvioDarkColors = darkColorScheme(
primary = Color(0xFF2E86B8),
onPrimary = Color(0xFFD2E8F7),
primaryContainer = Color(0xFF102531),
onPrimaryContainer = Color(0xFFE2F1FA),
secondary = Color(0xFF8A929C),
onSecondary = Color(0xFFEEF1F3),
background = Color(0xFF020404),
onBackground = Color(0xFFF5F7F8),
surface = Color(0xFF0A0D0D),
onSurface = Color(0xFFF5F7F8),
surfaceVariant = Color(0xFF121616),
onSurfaceVariant = Color(0xFF969CA3),
outline = Color(0xFF252A2A),
error = Color(0xFFE36A8A),
onError = Color(0xFFFCE5EC),
)
private val JetBrainsSans: FontFamily
@Composable
get() = FontFamily(
@ -84,54 +65,54 @@ private val NuvioTypography: Typography
get() = Typography(
displayLarge = TextStyle(
fontFamily = JetBrainsSans,
fontSize = 38.sp,
lineHeight = 42.sp,
fontSize = NuvioTokens.Type.pageDisplay,
lineHeight = NuvioTokens.LineHeight.pageDisplay,
fontWeight = FontWeight.Bold,
letterSpacing = (-1.2).sp,
letterSpacing = NuvioTokens.LetterSpacing.pageDisplay,
),
headlineLarge = TextStyle(
fontFamily = JetBrainsSans,
fontSize = 26.sp,
lineHeight = 30.sp,
fontSize = NuvioTokens.Type.headline,
lineHeight = NuvioTokens.LineHeight.headline,
fontWeight = FontWeight.SemiBold,
letterSpacing = (-0.8).sp,
letterSpacing = NuvioTokens.LetterSpacing.headline,
),
titleLarge = TextStyle(
fontFamily = JetBrainsSans,
fontSize = 18.sp,
lineHeight = 24.sp,
fontSize = NuvioTokens.Type.titleSm,
lineHeight = NuvioTokens.LineHeight.materialTitleLarge,
fontWeight = FontWeight.SemiBold,
),
titleMedium = TextStyle(
fontFamily = JetBrainsSans,
fontSize = 16.sp,
lineHeight = 20.sp,
fontSize = NuvioTokens.Type.bodyLg,
lineHeight = NuvioTokens.LineHeight.bodyMd,
fontWeight = FontWeight.SemiBold,
),
bodyLarge = TextStyle(
fontFamily = JetBrainsSans,
fontSize = 15.sp,
lineHeight = 22.sp,
fontSize = NuvioTokens.Type.bodyApp,
lineHeight = NuvioTokens.LineHeight.bodyApp,
fontWeight = FontWeight.Normal,
),
bodyMedium = TextStyle(
fontFamily = JetBrainsSans,
fontSize = 14.sp,
lineHeight = 20.sp,
fontSize = NuvioTokens.Type.bodyMd,
lineHeight = NuvioTokens.LineHeight.bodyMd,
fontWeight = FontWeight.Normal,
),
labelLarge = TextStyle(
fontFamily = JetBrainsSans,
fontSize = 14.sp,
lineHeight = 18.sp,
fontSize = NuvioTokens.Type.bodyMd,
lineHeight = NuvioTokens.LineHeight.bodySm,
fontWeight = FontWeight.SemiBold,
),
labelMedium = TextStyle(
fontFamily = JetBrainsSans,
fontSize = 12.sp,
lineHeight = 14.sp,
fontSize = NuvioTokens.Type.labelSm,
lineHeight = NuvioTokens.LineHeight.labelXs,
fontWeight = FontWeight.SemiBold,
letterSpacing = 0.8.sp,
letterSpacing = NuvioTokens.LetterSpacing.label,
),
)
@ -140,62 +121,62 @@ private val NuvioTypeTokens: NuvioTypeScale
get() = NuvioTypeScale(
labelXs = TextStyle(
fontFamily = JetBrainsSans,
fontSize = 11.sp,
lineHeight = 14.sp,
fontSize = NuvioTokens.Type.labelXs,
lineHeight = NuvioTokens.LineHeight.labelXs,
fontWeight = FontWeight.SemiBold,
),
labelSm = TextStyle(
fontFamily = JetBrainsSans,
fontSize = 12.sp,
lineHeight = 15.sp,
fontSize = NuvioTokens.Type.labelSm,
lineHeight = NuvioTokens.LineHeight.labelSm,
fontWeight = FontWeight.SemiBold,
),
bodySm = TextStyle(
fontFamily = JetBrainsSans,
fontSize = 13.sp,
lineHeight = 18.sp,
fontSize = NuvioTokens.Type.bodySm,
lineHeight = NuvioTokens.LineHeight.bodySm,
fontWeight = FontWeight.Normal,
),
bodyMd = TextStyle(
fontFamily = JetBrainsSans,
fontSize = 14.sp,
lineHeight = 20.sp,
fontSize = NuvioTokens.Type.bodyMd,
lineHeight = NuvioTokens.LineHeight.bodyMd,
fontWeight = FontWeight.Normal,
),
bodyLg = TextStyle(
fontFamily = JetBrainsSans,
fontSize = 16.sp,
lineHeight = 22.sp,
fontSize = NuvioTokens.Type.bodyLg,
lineHeight = NuvioTokens.LineHeight.bodyLg,
fontWeight = FontWeight.Normal,
),
titleSm = TextStyle(
fontFamily = JetBrainsSans,
fontSize = 18.sp,
lineHeight = 22.sp,
fontSize = NuvioTokens.Type.titleSm,
lineHeight = NuvioTokens.LineHeight.titleSm,
fontWeight = FontWeight.SemiBold,
),
titleMd = TextStyle(
fontFamily = JetBrainsSans,
fontSize = 22.sp,
lineHeight = 26.sp,
fontSize = NuvioTokens.Type.titleMd,
lineHeight = NuvioTokens.LineHeight.titleMd,
fontWeight = FontWeight.SemiBold,
),
titleLg = TextStyle(
fontFamily = JetBrainsSans,
fontSize = 28.sp,
lineHeight = 32.sp,
fontSize = NuvioTokens.Type.titleLg,
lineHeight = NuvioTokens.LineHeight.titleLg,
fontWeight = FontWeight.SemiBold,
),
displaySm = TextStyle(
fontFamily = JetBrainsSans,
fontSize = 32.sp,
lineHeight = 36.sp,
fontSize = NuvioTokens.Type.displaySm,
lineHeight = NuvioTokens.LineHeight.displaySm,
fontWeight = FontWeight.Bold,
),
displayMd = TextStyle(
fontFamily = JetBrainsSans,
fontSize = 48.sp,
lineHeight = 52.sp,
fontSize = NuvioTokens.Type.displayMd,
lineHeight = NuvioTokens.LineHeight.displayMd,
fontWeight = FontWeight.Bold,
),
)
@ -211,7 +192,9 @@ fun NuvioTheme(
amoled: Boolean = false,
content: @Composable () -> Unit,
) {
val colorScheme = buildColorScheme(ThemeColors.getColorPalette(appTheme), amoled = amoled)
val palette = ThemeColors.getColorPalette(appTheme)
val colorScheme = buildColorScheme(palette, amoled = amoled)
val tokens = defaultNuvioThemeTokens(palette, amoled = amoled, colorScheme = colorScheme)
val density = LocalDensity.current
CompositionLocalProvider(
@ -219,6 +202,7 @@ fun NuvioTheme(
density = density.density,
fontScale = 1f,
),
LocalNuvioThemeTokens provides tokens,
LocalNuvioTypeScale provides NuvioTypeTokens,
LocalRippleConfiguration provides NuvioRippleConfiguration,
LocalAppTheme provides appTheme,

View file

@ -0,0 +1,85 @@
package com.nuvio.app.core.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@Composable
fun NuvioTokenPreviewSurface(
modifier: Modifier = Modifier,
themes: List<AppTheme> = AppTheme.entries,
) {
LazyColumn(
modifier = modifier.background(MaterialTheme.nuvio.colors.background),
contentPadding = PaddingValues(MaterialTheme.nuvio.spacing.screenHorizontal),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.nuvio.spacing.sectionGap),
) {
items(themes.size) { index ->
val theme = themes[index]
NuvioTheme(appTheme = theme) {
TokenPreviewThemeSection(theme = theme)
}
}
}
}
@Composable
private fun TokenPreviewThemeSection(theme: AppTheme) {
val tokens = MaterialTheme.nuvio
Column(
modifier = Modifier
.fillMaxWidth()
.clip(tokens.shapes.card)
.background(tokens.colors.surface)
.border(tokens.borders.thin, tokens.colors.borderSubtle, tokens.shapes.card)
.padding(tokens.spacing.cardPadding),
verticalArrangement = Arrangement.spacedBy(tokens.spacing.listGap),
) {
Text(
text = theme.name,
style = MaterialTheme.nuvioTypeScale.titleMd,
color = tokens.colors.textPrimary,
)
Row(horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap)) {
listOf(
tokens.colors.background,
tokens.colors.surface,
tokens.colors.surfaceCard,
tokens.colors.accent,
tokens.colors.success,
tokens.colors.warning,
tokens.colors.danger,
).forEach { color ->
Box(
modifier = Modifier
.size(NuvioTokens.Icon.xl)
.clip(RoundedCornerShape(NuvioTokens.Radius.sm))
.background(color)
.border(tokens.borders.hairline, tokens.colors.borderDefault, RoundedCornerShape(NuvioTokens.Radius.sm)),
)
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(NuvioTokens.Space.s8)
.clip(tokens.shapes.chip)
.background(tokens.colors.accent),
)
}
}

View file

@ -0,0 +1,526 @@
package com.nuvio.app.core.ui
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.Easing
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
object NuvioTokens {
object Space {
val none = 0.dp
val hairline = 0.5.dp
val s1 = 1.dp
val s2 = 2.dp
val s3 = 3.dp
val s4 = 4.dp
val s5 = 5.dp
val s6 = 6.dp
val s7 = 7.dp
val s8 = 8.dp
val s10 = 10.dp
val s12 = 12.dp
val s14 = 14.dp
val s16 = 16.dp
val s18 = 18.dp
val s20 = 20.dp
val s22 = 22.dp
val s24 = 24.dp
val s28 = 28.dp
val s32 = 32.dp
val s36 = 36.dp
val s40 = 40.dp
val s48 = 48.dp
val s56 = 56.dp
val s64 = 64.dp
val s72 = 72.dp
val s80 = 80.dp
val s96 = 96.dp
}
object Radius {
val none = Space.none
val xs = Space.s4
val sm = Space.s6
val md = Space.s8
val lg = Space.s12
val xl = Space.s16
val xxl = Space.s24
val full = 999.dp
val card = xxl
val compactCard = lg
val sheet = xxl
val dialog = xxl
val button = xl
val chip = full
val poster = lg
val avatar = full
val playerPanel = xxl
}
object Border {
val hairline = Space.hairline
val thin = Space.s1
val medium = Space.s2
}
object Elevation {
val none = Space.none
val raised = Space.s2
val modal = Space.s8
val overlay = Space.s12
val playerControls = Space.s4
}
object Opacity {
const val invisible = 0f
const val disabled = 0.38f
const val secondary = 0.70f
const val muted = 0.60f
const val selected = 0.15f
const val hover = 0.08f
const val pressed = 0.12f
const val subtle = 0.06f
const val medium = 0.52f
const val strong = 0.75f
const val overlayLight = 0.35f
const val overlayMedium = 0.56f
const val overlayHeavy = 0.82f
const val visible = 1f
}
object Motion {
const val instantMillis = 0
const val fastMillis = 150
const val normalMillis = 220
const val sheetEnterMillis = 300
const val sheetExitMillis = 250
const val slowMillis = 400
const val cinematicMillis = 700
val standard: Easing = CubicBezierEasing(0.2f, 0f, 0f, 1f)
val emphasized: Easing = CubicBezierEasing(0.2f, 0f, 0f, 1f)
val decelerate: Easing = CubicBezierEasing(0f, 0f, 0f, 1f)
val accelerate: Easing = CubicBezierEasing(0.3f, 0f, 1f, 1f)
}
object Icon {
val xs = Space.s12
val sm = Space.s16
val md = Space.s20
val lg = Space.s24
val xl = Space.s32
val xxl = Space.s40
}
object Type {
val labelXs = 11.sp
val labelSm = 12.sp
val bodySm = 13.sp
val bodyMd = 14.sp
val bodyApp = 15.sp
val bodyLg = 16.sp
val titleSm = 18.sp
val headline = 26.sp
val titleMd = 22.sp
val titleLg = 28.sp
val displaySm = 32.sp
val pageDisplay = 38.sp
val displayMd = 48.sp
}
object LineHeight {
val labelXs = 14.sp
val labelSm = 15.sp
val bodySm = 18.sp
val bodyMd = 20.sp
val bodyApp = 22.sp
val bodyLg = 22.sp
val titleSm = 22.sp
val materialTitleLarge = 24.sp
val headline = 30.sp
val titleMd = 26.sp
val titleLg = 32.sp
val displaySm = 36.sp
val pageDisplay = 42.sp
val displayMd = 52.sp
}
object LetterSpacing {
val none = 0.sp
val pageDisplay = (-1.2).sp
val headline = (-0.8).sp
val label = 0.8.sp
}
object Breakpoint {
val phone = 0.dp
val largePhone = 420.dp
val tablet = 600.dp
val largeTablet = 840.dp
val desktop = 1024.dp
val playerWide = 1280.dp
}
object Z {
const val base = 0f
const val stickyHeader = 2f
const val navigation = 4f
const val sheet = 8f
const val dialog = 10f
const val playerOverlay = 12f
const val toast = 16f
}
}
@Immutable
data class NuvioColorTokens(
val background: Color,
val backgroundAmoled: Color,
val surface: Color,
val surfaceElevated: Color,
val surfaceCard: Color,
val surfaceSheet: Color,
val surfaceDialog: Color,
val surfacePopover: Color,
val nativeChrome: Color,
val textPrimary: Color,
val textSecondary: Color,
val textMuted: Color,
val textDisabled: Color,
val textInverse: Color,
val accent: Color,
val accentStrong: Color,
val onAccent: Color,
val focusRing: Color,
val focusBackground: Color,
val borderSubtle: Color,
val borderDefault: Color,
val borderStrong: Color,
val borderFocus: Color,
val borderSelected: Color,
val success: Color,
val warning: Color,
val danger: Color,
val info: Color,
val neutral: Color,
val overlayScrim: Color,
val overlayHover: Color,
val overlayPressed: Color,
val overlaySelected: Color,
val overlayDisabled: Color,
val shimmer: Color,
val skeleton: Color,
val playerControlsBackground: Color,
val playerControlsForeground: Color,
val playerTimelineTrack: Color,
val playerTimelineFill: Color,
val playerSubtitlePreview: Color,
val playerBuffering: Color,
val parentalGuide: Color,
)
@Immutable
data class NuvioShapeTokens(
val card: Shape,
val compactCard: Shape,
val sheet: Shape,
val dialog: Shape,
val button: Shape,
val chip: Shape,
val poster: Shape,
val avatar: Shape,
val playerPanel: Shape,
)
@Immutable
data class NuvioSpacingTokens(
val screenHorizontal: Dp,
val screenTop: Dp,
val screenBottom: Dp,
val sectionGap: Dp,
val listGap: Dp,
val railGap: Dp,
val cardPadding: Dp,
val cardPaddingCompact: Dp,
val controlGap: Dp,
val dialogPadding: Dp,
val sheetPadding: Dp,
val playerOverlayPadding: Dp,
)
@Immutable
data class NuvioBorderTokens(
val hairline: Dp,
val thin: Dp,
val medium: Dp,
)
@Immutable
data class NuvioElevationTokens(
val flat: Dp,
val raised: Dp,
val modal: Dp,
val overlay: Dp,
val playerControls: Dp,
)
@Immutable
data class NuvioMotionTokens(
val instantMillis: Int,
val fastMillis: Int,
val normalMillis: Int,
val sheetEnterMillis: Int,
val sheetExitMillis: Int,
val slowMillis: Int,
val cinematicMillis: Int,
val standard: Easing,
val emphasized: Easing,
val decelerate: Easing,
val accelerate: Easing,
)
@Immutable
data class NuvioOpacityTokens(
val invisible: Float,
val disabled: Float,
val secondary: Float,
val muted: Float,
val selected: Float,
val hover: Float,
val pressed: Float,
val subtle: Float,
val medium: Float,
val strong: Float,
val overlayLight: Float,
val overlayMedium: Float,
val overlayHeavy: Float,
val visible: Float,
)
@Immutable
data class NuvioIconTokens(
val xs: Dp,
val sm: Dp,
val md: Dp,
val lg: Dp,
val xl: Dp,
val xxl: Dp,
)
@Immutable
data class NuvioBreakpointTokens(
val phone: Dp,
val largePhone: Dp,
val tablet: Dp,
val largeTablet: Dp,
val desktop: Dp,
val playerWide: Dp,
)
@Immutable
data class NuvioComponentTokens(
val navItemShape: Shape,
val navIconSize: Dp,
val navItemMaxWidth: Dp,
val sheetMaxWidth: Dp,
val dialogMaxWidth: Dp,
val chipHorizontalPadding: Dp,
val chipVerticalPadding: Dp,
val posterRadius: Dp,
val avatarSize: Dp,
val playerPanelMaxWidth: Dp,
)
@Immutable
data class NuvioThemeTokens(
val colors: NuvioColorTokens,
val spacing: NuvioSpacingTokens,
val shapes: NuvioShapeTokens,
val borders: NuvioBorderTokens,
val elevation: NuvioElevationTokens,
val motion: NuvioMotionTokens,
val opacity: NuvioOpacityTokens,
val icons: NuvioIconTokens,
val breakpoints: NuvioBreakpointTokens,
val components: NuvioComponentTokens,
)
internal val LocalNuvioThemeTokens = staticCompositionLocalOf {
defaultNuvioThemeTokens(ThemeColors.White, amoled = false, colorScheme = null)
}
val MaterialTheme.nuvio: NuvioThemeTokens
@Composable
@Stable
get() = LocalNuvioThemeTokens.current
internal fun defaultNuvioThemeTokens(
palette: ThemeColorPalette,
amoled: Boolean,
colorScheme: ColorScheme?,
): NuvioThemeTokens {
val background = if (amoled) Color.Black else palette.background
val textPrimary = Color(0xFFF5F7F8)
val textSecondary = Color(0xFFB8BEC5)
val textMuted = Color(0xFF969CA3)
val surface = palette.backgroundElevated
val surfaceCard = palette.backgroundCard
val accent = palette.secondary
val accentStrong = palette.secondaryVariant
val borderSubtle = Color(0xFF252A2A).copy(alpha = 0.55f)
val borderDefault = Color(0xFF252A2A)
val overlayScrim = Color.Black.copy(alpha = NuvioTokens.Opacity.overlayMedium)
return NuvioThemeTokens(
colors = NuvioColorTokens(
background = background,
backgroundAmoled = Color.Black,
surface = surface,
surfaceElevated = surface,
surfaceCard = surfaceCard,
surfaceSheet = surface,
surfaceDialog = surface,
surfacePopover = surfaceCard,
nativeChrome = background,
textPrimary = textPrimary,
textSecondary = textSecondary,
textMuted = textMuted,
textDisabled = textMuted.copy(alpha = NuvioTokens.Opacity.disabled),
textInverse = Color(0xFF111111),
accent = accent,
accentStrong = accentStrong,
onAccent = palette.onSecondary,
focusRing = palette.focusRing,
focusBackground = palette.focusBackground,
borderSubtle = borderSubtle,
borderDefault = borderDefault,
borderStrong = Color(0xFF3A4040),
borderFocus = palette.focusRing,
borderSelected = accent.copy(alpha = NuvioTokens.Opacity.strong),
success = Color(0xFF66BB6A),
warning = Color(0xFFFFC857),
danger = colorScheme?.error ?: Color(0xFFE36A8A),
info = Color(0xFF42A5F5),
neutral = textMuted,
overlayScrim = overlayScrim,
overlayHover = Color.White.copy(alpha = NuvioTokens.Opacity.hover),
overlayPressed = Color.White.copy(alpha = NuvioTokens.Opacity.pressed),
overlaySelected = Color.White.copy(alpha = NuvioTokens.Opacity.selected),
overlayDisabled = Color.Black.copy(alpha = NuvioTokens.Opacity.disabled),
shimmer = Color.White.copy(alpha = 0.10f),
skeleton = Color.White.copy(alpha = 0.06f),
playerControlsBackground = Color.Black.copy(alpha = 0.72f),
playerControlsForeground = Color.White,
playerTimelineTrack = Color.White.copy(alpha = 0.30f),
playerTimelineFill = accent,
playerSubtitlePreview = Color.Black.copy(alpha = 0.55f),
playerBuffering = accent,
parentalGuide = Color(0xFF5D1F1F),
),
spacing = NuvioSpacingTokens(
screenHorizontal = NuvioTokens.Space.s16,
screenTop = NuvioTokens.Space.s10,
screenBottom = NuvioTokens.Space.s18,
sectionGap = NuvioTokens.Space.s24,
listGap = NuvioTokens.Space.s12,
railGap = NuvioTokens.Space.s14,
cardPadding = NuvioTokens.Space.s18,
cardPaddingCompact = NuvioTokens.Space.s14,
controlGap = NuvioTokens.Space.s8,
dialogPadding = NuvioTokens.Space.s20,
sheetPadding = NuvioTokens.Space.s20,
playerOverlayPadding = NuvioTokens.Space.s24,
),
shapes = NuvioShapeTokens(
card = RoundedCornerShape(NuvioTokens.Radius.card),
compactCard = RoundedCornerShape(NuvioTokens.Radius.compactCard),
sheet = RoundedCornerShape(NuvioTokens.Radius.sheet),
dialog = RoundedCornerShape(NuvioTokens.Radius.dialog),
button = RoundedCornerShape(NuvioTokens.Radius.button),
chip = RoundedCornerShape(NuvioTokens.Radius.chip),
poster = RoundedCornerShape(NuvioTokens.Radius.poster),
avatar = RoundedCornerShape(NuvioTokens.Radius.avatar),
playerPanel = RoundedCornerShape(NuvioTokens.Radius.playerPanel),
),
borders = NuvioBorderTokens(
hairline = NuvioTokens.Border.hairline,
thin = NuvioTokens.Border.thin,
medium = NuvioTokens.Border.medium,
),
elevation = NuvioElevationTokens(
flat = NuvioTokens.Elevation.none,
raised = NuvioTokens.Elevation.raised,
modal = NuvioTokens.Elevation.modal,
overlay = NuvioTokens.Elevation.overlay,
playerControls = NuvioTokens.Elevation.playerControls,
),
motion = NuvioMotionTokens(
instantMillis = NuvioTokens.Motion.instantMillis,
fastMillis = NuvioTokens.Motion.fastMillis,
normalMillis = NuvioTokens.Motion.normalMillis,
sheetEnterMillis = NuvioTokens.Motion.sheetEnterMillis,
sheetExitMillis = NuvioTokens.Motion.sheetExitMillis,
slowMillis = NuvioTokens.Motion.slowMillis,
cinematicMillis = NuvioTokens.Motion.cinematicMillis,
standard = NuvioTokens.Motion.standard,
emphasized = NuvioTokens.Motion.emphasized,
decelerate = NuvioTokens.Motion.decelerate,
accelerate = NuvioTokens.Motion.accelerate,
),
opacity = NuvioOpacityTokens(
invisible = NuvioTokens.Opacity.invisible,
disabled = NuvioTokens.Opacity.disabled,
secondary = NuvioTokens.Opacity.secondary,
muted = NuvioTokens.Opacity.muted,
selected = NuvioTokens.Opacity.selected,
hover = NuvioTokens.Opacity.hover,
pressed = NuvioTokens.Opacity.pressed,
subtle = NuvioTokens.Opacity.subtle,
medium = NuvioTokens.Opacity.medium,
strong = NuvioTokens.Opacity.strong,
overlayLight = NuvioTokens.Opacity.overlayLight,
overlayMedium = NuvioTokens.Opacity.overlayMedium,
overlayHeavy = NuvioTokens.Opacity.overlayHeavy,
visible = NuvioTokens.Opacity.visible,
),
icons = NuvioIconTokens(
xs = NuvioTokens.Icon.xs,
sm = NuvioTokens.Icon.sm,
md = NuvioTokens.Icon.md,
lg = NuvioTokens.Icon.lg,
xl = NuvioTokens.Icon.xl,
xxl = NuvioTokens.Icon.xxl,
),
breakpoints = NuvioBreakpointTokens(
phone = NuvioTokens.Breakpoint.phone,
largePhone = NuvioTokens.Breakpoint.largePhone,
tablet = NuvioTokens.Breakpoint.tablet,
largeTablet = NuvioTokens.Breakpoint.largeTablet,
desktop = NuvioTokens.Breakpoint.desktop,
playerWide = NuvioTokens.Breakpoint.playerWide,
),
components = NuvioComponentTokens(
navItemShape = RoundedCornerShape(NuvioTokens.Radius.xl),
navIconSize = NuvioTokens.Icon.xl,
navItemMaxWidth = 150.dp,
sheetMaxWidth = 520.dp,
dialogMaxWidth = 460.dp,
chipHorizontalPadding = NuvioTokens.Space.s14,
chipVerticalPadding = NuvioTokens.Space.s8,
posterRadius = NuvioTokens.Radius.poster,
avatarSize = NuvioTokens.Space.s48,
playerPanelMaxWidth = 600.dp,
),
)
}

View file

@ -5,7 +5,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
data class NuvioTypeScale(
val labelXs: TextStyle,
@ -22,16 +21,16 @@ data class NuvioTypeScale(
internal val LocalNuvioTypeScale = staticCompositionLocalOf {
NuvioTypeScale(
labelXs = TextStyle(fontSize = 11.sp, lineHeight = 14.sp, fontWeight = FontWeight.Medium),
labelSm = TextStyle(fontSize = 12.sp, lineHeight = 15.sp, fontWeight = FontWeight.Medium),
bodySm = TextStyle(fontSize = 13.sp, lineHeight = 18.sp, fontWeight = FontWeight.Normal),
bodyMd = TextStyle(fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight.Normal),
bodyLg = TextStyle(fontSize = 16.sp, lineHeight = 22.sp, fontWeight = FontWeight.Medium),
titleSm = TextStyle(fontSize = 18.sp, lineHeight = 22.sp, fontWeight = FontWeight.Bold),
titleMd = TextStyle(fontSize = 22.sp, lineHeight = 26.sp, fontWeight = FontWeight.Bold),
titleLg = TextStyle(fontSize = 28.sp, lineHeight = 32.sp, fontWeight = FontWeight.Bold),
displaySm = TextStyle(fontSize = 32.sp, lineHeight = 36.sp, fontWeight = FontWeight.ExtraBold),
displayMd = TextStyle(fontSize = 48.sp, lineHeight = 52.sp, fontWeight = FontWeight.ExtraBold),
labelXs = TextStyle(fontSize = NuvioTokens.Type.labelXs, lineHeight = NuvioTokens.LineHeight.labelXs, fontWeight = FontWeight.Medium),
labelSm = TextStyle(fontSize = NuvioTokens.Type.labelSm, lineHeight = NuvioTokens.LineHeight.labelSm, fontWeight = FontWeight.Medium),
bodySm = TextStyle(fontSize = NuvioTokens.Type.bodySm, lineHeight = NuvioTokens.LineHeight.bodySm, fontWeight = FontWeight.Normal),
bodyMd = TextStyle(fontSize = NuvioTokens.Type.bodyMd, lineHeight = NuvioTokens.LineHeight.bodyMd, fontWeight = FontWeight.Normal),
bodyLg = TextStyle(fontSize = NuvioTokens.Type.bodyLg, lineHeight = NuvioTokens.LineHeight.bodyLg, fontWeight = FontWeight.Medium),
titleSm = TextStyle(fontSize = NuvioTokens.Type.titleSm, lineHeight = NuvioTokens.LineHeight.titleSm, fontWeight = FontWeight.Bold),
titleMd = TextStyle(fontSize = NuvioTokens.Type.titleMd, lineHeight = NuvioTokens.LineHeight.titleMd, fontWeight = FontWeight.Bold),
titleLg = TextStyle(fontSize = NuvioTokens.Type.titleLg, lineHeight = NuvioTokens.LineHeight.titleLg, fontWeight = FontWeight.Bold),
displaySm = TextStyle(fontSize = NuvioTokens.Type.displaySm, lineHeight = NuvioTokens.LineHeight.displaySm, fontWeight = FontWeight.ExtraBold),
displayMd = TextStyle(fontSize = NuvioTokens.Type.displayMd, lineHeight = NuvioTokens.LineHeight.displayMd, fontWeight = FontWeight.ExtraBold),
)
}

View file

@ -5,6 +5,7 @@ import androidx.compose.ui.graphics.Color
data class ThemeColorPalette(
val secondary: Color,
val secondaryVariant: Color,
val nativeAccentHex: String,
val onSecondary: Color = Color.White,
val onSecondaryVariant: Color = Color.White,
val focusRing: Color,
@ -19,6 +20,7 @@ object ThemeColors {
val Crimson = ThemeColorPalette(
secondary = Color(0xFFE53935),
secondaryVariant = Color(0xFFC62828),
nativeAccentHex = "#E53935",
focusRing = Color(0xFFFF5252),
focusBackground = Color(0xFF3D1A1A),
background = Color(0xFF0D0D0D),
@ -29,6 +31,7 @@ object ThemeColors {
val Ocean = ThemeColorPalette(
secondary = Color(0xFF1E88E5),
secondaryVariant = Color(0xFF1565C0),
nativeAccentHex = "#1E88E5",
focusRing = Color(0xFF42A5F5),
focusBackground = Color(0xFF1A2D3D),
background = Color(0xFF0D0D0F),
@ -39,6 +42,7 @@ object ThemeColors {
val Violet = ThemeColorPalette(
secondary = Color(0xFF8E24AA),
secondaryVariant = Color(0xFF6A1B9A),
nativeAccentHex = "#8E24AA",
focusRing = Color(0xFFAB47BC),
focusBackground = Color(0xFF2D1A3D),
background = Color(0xFF0D0D0F),
@ -49,6 +53,7 @@ object ThemeColors {
val Emerald = ThemeColorPalette(
secondary = Color(0xFF43A047),
secondaryVariant = Color(0xFF2E7D32),
nativeAccentHex = "#43A047",
focusRing = Color(0xFF66BB6A),
focusBackground = Color(0xFF1A3D1E),
background = Color(0xFF0D0D0D),
@ -59,6 +64,7 @@ object ThemeColors {
val Amber = ThemeColorPalette(
secondary = Color(0xFFFB8C00),
secondaryVariant = Color(0xFFEF6C00),
nativeAccentHex = "#FB8C00",
focusRing = Color(0xFFFFA726),
focusBackground = Color(0xFF3D2D1A),
background = Color(0xFF0F0D0D),
@ -69,6 +75,7 @@ object ThemeColors {
val Rose = ThemeColorPalette(
secondary = Color(0xFFD81B60),
secondaryVariant = Color(0xFFC2185B),
nativeAccentHex = "#D81B60",
focusRing = Color(0xFFEC407A),
focusBackground = Color(0xFF3D1A2D),
background = Color(0xFF0D0D0D),
@ -79,6 +86,7 @@ object ThemeColors {
val White = ThemeColorPalette(
secondary = Color(0xFFF5F5F5),
secondaryVariant = Color(0xFFE0E0E0),
nativeAccentHex = "#F5F5F5",
onSecondary = Color(0xFF111111),
onSecondaryVariant = Color(0xFF111111),
focusRing = Color(0xFFFFFFFF),

View file

@ -26,7 +26,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.nuvio.app.features.trakt.TraktListTab
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.action_cancel
@ -49,35 +48,36 @@ fun TraktListPickerDialog(
onDismiss: () -> Unit,
) {
if (!visible) return
val tokens = MaterialTheme.nuvio
BasicAlertDialog(
onDismissRequest = onDismiss,
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(20.dp),
color = tokens.colors.surfaceDialog,
shape = RoundedCornerShape(NuvioTokens.Radius.xl),
) {
Column(
modifier = Modifier.padding(18.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.padding(tokens.spacing.cardPadding),
verticalArrangement = Arrangement.spacedBy(tokens.spacing.listGap),
) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
)
Text(
text = stringResource(Res.string.compose_trakt_list_picker_subtitle),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
)
if (!errorMessage.isNullOrBlank()) {
Text(
text = errorMessage,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
color = tokens.colors.danger,
)
}
@ -85,21 +85,21 @@ fun TraktListPickerDialog(
Box(
modifier = Modifier
.fillMaxWidth()
.height(280.dp),
.height(NuvioTokens.Space.s80 + NuvioTokens.Space.s80 + NuvioTokens.Space.s80 + NuvioTokens.Space.s40),
contentAlignment = Alignment.Center,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(tokens.spacing.listGap),
) {
CircularProgressIndicator(
strokeWidth = 2.dp,
modifier = Modifier.size(24.dp),
strokeWidth = tokens.borders.medium,
modifier = Modifier.size(tokens.icons.lg),
)
Text(
text = stringResource(Res.string.compose_trakt_list_picker_loading),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
)
}
}
@ -107,8 +107,8 @@ fun TraktListPickerDialog(
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.height(280.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
.height(NuvioTokens.Space.s80 + NuvioTokens.Space.s80 + NuvioTokens.Space.s80 + NuvioTokens.Space.s40),
verticalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap),
) {
items(items = tabs, key = { it.key }) { tab ->
val selected = membership[tab.key] == true
@ -117,27 +117,27 @@ fun TraktListPickerDialog(
.fillMaxWidth()
.background(
color = if (selected) {
MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)
tokens.colors.accent.copy(alpha = tokens.opacity.selected)
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)
tokens.colors.surfaceCard.copy(alpha = tokens.opacity.medium)
},
shape = RoundedCornerShape(12.dp),
shape = tokens.shapes.compactCard,
)
.clickable(enabled = !isPending) { onToggle(tab.key) }
.padding(horizontal = 14.dp, vertical = 12.dp),
.padding(horizontal = NuvioTokens.Space.s14, vertical = tokens.spacing.listGap),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = tab.title,
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
)
if (selected) {
androidx.compose.material3.Icon(
imageVector = Icons.Rounded.Check,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
tint = tokens.colors.accent,
)
}
}
@ -147,14 +147,14 @@ fun TraktListPickerDialog(
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End),
horizontalArrangement = Arrangement.spacedBy(NuvioTokens.Space.s10, Alignment.End),
) {
Button(
onClick = onDismiss,
enabled = !isPending,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurface,
containerColor = tokens.colors.surfaceCard,
contentColor = tokens.colors.textPrimary,
),
) {
Text(stringResource(Res.string.action_cancel))
@ -165,9 +165,9 @@ fun TraktListPickerDialog(
) {
if (isPending) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp,
modifier = Modifier.size(16.dp),
color = tokens.colors.onAccent,
strokeWidth = tokens.borders.medium,
modifier = Modifier.size(tokens.icons.sm),
)
} else {
Text(stringResource(Res.string.action_save))

View file

@ -30,10 +30,8 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
@ -55,10 +53,10 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import com.nuvio.app.core.ui.NuvioTokens
import com.nuvio.app.core.ui.nuvio
import com.nuvio.app.features.debrid.DebridSettingsRepository
import com.nuvio.app.features.details.MetaVideo
import com.nuvio.app.features.streams.StreamItem
@ -97,12 +95,12 @@ fun PlayerEpisodesPanel(
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
val colorScheme = MaterialTheme.colorScheme
val tokens = MaterialTheme.nuvio
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(200)),
exit = fadeOut(tween(200)),
enter = fadeIn(tween(NuvioTokens.Motion.normalMillis)),
exit = fadeOut(tween(NuvioTokens.Motion.normalMillis)),
) {
Box(
modifier = modifier
@ -112,22 +110,24 @@ fun PlayerEpisodesPanel(
interactionSource = remember { MutableInteractionSource() },
onClick = onDismiss,
)
.background(colorScheme.scrim.copy(alpha = 0.52f)),
.background(tokens.colors.overlayScrim.copy(alpha = tokens.opacity.medium)),
contentAlignment = Alignment.Center,
) {
AnimatedVisibility(
visible = visible,
enter = slideInVertically(tween(300)) { it / 3 } + fadeIn(tween(300)),
exit = slideOutVertically(tween(250)) { it / 3 } + fadeOut(tween(250)),
enter = slideInVertically(tween(NuvioTokens.Motion.sheetEnterMillis)) { it / 3 } +
fadeIn(tween(NuvioTokens.Motion.sheetEnterMillis)),
exit = slideOutVertically(tween(NuvioTokens.Motion.sheetExitMillis)) { it / 3 } +
fadeOut(tween(NuvioTokens.Motion.sheetExitMillis)),
) {
Box(
modifier = Modifier
.widthIn(max = 520.dp)
.widthIn(max = tokens.components.playerPanelMaxWidth)
.fillMaxWidth(0.92f)
.heightIn(max = 620.dp)
.clip(RoundedCornerShape(24.dp))
.background(colorScheme.surface)
.border(1.dp, colorScheme.outlineVariant.copy(alpha = 0.8f), RoundedCornerShape(24.dp))
.heightIn(max = tokens.components.dialogMaxWidth + NuvioTokens.Space.s64)
.clip(tokens.shapes.playerPanel)
.background(tokens.colors.surfaceSheet)
.border(tokens.borders.thin, tokens.colors.borderDefault, tokens.shapes.playerPanel)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
@ -186,7 +186,7 @@ private fun EpisodesListSubView(
onEpisodeSelected: (MetaVideo) -> Unit,
onDismiss: () -> Unit,
) {
val colorScheme = MaterialTheme.colorScheme
val tokens = MaterialTheme.nuvio
val groupedEpisodes = remember(episodes) {
episodes
@ -251,14 +251,14 @@ private fun EpisodesListSubView(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 16.dp),
.padding(horizontal = tokens.spacing.sheetPadding, vertical = tokens.spacing.cardPadding),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(Res.string.compose_player_panel_episodes),
color = colorScheme.onSurface,
fontSize = 18.sp,
color = tokens.colors.textPrimary,
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold,
)
PanelChipButton(label = stringResource(Res.string.action_close), onClick = onDismiss)
@ -270,9 +270,9 @@ private fun EpisodesListSubView(
state = seasonListState,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.padding(bottom = 12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
.padding(horizontal = tokens.spacing.sheetPadding)
.padding(bottom = tokens.spacing.listGap),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap),
) {
items(availableSeasons, key = { season -> season }) { season ->
val label = if (season == 0) {
@ -297,21 +297,21 @@ private fun EpisodesListSubView(
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 40.dp),
.padding(vertical = NuvioTokens.Space.s40),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResource(Res.string.compose_player_no_episodes_available),
color = colorScheme.onSurfaceVariant,
fontSize = 14.sp,
color = tokens.colors.textMuted,
style = MaterialTheme.typography.bodyMedium,
)
}
} else {
LazyColumn(
state = episodeListState,
modifier = Modifier.padding(horizontal = 12.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = 16.dp),
modifier = Modifier.padding(horizontal = tokens.spacing.cardPaddingCompact),
verticalArrangement = Arrangement.spacedBy(NuvioTokens.Space.s4),
contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = tokens.spacing.cardPadding),
) {
itemsIndexed(
items = seasonEpisodes,
@ -352,27 +352,27 @@ private fun EpisodeRow(
blurUnwatchedEpisodes: Boolean,
onClick: () -> Unit,
) {
val colorScheme = MaterialTheme.colorScheme
val tokens = MaterialTheme.nuvio
val shouldBlurArtwork = blurUnwatchedEpisodes && !isWatched && !isCurrent
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.clip(tokens.shapes.compactCard)
.background(
if (isCurrent) colorScheme.primaryContainer.copy(alpha = 0.55f) else Color.Transparent,
if (isCurrent) tokens.colors.overlaySelected else Color.Transparent,
)
.then(
if (isCurrent) {
Modifier.border(1.dp, colorScheme.primary.copy(alpha = 0.45f), RoundedCornerShape(12.dp))
Modifier.border(tokens.borders.thin, tokens.colors.borderSelected, tokens.shapes.compactCard)
} else {
Modifier
},
)
.clickable(onClick = onClick)
.padding(horizontal = 12.dp, vertical = 10.dp),
.padding(horizontal = NuvioTokens.Space.s12, vertical = NuvioTokens.Space.s10),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.listGap),
) {
// Thumbnail
if (episode.thumbnail != null) {
@ -380,10 +380,10 @@ private fun EpisodeRow(
model = episode.thumbnail,
contentDescription = null,
modifier = Modifier
.width(80.dp)
.height(48.dp)
.clip(RoundedCornerShape(8.dp))
.then(if (shouldBlurArtwork) Modifier.blur(18.dp) else Modifier),
.width(NuvioTokens.Space.s80)
.height(NuvioTokens.Space.s48)
.clip(tokens.shapes.compactCard)
.then(if (shouldBlurArtwork) Modifier.blur(NuvioTokens.Space.s18) else Modifier),
contentScale = ContentScale.Crop,
)
}
@ -391,7 +391,7 @@ private fun EpisodeRow(
Column(modifier = Modifier.weight(1f)) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap),
) {
val episodeLabel = buildString {
if (episode.season != null && episode.episode != null) {
@ -409,22 +409,22 @@ private fun EpisodeRow(
if (episodeLabel.isNotBlank()) {
Text(
text = episodeLabel,
color = colorScheme.onSurfaceVariant,
fontSize = 11.sp,
color = tokens.colors.textMuted,
fontSize = NuvioTokens.Type.labelXs,
fontWeight = FontWeight.SemiBold,
)
}
if (isCurrent) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(999.dp))
.background(colorScheme.primaryContainer)
.padding(horizontal = 6.dp, vertical = 2.dp),
.clip(tokens.shapes.chip)
.background(tokens.colors.accent)
.padding(horizontal = NuvioTokens.Space.s6, vertical = NuvioTokens.Space.s2),
) {
Text(
text = stringResource(Res.string.compose_player_playing),
color = colorScheme.onPrimaryContainer,
fontSize = 9.sp,
color = tokens.colors.onAccent,
fontSize = NuvioTokens.Type.labelXs,
fontWeight = FontWeight.SemiBold,
)
}
@ -432,8 +432,8 @@ private fun EpisodeRow(
}
Text(
text = episode.title,
color = colorScheme.onSurface,
fontSize = 13.sp,
color = tokens.colors.textPrimary,
fontSize = NuvioTokens.Type.bodySm,
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@ -441,8 +441,8 @@ private fun EpisodeRow(
episode.overview?.let { overview ->
Text(
text = overview,
color = colorScheme.onSurfaceVariant,
fontSize = 11.sp,
color = tokens.colors.textSecondary,
fontSize = NuvioTokens.Type.labelXs,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
@ -462,7 +462,7 @@ private fun EpisodeStreamsSubView(
onReload: () -> Unit,
onDismiss: () -> Unit,
) {
val colorScheme = MaterialTheme.colorScheme
val tokens = MaterialTheme.nuvio
val debridSettings by remember {
DebridSettingsRepository.ensureLoaded()
DebridSettingsRepository.uiState
@ -476,14 +476,14 @@ private fun EpisodeStreamsSubView(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 16.dp),
.padding(horizontal = tokens.spacing.sheetPadding, vertical = tokens.spacing.cardPadding),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(Res.string.compose_player_panel_streams),
color = colorScheme.onSurface,
fontSize = 18.sp,
color = tokens.colors.textPrimary,
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold,
)
PanelChipButton(label = stringResource(Res.string.action_close), onClick = onDismiss)
@ -493,10 +493,10 @@ private fun EpisodeStreamsSubView(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.padding(bottom = 8.dp),
.padding(horizontal = tokens.spacing.sheetPadding)
.padding(bottom = tokens.spacing.controlGap),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap),
) {
PanelChipButton(
label = stringResource(Res.string.action_back),
@ -524,8 +524,8 @@ private fun EpisodeStreamsSubView(
append(episode.title)
}
},
color = colorScheme.onSurfaceVariant,
fontSize = 12.sp,
color = tokens.colors.textMuted,
fontSize = NuvioTokens.Type.labelSm,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f),
@ -541,9 +541,9 @@ private fun EpisodeStreamsSubView(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState())
.padding(horizontal = 20.dp)
.padding(bottom = 12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
.padding(horizontal = tokens.spacing.sheetPadding)
.padding(bottom = tokens.spacing.listGap),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap),
) {
AddonFilterChip(
label = stringResource(Res.string.collections_tab_all),
@ -569,13 +569,13 @@ private fun EpisodeStreamsSubView(
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 40.dp),
.padding(vertical = NuvioTokens.Space.s40),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
color = colorScheme.primary,
strokeWidth = 2.dp,
modifier = Modifier.size(28.dp),
color = tokens.colors.accent,
strokeWidth = tokens.borders.medium,
modifier = Modifier.size(tokens.icons.lg + NuvioTokens.Space.s4),
)
}
}
@ -584,13 +584,13 @@ private fun EpisodeStreamsSubView(
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 40.dp),
.padding(vertical = NuvioTokens.Space.s40),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResource(Res.string.compose_player_no_streams_found),
color = colorScheme.onSurfaceVariant,
fontSize = 14.sp,
color = tokens.colors.textMuted,
style = MaterialTheme.typography.bodyMedium,
)
}
}
@ -598,9 +598,9 @@ private fun EpisodeStreamsSubView(
else -> {
val streams = streamsUiState.filteredGroups.flatMap { it.streams }
LazyColumn(
modifier = Modifier.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = 16.dp),
modifier = Modifier.padding(horizontal = tokens.spacing.cardPadding),
verticalArrangement = Arrangement.spacedBy(NuvioTokens.Space.s6),
contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = tokens.spacing.cardPadding),
) {
itemsIndexed(
items = streams,
@ -624,23 +624,23 @@ private fun EpisodeSourceStreamRow(
enabled: Boolean,
onClick: () -> Unit,
) {
val colorScheme = MaterialTheme.colorScheme
val tokens = MaterialTheme.nuvio
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(colorScheme.surfaceVariant.copy(alpha = 0.35f))
.clip(tokens.shapes.compactCard)
.background(tokens.colors.surfaceCard)
.clickable(enabled = enabled, onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
.padding(horizontal = tokens.spacing.cardPadding, vertical = tokens.spacing.listGap),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.listGap),
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = stream.streamLabel,
color = colorScheme.onSurface,
fontSize = 14.sp,
color = tokens.colors.textPrimary,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@ -649,8 +649,8 @@ private fun EpisodeSourceStreamRow(
if (subtitle != stream.streamLabel) {
Text(
text = subtitle,
color = colorScheme.onSurfaceVariant,
fontSize = 12.sp,
color = tokens.colors.textSecondary,
style = MaterialTheme.typography.bodySmall,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
@ -658,8 +658,8 @@ private fun EpisodeSourceStreamRow(
}
Text(
text = stream.addonName,
color = colorScheme.onSurfaceVariant,
fontSize = 11.sp,
color = tokens.colors.textMuted,
fontSize = NuvioTokens.Type.labelXs,
fontStyle = FontStyle.Italic,
maxLines = 1,
overflow = TextOverflow.Ellipsis,

View file

@ -23,13 +23,10 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.CircularProgressIndicator
@ -43,13 +40,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nuvio.app.core.ui.NuvioTokens
import com.nuvio.app.core.ui.nuvio
import com.nuvio.app.features.debrid.DebridSettingsRepository
import com.nuvio.app.features.streams.StreamBadge
import com.nuvio.app.features.streams.StreamBadgeImage
@ -74,7 +70,7 @@ fun PlayerSourcesPanel(
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
val colorScheme = MaterialTheme.colorScheme
val tokens = MaterialTheme.nuvio
val debridSettings by remember {
DebridSettingsRepository.ensureLoaded()
DebridSettingsRepository.uiState
@ -86,8 +82,8 @@ fun PlayerSourcesPanel(
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(200)),
exit = fadeOut(tween(200)),
enter = fadeIn(tween(NuvioTokens.Motion.normalMillis)),
exit = fadeOut(tween(NuvioTokens.Motion.normalMillis)),
) {
Box(
modifier = modifier
@ -97,22 +93,24 @@ fun PlayerSourcesPanel(
interactionSource = remember { MutableInteractionSource() },
onClick = onDismiss,
)
.background(colorScheme.scrim.copy(alpha = 0.52f)),
.background(tokens.colors.overlayScrim.copy(alpha = tokens.opacity.medium)),
contentAlignment = Alignment.Center,
) {
AnimatedVisibility(
visible = visible,
enter = slideInVertically(tween(300)) { it / 3 } + fadeIn(tween(300)),
exit = slideOutVertically(tween(250)) { it / 3 } + fadeOut(tween(250)),
enter = slideInVertically(tween(NuvioTokens.Motion.sheetEnterMillis)) { it / 3 } +
fadeIn(tween(NuvioTokens.Motion.sheetEnterMillis)),
exit = slideOutVertically(tween(NuvioTokens.Motion.sheetExitMillis)) { it / 3 } +
fadeOut(tween(NuvioTokens.Motion.sheetExitMillis)),
) {
Box(
modifier = Modifier
.widthIn(max = 520.dp)
.widthIn(max = tokens.components.playerPanelMaxWidth)
.fillMaxWidth(0.92f)
.heightIn(max = 600.dp)
.clip(RoundedCornerShape(24.dp))
.background(colorScheme.surface)
.border(1.dp, colorScheme.outlineVariant.copy(alpha = 0.8f), RoundedCornerShape(24.dp))
.heightIn(max = tokens.components.dialogMaxWidth + NuvioTokens.Space.s40)
.clip(tokens.shapes.playerPanel)
.background(tokens.colors.surfaceSheet)
.border(tokens.borders.thin, tokens.colors.borderDefault, tokens.shapes.playerPanel)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
@ -124,17 +122,17 @@ fun PlayerSourcesPanel(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 16.dp),
.padding(horizontal = tokens.spacing.sheetPadding, vertical = tokens.spacing.cardPadding),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(Res.string.compose_player_panel_sources),
color = colorScheme.onSurface,
fontSize = 18.sp,
color = tokens.colors.textPrimary,
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold,
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap)) {
PanelChipButton(
label = stringResource(Res.string.compose_action_reload),
icon = Icons.Rounded.Refresh,
@ -156,9 +154,9 @@ fun PlayerSourcesPanel(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState())
.padding(horizontal = 20.dp)
.padding(bottom = 12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
.padding(horizontal = tokens.spacing.sheetPadding)
.padding(bottom = tokens.spacing.listGap),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap),
) {
AddonFilterChip(
label = stringResource(Res.string.collections_tab_all),
@ -184,13 +182,13 @@ fun PlayerSourcesPanel(
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 40.dp),
.padding(vertical = NuvioTokens.Space.s40),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
color = colorScheme.primary,
strokeWidth = 2.dp,
modifier = Modifier.size(28.dp),
color = tokens.colors.accent,
strokeWidth = tokens.borders.medium,
modifier = Modifier.size(tokens.icons.lg + NuvioTokens.Space.s4),
)
}
}
@ -199,13 +197,13 @@ fun PlayerSourcesPanel(
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 40.dp),
.padding(vertical = NuvioTokens.Space.s40),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResource(Res.string.compose_player_no_streams_found),
color = colorScheme.onSurfaceVariant,
fontSize = 14.sp,
color = tokens.colors.textMuted,
style = MaterialTheme.typography.bodyMedium,
)
}
}
@ -213,9 +211,9 @@ fun PlayerSourcesPanel(
else -> {
val streams = streamsUiState.filteredGroups.flatMap { it.streams }
LazyColumn(
modifier = Modifier.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = 16.dp),
modifier = Modifier.padding(horizontal = tokens.spacing.cardPadding),
verticalArrangement = Arrangement.spacedBy(NuvioTokens.Space.s6),
contentPadding = androidx.compose.foundation.layout.PaddingValues(bottom = tokens.spacing.cardPadding),
) {
itemsIndexed(
items = streams,
@ -254,36 +252,36 @@ private fun SourceStreamRow(
badgePlacement: StreamBadgePlacement,
onClick: () -> Unit,
) {
val colorScheme = MaterialTheme.colorScheme
val cardShape = RoundedCornerShape(12.dp)
val tokens = MaterialTheme.nuvio
val cardShape = tokens.shapes.compactCard
val badgeImages = stream.badges.filter { it.imageURL.isNotBlank() }
val hasBadgeMetadata = badgeImages.isNotEmpty() || (showFileSizeBadges && stream.behaviorHints.videoSize != null)
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 68.dp)
.heightIn(min = NuvioTokens.Space.s64 + NuvioTokens.Space.s4)
.shadow(
elevation = 2.dp,
elevation = tokens.elevation.raised,
shape = cardShape,
ambientColor = Color.Black.copy(alpha = 0.04f),
spotColor = Color.Black.copy(alpha = 0.04f),
ambientColor = tokens.colors.overlayScrim.copy(alpha = tokens.opacity.subtle),
spotColor = tokens.colors.overlayScrim.copy(alpha = tokens.opacity.subtle),
)
.clip(cardShape)
.background(
if (isCurrent) colorScheme.primaryContainer.copy(alpha = 0.4f) else Color.White.copy(alpha = 0.05f),
if (isCurrent) tokens.colors.overlaySelected else tokens.colors.surfaceCard,
)
.then(
if (isCurrent) {
Modifier.border(1.dp, colorScheme.primary.copy(alpha = 0.45f), cardShape)
Modifier.border(tokens.borders.thin, tokens.colors.borderSelected, cardShape)
} else {
Modifier
},
)
.clickable(enabled = enabled, onClick = onClick)
.padding(14.dp),
.padding(tokens.spacing.cardPaddingCompact),
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.listGap),
) {
Column(modifier = Modifier.weight(1f)) {
if (hasBadgeMetadata && badgePlacement == StreamBadgePlacement.TOP) {
@ -292,35 +290,33 @@ private fun SourceStreamRow(
stream = stream,
showFileSizeBadges = showFileSizeBadges,
)
Spacer(modifier = Modifier.height(6.dp))
Spacer(modifier = Modifier.height(NuvioTokens.Space.s6))
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap),
) {
Text(
text = stream.streamLabel,
color = colorScheme.onSurface,
color = tokens.colors.textPrimary,
style = MaterialTheme.typography.bodyMedium.copy(
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
lineHeight = 20.sp,
letterSpacing = 0.1.sp,
letterSpacing = NuvioTokens.LetterSpacing.none,
),
modifier = Modifier.weight(1f),
)
if (isCurrent) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(999.dp))
.background(colorScheme.primaryContainer)
.padding(horizontal = 8.dp, vertical = 3.dp),
.clip(tokens.shapes.chip)
.background(tokens.colors.accent)
.padding(horizontal = NuvioTokens.Space.s8, vertical = NuvioTokens.Space.s3),
) {
Text(
text = stringResource(Res.string.compose_player_playing),
color = colorScheme.onPrimaryContainer,
fontSize = 10.sp,
color = tokens.colors.onAccent,
fontSize = NuvioTokens.Type.labelXs,
fontWeight = FontWeight.SemiBold,
)
}
@ -329,18 +325,15 @@ private fun SourceStreamRow(
val subtitle = stream.streamSubtitle
if (!subtitle.isNullOrBlank() && subtitle != stream.streamLabel) {
Spacer(modifier = Modifier.height(2.dp))
Spacer(modifier = Modifier.height(NuvioTokens.Space.s2))
Text(
text = subtitle,
style = MaterialTheme.typography.bodySmall.copy(
fontSize = 12.sp,
lineHeight = 18.sp,
),
color = colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
color = tokens.colors.textSecondary,
)
}
Spacer(modifier = Modifier.height(6.dp))
Spacer(modifier = Modifier.height(NuvioTokens.Space.s6))
if (badgePlacement == StreamBadgePlacement.BOTTOM) {
SourceStreamBadgeRow(
badgeImages = badgeImages,
@ -349,9 +342,9 @@ private fun SourceStreamRow(
) {
Text(
text = stream.addonName,
modifier = if (hasBadgeMetadata) Modifier.padding(start = 4.dp) else Modifier,
color = colorScheme.onSurfaceVariant,
fontSize = 11.sp,
modifier = if (hasBadgeMetadata) Modifier.padding(start = NuvioTokens.Space.s4) else Modifier,
color = tokens.colors.textMuted,
fontSize = NuvioTokens.Type.labelXs,
fontStyle = FontStyle.Italic,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@ -360,8 +353,8 @@ private fun SourceStreamRow(
} else {
Text(
text = stream.addonName,
color = colorScheme.onSurfaceVariant,
fontSize = 11.sp,
color = tokens.colors.textMuted,
fontSize = NuvioTokens.Type.labelXs,
fontStyle = FontStyle.Italic,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@ -382,7 +375,7 @@ private fun SourceStreamBadgeRow(
Row(
modifier = modifier.horizontalScroll(rememberScrollState()),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(NuvioTokens.Space.s4),
) {
badgeImages.forEach { badge ->
StreamBadgeImage(badge = badge)
@ -402,46 +395,46 @@ internal fun AddonFilterChip(
hasError: Boolean = false,
onClick: () -> Unit,
) {
val colorScheme = MaterialTheme.colorScheme
val tokens = MaterialTheme.nuvio
Box(
modifier = Modifier
.clip(RoundedCornerShape(20.dp))
.clip(tokens.shapes.chip)
.background(
when {
isSelected -> colorScheme.primaryContainer
else -> colorScheme.surfaceVariant.copy(alpha = 0.92f)
isSelected -> tokens.colors.overlaySelected
else -> tokens.colors.surfacePopover
},
)
.then(
if (isSelected) {
Modifier.border(1.dp, colorScheme.primary.copy(alpha = 0.4f), RoundedCornerShape(20.dp))
Modifier.border(tokens.borders.thin, tokens.colors.borderSelected, tokens.shapes.chip)
} else {
Modifier.border(1.dp, colorScheme.outlineVariant.copy(alpha = 0.7f), RoundedCornerShape(20.dp))
Modifier.border(tokens.borders.thin, tokens.colors.borderSubtle, tokens.shapes.chip)
},
)
.clickable(onClick = onClick)
.padding(horizontal = 14.dp, vertical = 8.dp),
.padding(horizontal = NuvioTokens.Space.s14, vertical = NuvioTokens.Space.s8),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
horizontalArrangement = Arrangement.spacedBy(NuvioTokens.Space.s6),
) {
if (isLoading) {
CircularProgressIndicator(
color = colorScheme.primary,
strokeWidth = 1.5.dp,
modifier = Modifier.size(12.dp),
color = tokens.colors.accent,
strokeWidth = tokens.borders.thin + NuvioTokens.Space.hairline,
modifier = Modifier.size(NuvioTokens.Icon.xs),
)
}
Text(
text = label,
color = when {
hasError -> colorScheme.error
isSelected -> colorScheme.onPrimaryContainer
else -> colorScheme.onSurfaceVariant
hasError -> tokens.colors.danger
isSelected -> tokens.colors.textPrimary
else -> tokens.colors.textMuted
},
fontSize = 12.sp,
fontSize = NuvioTokens.Type.labelSm,
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
)
}
@ -454,32 +447,32 @@ internal fun PanelChipButton(
onClick: () -> Unit,
icon: androidx.compose.ui.graphics.vector.ImageVector? = null,
) {
val colorScheme = MaterialTheme.colorScheme
val tokens = MaterialTheme.nuvio
Box(
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.background(colorScheme.surfaceVariant.copy(alpha = 0.9f))
.border(1.dp, colorScheme.outlineVariant.copy(alpha = 0.7f), RoundedCornerShape(16.dp))
.clip(tokens.shapes.compactCard)
.background(tokens.colors.surfacePopover)
.border(tokens.borders.thin, tokens.colors.borderSubtle, tokens.shapes.compactCard)
.clickable(onClick = onClick)
.padding(horizontal = 12.dp, vertical = 6.dp),
.padding(horizontal = NuvioTokens.Space.s12, vertical = NuvioTokens.Space.s6),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(NuvioTokens.Space.s4),
) {
if (icon != null) {
Icon(
imageVector = icon,
contentDescription = null,
tint = colorScheme.onSurfaceVariant,
modifier = Modifier.size(14.dp),
tint = tokens.colors.textMuted,
modifier = Modifier.size(NuvioTokens.Space.s14),
)
}
Text(
text = label,
color = colorScheme.onSurfaceVariant,
fontSize = 12.sp,
color = tokens.colors.textMuted,
fontSize = NuvioTokens.Type.labelSm,
)
}
}

View file

@ -27,8 +27,6 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.Backspace
import androidx.compose.material.icons.rounded.Add
@ -69,6 +67,8 @@ import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import com.nuvio.app.core.ui.NuvioTokens
import com.nuvio.app.core.ui.nuvio
import com.nuvio.app.isIos
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -87,6 +87,7 @@ fun ProfileSwitcherTab(
triggerContent: (@Composable (selected: Boolean) -> Unit)? = null,
modifier: Modifier = Modifier,
) {
val tokens = MaterialTheme.nuvio
val profileState by ProfileRepository.state.collectAsStateWithLifecycle()
val activeProfile = profileState.activeProfile
val profiles = profileState.profiles
@ -243,7 +244,7 @@ fun ProfileSwitcherTab(
if (popupVisible && profiles.isNotEmpty()) {
Popup(
alignment = Alignment.BottomCenter,
offset = IntOffset(0, with(density) { -64.dp.roundToPx() }),
offset = IntOffset(0, with(density) { -NuvioTokens.Space.s64.roundToPx() }),
properties = PopupProperties(focusable = true),
onDismissRequest = { showPopup = false },
) {
@ -256,17 +257,17 @@ fun ProfileSwitcherTab(
scaleY = popupScale.value
translationY = popupTranslateY.value
}
.shadow(16.dp, RoundedCornerShape(28.dp))
.shadow(tokens.elevation.overlay, tokens.shapes.sheet)
.background(
MaterialTheme.colorScheme.surfaceContainerHigh,
RoundedCornerShape(28.dp),
tokens.colors.surfaceSheet,
tokens.shapes.sheet,
)
.padding(16.dp),
.padding(tokens.spacing.sheetPadding),
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
// Profile avatars row
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.cardPadding),
verticalAlignment = Alignment.Top,
) {
profiles.forEachIndexed { index, profile ->
@ -340,6 +341,7 @@ private fun PopupAddProfileBubble(
delayMs: Int,
onClick: () -> Unit,
) {
val tokens = MaterialTheme.nuvio
val itemAlpha = remember { Animatable(0f) }
val itemScale = remember { Animatable(0.4f) }
@ -365,36 +367,36 @@ private fun PopupAddProfileBubble(
scaleX = itemScale.value
scaleY = itemScale.value
}
.clip(RoundedCornerShape(16.dp))
.clip(tokens.shapes.compactCard)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onClick,
)
.padding(4.dp),
.padding(NuvioTokens.Space.s4),
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant)
.border(1.5.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.4f), CircleShape),
.size(tokens.components.avatarSize)
.clip(tokens.shapes.avatar)
.background(tokens.colors.surfaceCard)
.border(tokens.borders.thin + NuvioTokens.Space.hairline, tokens.colors.borderDefault.copy(alpha = tokens.opacity.medium), tokens.shapes.avatar),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = stringResource(Res.string.compose_profile_add_profile),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(22.dp),
tint = tokens.colors.textMuted,
modifier = Modifier.size(tokens.icons.md + NuvioTokens.Space.s2),
)
}
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(NuvioTokens.Space.s4))
Text(
text = stringResource(Res.string.compose_profile_add_profile),
style = MaterialTheme.typography.labelSmall.copy(fontSize = 10.sp),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelSmall.copy(fontSize = NuvioTokens.Type.labelXs),
color = tokens.colors.textMuted,
fontWeight = FontWeight.Medium,
textAlign = TextAlign.Center,
maxLines = 1,
@ -413,6 +415,7 @@ private fun PopupProfileBubble(
onBoundsChanged: (Rect) -> Unit,
onClick: () -> Unit,
) {
val tokens = MaterialTheme.nuvio
val avatarColor = remember(profile.avatarColorHex) { parseHexColor(profile.avatarColorHex) }
val avatarItem = remember(profile.avatarId, avatars) {
profile.avatarId?.let { id -> avatars.find { it.id == id } }
@ -458,13 +461,13 @@ private fun PopupProfileBubble(
scaleX = itemScale.value * pressScale
scaleY = itemScale.value * pressScale
}
.clip(RoundedCornerShape(16.dp))
.clip(tokens.shapes.compactCard)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onClick,
)
.padding(4.dp),
.padding(NuvioTokens.Space.s4),
) {
Box(
modifier = Modifier.size(52.dp),
@ -473,7 +476,7 @@ private fun PopupProfileBubble(
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.clip(tokens.shapes.avatar)
.background(
if (avatarImageUrl != null) {
avatarItem?.bgColor?.let { parseHexColor(it) } ?: avatarColor
@ -484,19 +487,19 @@ private fun PopupProfileBubble(
.then(
when {
isSelected -> Modifier.border(
2.5.dp,
MaterialTheme.colorScheme.primary,
CircleShape,
tokens.borders.medium + NuvioTokens.Space.hairline,
tokens.colors.borderSelected,
tokens.shapes.avatar,
)
isActive -> Modifier.border(
2.dp,
tokens.borders.medium,
avatarColor.copy(alpha = 0.6f),
CircleShape,
tokens.shapes.avatar,
)
avatarImageUrl == null -> Modifier.border(
1.5.dp,
tokens.borders.thin + NuvioTokens.Space.hairline,
avatarColor.copy(alpha = 0.3f),
CircleShape,
tokens.shapes.avatar,
)
else -> Modifier
},
@ -507,13 +510,13 @@ private fun PopupProfileBubble(
AsyncImage(
model = avatarImageUrl,
contentDescription = profile.name,
modifier = Modifier.size(48.dp).clip(CircleShape),
modifier = Modifier.size(48.dp).clip(tokens.shapes.avatar),
contentScale = ContentScale.Crop,
)
} else if (profile.name.isNotBlank()) {
Text(
text = profile.name.take(1).uppercase(),
style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp),
style = MaterialTheme.typography.titleMedium.copy(fontSize = NuvioTokens.Type.titleSm),
color = avatarColor,
fontWeight = FontWeight.Bold,
)
@ -522,7 +525,7 @@ private fun PopupProfileBubble(
imageVector = Icons.Rounded.Person,
contentDescription = null,
tint = avatarColor,
modifier = Modifier.size(24.dp),
modifier = Modifier.size(tokens.icons.lg),
)
}
}
@ -533,30 +536,29 @@ private fun PopupProfileBubble(
modifier = Modifier
.align(Alignment.BottomEnd)
.size(18.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant)
.border(1.5.dp, MaterialTheme.colorScheme.surfaceContainerHigh, CircleShape),
.clip(tokens.shapes.avatar)
.background(tokens.colors.surfacePopover)
.border(tokens.borders.thin + NuvioTokens.Space.hairline, tokens.colors.surfaceSheet, tokens.shapes.avatar),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Rounded.Lock,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
tint = tokens.colors.textMuted,
modifier = Modifier.size(10.dp),
)
}
}
}
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(NuvioTokens.Space.s4))
Text(
text = profile.name.ifBlank {
stringResource(Res.string.profile_label_number, profile.profileIndex)
},
style = MaterialTheme.typography.labelSmall.copy(fontSize = 10.sp),
color = if (isSelected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelSmall.copy(fontSize = NuvioTokens.Type.labelXs),
color = if (isSelected) tokens.colors.accent else tokens.colors.textMuted,
fontWeight = if (isActive || isSelected) FontWeight.Bold else FontWeight.Medium,
textAlign = TextAlign.Center,
maxLines = 1,
@ -588,6 +590,7 @@ private fun InlinePinEntry(
onCancel: () -> Unit,
verifyPin: suspend (String) -> PinVerifyResult,
) {
val tokens = MaterialTheme.nuvio
var pin by remember { mutableStateOf("") }
var error by remember { mutableStateOf<String?>(null) }
var isVerifying by remember { mutableStateOf(false) }
@ -596,18 +599,18 @@ private fun InlinePinEntry(
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(top = 16.dp),
modifier = Modifier.padding(top = tokens.spacing.cardPadding),
) {
Text(
text = stringResource(Res.string.pin_enter_for, profileName),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
)
Spacer(modifier = Modifier.height(14.dp))
Spacer(modifier = Modifier.height(NuvioTokens.Space.s14))
// PIN dots with bounce animation
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(tokens.spacing.listGap)) {
repeat(4) { index ->
val filled = index < pin.length
val dotScale = remember { Animatable(1f) }
@ -625,9 +628,9 @@ private fun InlinePinEntry(
}
val dotColor = when {
error != null -> MaterialTheme.colorScheme.error
filled -> MaterialTheme.colorScheme.primary
else -> MaterialTheme.colorScheme.outline
error != null -> tokens.colors.danger
filled -> tokens.colors.accent
else -> tokens.colors.borderDefault
}
Box(
modifier = Modifier
@ -635,11 +638,11 @@ private fun InlinePinEntry(
scaleX = dotScale.value
scaleY = dotScale.value
}
.size(14.dp)
.clip(CircleShape)
.size(NuvioTokens.Space.s14)
.clip(tokens.shapes.avatar)
.then(
if (filled) Modifier.background(dotColor)
else Modifier.border(2.dp, dotColor, CircleShape),
else Modifier.border(tokens.borders.medium, dotColor, tokens.shapes.avatar),
),
)
}
@ -654,12 +657,12 @@ private fun InlinePinEntry(
Text(
text = error.orEmpty(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(top = 8.dp),
color = tokens.colors.danger,
modifier = Modifier.padding(top = tokens.spacing.controlGap),
)
}
Spacer(modifier = Modifier.height(14.dp))
Spacer(modifier = Modifier.height(NuvioTokens.Space.s14))
// Compact number pad
CompactPinKeypad(
@ -696,17 +699,17 @@ private fun InlinePinEntry(
},
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(tokens.spacing.controlGap))
Text(
text = stringResource(Res.string.pin_cancel),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
color = tokens.colors.accent,
fontWeight = FontWeight.SemiBold,
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.clip(tokens.shapes.compactCard)
.clickable(onClick = onCancel)
.padding(horizontal = 16.dp, vertical = 6.dp),
.padding(horizontal = tokens.spacing.cardPadding, vertical = NuvioTokens.Space.s6),
)
}
}
@ -716,6 +719,7 @@ private fun CompactPinKeypad(
onDigit: (String) -> Unit,
onBackspace: () -> Unit,
) {
val tokens = MaterialTheme.nuvio
val rows = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
@ -723,45 +727,45 @@ private fun CompactPinKeypad(
listOf("", "0", ""),
)
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap)) {
rows.forEach { row ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap, Alignment.CenterHorizontally),
) {
row.forEach { key ->
when (key) {
"" -> Spacer(modifier = Modifier.size(48.dp))
"" -> Spacer(modifier = Modifier.size(tokens.components.avatarSize))
"" -> {
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant)
.size(tokens.components.avatarSize)
.clip(tokens.shapes.avatar)
.background(tokens.colors.surfaceCard)
.clickable(onClick = onBackspace),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.Backspace,
contentDescription = stringResource(Res.string.pin_backspace),
tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.size(20.dp),
tint = tokens.colors.textPrimary,
modifier = Modifier.size(tokens.icons.md),
)
}
}
else -> {
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant)
.size(tokens.components.avatarSize)
.clip(tokens.shapes.avatar)
.background(tokens.colors.surfaceCard)
.clickable { onDigit(key) },
contentAlignment = Alignment.Center,
) {
Text(
text = key,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
fontWeight = FontWeight.Medium,
)
}
@ -780,6 +784,7 @@ fun ActiveProfileMiniAvatar(
selected: Boolean,
size: Int = 24,
) {
val tokens = MaterialTheme.nuvio
if (profile == null) {
Icon(
imageVector = Icons.Rounded.Person,
@ -798,7 +803,7 @@ fun ActiveProfileMiniAvatar(
}
val borderColor = if (selected) {
MaterialTheme.colorScheme.primary
tokens.colors.borderSelected
} else {
avatarColor.copy(alpha = 0.5f)
}
@ -806,7 +811,7 @@ fun ActiveProfileMiniAvatar(
Box(
modifier = Modifier
.size(size.dp)
.clip(CircleShape)
.clip(tokens.shapes.avatar)
.background(
if (avatarImageUrl != null) {
avatarItem?.bgColor?.let { parseHexColor(it) } ?: avatarColor
@ -814,14 +819,14 @@ fun ActiveProfileMiniAvatar(
avatarColor.copy(alpha = 0.15f)
},
)
.border(1.5.dp, borderColor, CircleShape),
.border(tokens.borders.thin + NuvioTokens.Space.hairline, borderColor, tokens.shapes.avatar),
contentAlignment = Alignment.Center,
) {
if (avatarImageUrl != null) {
AsyncImage(
model = avatarImageUrl,
contentDescription = profile.name,
modifier = Modifier.size(size.dp).clip(CircleShape),
modifier = Modifier.size(size.dp).clip(tokens.shapes.avatar),
contentScale = ContentScale.Crop,
)
} else if (profile.name.isNotBlank()) {

View file

@ -45,9 +45,11 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.nuvio.app.core.ui.NuvioTokens
import com.nuvio.app.core.ui.NuvioActionLabel
import com.nuvio.app.core.ui.NuvioBackButton
import com.nuvio.app.core.ui.NuvioSectionLabel
import com.nuvio.app.core.ui.nuvio
import com.nuvio.app.core.ui.nuvioBlockPointerPassthrough
import com.nuvio.app.features.home.HomeCatalogSettingsItem
import nuvio.composeapp.generated.resources.Res
@ -69,15 +71,14 @@ private fun SettingsCard(
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit,
) {
val colorScheme = MaterialTheme.colorScheme
val isAmoled = colorScheme.background == Color.Black && colorScheme.surface == Color(0xFF050505)
val tokens = MaterialTheme.nuvio
Surface(
modifier = modifier.fillMaxWidth(),
color = if (isAmoled) Color(0xFF0B0B0B) else colorScheme.surface,
shape = RoundedCornerShape(if (isTablet) 20.dp else 16.dp),
color = tokens.colors.surface,
shape = if (isTablet) RoundedCornerShape(NuvioTokens.Radius.xl) else tokens.shapes.compactCard,
border = BorderStroke(
0.5.dp,
colorScheme.outlineVariant.copy(alpha = if (isAmoled) 0.24f else 0.16f),
tokens.borders.hairline,
tokens.colors.borderSubtle,
),
) {
Column(content = content)
@ -100,10 +101,11 @@ internal fun SettingsGroup(
@Composable
internal fun SettingsGroupDivider(isTablet: Boolean) {
val tokens = MaterialTheme.nuvio
HorizontalDivider(
modifier = Modifier.padding(start = if (isTablet) 78.dp else 66.dp),
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.12f),
modifier = Modifier.padding(start = if (isTablet) NuvioTokens.Space.s80 - NuvioTokens.Space.s2 else NuvioTokens.Space.s64 + NuvioTokens.Space.s2),
thickness = tokens.borders.hairline,
color = tokens.colors.borderSubtle,
)
}
@ -113,29 +115,30 @@ internal fun TabletPageHeader(
showBack: Boolean,
onBack: () -> Unit,
) {
val tokens = MaterialTheme.nuvio
Row(
modifier = Modifier
.fillMaxWidth()
.nuvioBlockPointerPassthrough(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.listGap),
) {
if (showBack) {
NuvioBackButton(
onClick = onBack,
modifier = Modifier
.size(36.dp),
shape = RoundedCornerShape(12.dp),
containerColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.onSurface,
buttonSize = 36.dp,
iconSize = 20.dp,
shape = tokens.shapes.compactCard,
containerColor = tokens.colors.surface,
contentColor = tokens.colors.textPrimary,
buttonSize = NuvioTokens.Space.s36,
iconSize = tokens.icons.md,
)
}
Text(
text = title,
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onBackground,
color = tokens.colors.textPrimary,
fontWeight = FontWeight.SemiBold,
)
}
@ -148,24 +151,25 @@ internal fun SettingsSidebarItem(
selected: Boolean,
onClick: () -> Unit,
) {
val primary = MaterialTheme.colorScheme.primary
val background = if (selected) primary.copy(alpha = 0.10f) else Color.Transparent
val iconChip = if (selected) primary.copy(alpha = 0.15f) else Color.Transparent
val contentColor = if (selected) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant
val tokens = MaterialTheme.nuvio
val primary = tokens.colors.accent
val background = if (selected) primary.copy(alpha = tokens.opacity.hover) else Color.Transparent
val iconChip = if (selected) primary.copy(alpha = tokens.opacity.selected) else Color.Transparent
val contentColor = if (selected) tokens.colors.textPrimary else tokens.colors.textMuted
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 2.dp)
.background(background, RoundedCornerShape(10.dp))
.padding(horizontal = tokens.spacing.listGap, vertical = NuvioTokens.Space.s2)
.background(background, RoundedCornerShape(NuvioTokens.Space.s10))
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
.padding(horizontal = tokens.spacing.screenHorizontal, vertical = tokens.spacing.listGap),
verticalAlignment = Alignment.CenterVertically,
) {
Surface(
modifier = Modifier.size(32.dp),
modifier = Modifier.size(tokens.icons.xl),
color = iconChip,
shape = RoundedCornerShape(8.dp),
shape = RoundedCornerShape(NuvioTokens.Radius.md),
) {
Row(
modifier = Modifier.fillMaxSize(),
@ -179,7 +183,7 @@ internal fun SettingsSidebarItem(
)
}
}
Spacer(modifier = Modifier.width(12.dp))
Spacer(modifier = Modifier.width(tokens.spacing.listGap))
Text(
text = label,
style = MaterialTheme.typography.bodyLarge,
@ -196,6 +200,7 @@ internal fun SettingsSection(
actions: @Composable RowScope.() -> Unit = {},
content: @Composable ColumnScope.() -> Unit,
) {
val tokens = MaterialTheme.nuvio
Column {
Row(
modifier = Modifier.fillMaxWidth(),
@ -209,7 +214,7 @@ internal fun SettingsSection(
content = actions,
)
}
Spacer(modifier = Modifier.height(if (isTablet) 12.dp else 10.dp))
Spacer(modifier = Modifier.height(if (isTablet) tokens.spacing.listGap else NuvioTokens.Space.s10))
content()
}
}
@ -224,8 +229,8 @@ internal fun SettingsNavigationRow(
isTablet: Boolean,
onClick: () -> Unit,
) {
val tokens = MaterialTheme.nuvio
val iconSize = if (isTablet) 42.dp else 36.dp
val iconRadius = if (isTablet) 12.dp else 10.dp
val verticalPadding = if (isTablet) 16.dp else 14.dp
val horizontalPadding = if (isTablet) 20.dp else 16.dp
@ -234,7 +239,7 @@ internal fun SettingsNavigationRow(
.fillMaxWidth()
.clickable(enabled = enabled, onClick = onClick)
.padding(horizontal = horizontalPadding, vertical = verticalPadding)
.alpha(if (enabled) 1f else 0.55f),
.alpha(if (enabled) NuvioTokens.Opacity.visible else tokens.opacity.medium),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
@ -248,8 +253,8 @@ internal fun SettingsNavigationRow(
if (icon != null || iconPainter != null) {
Surface(
modifier = Modifier.size(iconSize),
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f),
shape = RoundedCornerShape(iconRadius),
color = tokens.colors.accent.copy(alpha = tokens.opacity.pressed),
shape = tokens.shapes.compactCard,
) {
Row(
modifier = Modifier.fillMaxSize(),
@ -267,7 +272,7 @@ internal fun SettingsNavigationRow(
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
tint = tokens.colors.accent,
)
}
}
@ -278,14 +283,14 @@ internal fun SettingsNavigationRow(
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
fontWeight = FontWeight.Medium,
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
modifier = Modifier.alpha(0.92f),
)
}
@ -302,6 +307,7 @@ internal fun SettingsSwitchRow(
isTablet: Boolean,
onCheckedChange: (Boolean) -> Unit,
) {
val tokens = MaterialTheme.nuvio
val verticalPadding = if (isTablet) 16.dp else 14.dp
val horizontalPadding = if (isTablet) 20.dp else 16.dp
@ -318,20 +324,20 @@ internal fun SettingsSwitchRow(
.weight(1f)
.padding(end = 12.dp)
.widthIn(max = if (isTablet) 560.dp else Dp.Unspecified)
.alpha(if (enabled) 1f else 0.55f),
.alpha(if (enabled) NuvioTokens.Opacity.visible else tokens.opacity.medium),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
fontWeight = FontWeight.Medium,
)
if (!description.isNullOrBlank()) {
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
)
}
}
@ -341,10 +347,10 @@ internal fun SettingsSwitchRow(
enabled = enabled,
modifier = Modifier.padding(start = 4.dp),
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
checkedTrackColor = MaterialTheme.colorScheme.primary,
uncheckedThumbColor = MaterialTheme.colorScheme.onSurfaceVariant,
uncheckedTrackColor = MaterialTheme.colorScheme.outlineVariant,
checkedThumbColor = tokens.colors.onAccent,
checkedTrackColor = tokens.colors.accent,
uncheckedThumbColor = tokens.colors.textMuted,
uncheckedTrackColor = tokens.colors.borderDefault,
),
)
}
@ -361,6 +367,7 @@ internal fun HomescreenCatalogRow(
dragHandleScope: ReorderableCollectionItemScope,
onPinnedDragAttempt: () -> Unit = {},
) {
val tokens = MaterialTheme.nuvio
val horizontalPadding = if (isTablet) 20.dp else 16.dp
val verticalPadding = if (isTablet) 18.dp else 16.dp
val hapticFeedback = LocalHapticFeedback.current
@ -387,7 +394,7 @@ internal fun HomescreenCatalogRow(
Text(
text = item.displayTitle,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
fontWeight = FontWeight.SemiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
@ -399,7 +406,7 @@ internal fun HomescreenCatalogRow(
item.addonName
},
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
)
Text(
text = buildString {
@ -427,7 +434,7 @@ internal fun HomescreenCatalogRow(
}
},
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
)
}
Row(
@ -438,10 +445,10 @@ internal fun HomescreenCatalogRow(
checked = item.enabled,
onCheckedChange = onEnabledChange,
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
checkedTrackColor = MaterialTheme.colorScheme.primary,
uncheckedThumbColor = MaterialTheme.colorScheme.onSurfaceVariant,
uncheckedTrackColor = MaterialTheme.colorScheme.outlineVariant,
checkedThumbColor = tokens.colors.onAccent,
checkedTrackColor = tokens.colors.accent,
uncheckedThumbColor = tokens.colors.textMuted,
uncheckedTrackColor = tokens.colors.borderDefault,
),
)
if (item.isPinnedToTop) {
@ -451,7 +458,7 @@ internal fun HomescreenCatalogRow(
Icon(
imageVector = Icons.Rounded.Lock,
contentDescription = stringResource(Res.string.settings_homescreen_pinned),
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
tint = tokens.colors.textMuted.copy(alpha = tokens.opacity.medium),
)
}
} else {
@ -471,7 +478,7 @@ internal fun HomescreenCatalogRow(
Icon(
imageVector = Icons.Rounded.Menu,
contentDescription = stringResource(Res.string.settings_homescreen_reorder),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
tint = tokens.colors.textMuted,
)
}
}
@ -490,11 +497,11 @@ internal fun HomescreenCatalogRow(
label = { Text(stringResource(Res.string.settings_homescreen_display_name)) },
placeholder = { Text(item.defaultTitle) },
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.75f),
unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.42f),
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
disabledContainerColor = MaterialTheme.colorScheme.surface,
focusedBorderColor = tokens.colors.borderFocus.copy(alpha = tokens.opacity.strong),
unfocusedBorderColor = tokens.colors.borderDefault.copy(alpha = tokens.opacity.medium),
focusedContainerColor = tokens.colors.surface,
unfocusedContainerColor = tokens.colors.surface,
disabledContainerColor = tokens.colors.surface,
),
)
}

View file

@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.AccountCircle
import androidx.compose.material.icons.rounded.Close
@ -43,6 +42,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.nuvio.app.core.ui.NuvioTokens
import com.nuvio.app.core.ui.nuvio
import com.nuvio.app.isIos
import nuvio.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@ -974,12 +975,12 @@ private fun SettingsSearchRevealItem(
AnimatedVisibility(
visibleState = visibleState,
enter = expandVertically(
animationSpec = tween(durationMillis = 220),
animationSpec = tween(durationMillis = NuvioTokens.Motion.normalMillis),
expandFrom = Alignment.Top,
) + fadeIn(
animationSpec = tween(durationMillis = 180),
animationSpec = tween(durationMillis = NuvioTokens.Motion.fastMillis),
) + slideInVertically(
animationSpec = tween(durationMillis = 220),
animationSpec = tween(durationMillis = NuvioTokens.Motion.normalMillis),
initialOffsetY = { -it / 4 },
),
) {
@ -992,17 +993,18 @@ private fun SettingsSearchField(
query: String,
onQueryChange: (String) -> Unit,
) {
val tokens = MaterialTheme.nuvio
OutlinedTextField(
value = query,
onValueChange = onQueryChange,
modifier = Modifier.fillMaxWidth(),
singleLine = true,
shape = RoundedCornerShape(14.dp),
shape = tokens.shapes.compactCard,
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
tint = tokens.colors.textMuted,
)
},
trailingIcon = if (query.isNotBlank()) {
@ -1011,7 +1013,7 @@ private fun SettingsSearchField(
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = stringResource(Res.string.compose_search_clear),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
tint = tokens.colors.textMuted,
)
}
}
@ -1021,23 +1023,24 @@ private fun SettingsSearchField(
placeholder = {
Text(
text = stringResource(Res.string.settings_search_placeholder),
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
style = MaterialTheme.typography.bodyLarge,
)
},
textStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
textStyle = MaterialTheme.typography.bodyLarge.copy(color = tokens.colors.textPrimary),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.outline,
unfocusedBorderColor = MaterialTheme.colorScheme.outline,
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
cursorColor = MaterialTheme.colorScheme.primary,
focusedBorderColor = tokens.colors.borderFocus,
unfocusedBorderColor = tokens.colors.borderDefault,
focusedContainerColor = tokens.colors.surfaceCard,
unfocusedContainerColor = tokens.colors.surfaceCard,
cursorColor = tokens.colors.accent,
),
)
}
@Composable
private fun SettingsSearchEmptyState(isTablet: Boolean) {
val tokens = MaterialTheme.nuvio
SettingsSection(
title = stringResource(Res.string.settings_search_results_section),
isTablet = isTablet,
@ -1051,7 +1054,7 @@ private fun SettingsSearchEmptyState(isTablet: Boolean) {
Text(
text = stringResource(Res.string.settings_search_empty),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
fontWeight = FontWeight.Medium,
)
}

View file

@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Style
@ -47,6 +46,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nuvio.app.core.ui.NuvioTokens
import com.nuvio.app.core.ui.nuvio
import com.nuvio.app.features.streams.STREAM_BADGE_IMPORT_LIMIT
import com.nuvio.app.features.streams.StreamBadgeChip
import com.nuvio.app.features.streams.StreamBadgeChipSize
@ -177,18 +178,19 @@ private fun StreamBadgePositionDialog(
onPlacementSelected: (StreamBadgePlacement) -> Unit,
onDismiss: () -> Unit,
) {
val tokens = MaterialTheme.nuvio
BasicAlertDialog(onDismissRequest = onDismiss) {
SettingsDialogSurface(title = stringResource(Res.string.settings_stream_badge_position_dialog_title)) {
Text(
text = stringResource(Res.string.settings_stream_badge_position_dialog_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textSecondary,
)
StreamBadgePlacement.entries.forEach { placement ->
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap),
) {
RadioButton(
selected = placement == selectedPlacement,
@ -197,7 +199,7 @@ private fun StreamBadgePositionDialog(
Text(
text = streamBadgePlacementLabel(placement),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
)
}
}
@ -219,6 +221,7 @@ private fun BadgeUrlManagerDialog(
currentRules: StreamBadgeRules,
onDismiss: () -> Unit,
) {
val tokens = MaterialTheme.nuvio
val scope = rememberCoroutineScope()
val imports = currentRules.normalized().imports
var draftUrl by rememberSaveable { mutableStateOf("") }
@ -231,7 +234,7 @@ private fun BadgeUrlManagerDialog(
Text(
text = stringResource(Res.string.settings_stream_badge_urls_description, STREAM_BADGE_IMPORT_LIMIT),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textSecondary,
)
OutlinedTextField(
value = draftUrl,
@ -246,16 +249,16 @@ private fun BadgeUrlManagerDialog(
maxLines = 4,
enabled = !isImporting,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.75f),
unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.42f),
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
disabledContainerColor = MaterialTheme.colorScheme.surface,
focusedBorderColor = tokens.colors.borderFocus.copy(alpha = tokens.opacity.strong),
unfocusedBorderColor = tokens.colors.borderDefault.copy(alpha = tokens.opacity.medium),
focusedContainerColor = tokens.colors.surface,
unfocusedContainerColor = tokens.colors.surface,
disabledContainerColor = tokens.colors.surface,
),
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap, Alignment.End),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
@ -265,7 +268,7 @@ private fun BadgeUrlManagerDialog(
STREAM_BADGE_IMPORT_LIMIT,
),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
modifier = Modifier.weight(1f),
)
Button(
@ -289,9 +292,9 @@ private fun BadgeUrlManagerDialog(
) {
if (isImporting) {
CircularProgressIndicator(
strokeWidth = 2.dp,
modifier = Modifier.size(16.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = tokens.borders.medium,
modifier = Modifier.size(tokens.icons.sm),
color = tokens.colors.onAccent,
)
} else {
Text(text = stringResource(Res.string.action_import), maxLines = 1)
@ -302,7 +305,7 @@ private fun BadgeUrlManagerDialog(
Text(
text = message,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
color = tokens.colors.danger,
)
}
@ -310,14 +313,14 @@ private fun BadgeUrlManagerDialog(
Text(
text = stringResource(Res.string.settings_fusion_badges_empty),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
)
} else {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 300.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap),
) {
items(
items = imports,
@ -343,7 +346,7 @@ private fun BadgeUrlManagerDialog(
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap, Alignment.End),
verticalAlignment = Alignment.CenterVertically,
) {
TextButton(
@ -373,26 +376,27 @@ private fun BadgeUrlRow(
onPreview: () -> Unit,
onDelete: () -> Unit,
) {
val tokens = MaterialTheme.nuvio
val containerColor = if (import.isActive) {
MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)
tokens.colors.overlaySelected
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f)
tokens.colors.surfaceCard
}
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
shape = tokens.shapes.compactCard,
color = containerColor,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 10.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
.padding(horizontal = NuvioTokens.Space.s12, vertical = NuvioTokens.Space.s10),
verticalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap),
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap),
) {
if (showActiveChoice) {
RadioButton(
@ -404,7 +408,7 @@ private fun BadgeUrlRow(
Text(
text = import.sourceUrl,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f),
@ -413,7 +417,7 @@ private fun BadgeUrlRow(
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap, Alignment.End),
) {
val status = if (import.isActive) {
stringResource(Res.string.settings_fusion_badge_url_active)
@ -428,7 +432,7 @@ private fun BadgeUrlRow(
import.groups.size,
),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
modifier = Modifier.weight(1f),
)
TextButton(
@ -438,9 +442,9 @@ private fun BadgeUrlRow(
Icon(
imageVector = Icons.Rounded.Visibility,
contentDescription = null,
modifier = Modifier.size(16.dp),
modifier = Modifier.size(tokens.icons.sm),
)
Spacer(modifier = Modifier.width(4.dp))
Spacer(modifier = Modifier.width(NuvioTokens.Space.s4))
Text(text = stringResource(Res.string.settings_fusion_badge_preview_action), maxLines = 1)
}
IconButton(
@ -463,6 +467,7 @@ private fun BadgePreviewDialog(
import: StreamBadgeImport,
onDismiss: () -> Unit,
) {
val tokens = MaterialTheme.nuvio
val sections = badgePreviewSections(import)
val badgeCount = sections.sumOf { it.filters.size }
@ -471,27 +476,27 @@ private fun BadgePreviewDialog(
Text(
text = import.sourceUrl,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textSecondary,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
Text(
text = stringResource(Res.string.settings_fusion_badge_preview_count, badgeCount),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
)
if (sections.isEmpty()) {
Text(
text = stringResource(Res.string.settings_fusion_badge_preview_empty),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = tokens.colors.textMuted,
)
} else {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 460.dp),
verticalArrangement = Arrangement.spacedBy(14.dp),
verticalArrangement = Arrangement.spacedBy(tokens.spacing.railGap),
) {
items(
items = sections,
@ -499,18 +504,18 @@ private fun BadgePreviewDialog(
) { section ->
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap),
) {
Text(
text = section.title,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
fontWeight = FontWeight.SemiBold,
)
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
verticalArrangement = Arrangement.spacedBy(5.dp),
horizontalArrangement = Arrangement.spacedBy(NuvioTokens.Space.s5),
verticalArrangement = Arrangement.spacedBy(NuvioTokens.Space.s5),
) {
section.filters.forEach { filter ->
StreamBadgeChip(
@ -544,23 +549,24 @@ private fun SettingsDialogSurface(
title: String,
content: @Composable ColumnScope.() -> Unit,
) {
val tokens = MaterialTheme.nuvio
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
color = MaterialTheme.colorScheme.surface,
shape = tokens.shapes.dialog,
color = tokens.colors.surfaceDialog,
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.padding(tokens.spacing.dialogPadding),
verticalArrangement = Arrangement.spacedBy(tokens.spacing.listGap),
) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
color = tokens.colors.textPrimary,
fontWeight = FontWeight.SemiBold,
)
content()
Spacer(modifier = Modifier.height(2.dp))
Spacer(modifier = Modifier.height(NuvioTokens.Space.s2))
}
}
}

View file

@ -2,6 +2,7 @@ package com.nuvio.app.features.settings
import com.nuvio.app.core.ui.AppTheme
import com.nuvio.app.core.ui.NativeTabBridge
import com.nuvio.app.core.ui.ThemeColors
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -95,12 +96,5 @@ object ThemeSettingsRepository {
}
}
private fun AppTheme.nativeTabAccentHex(): String = when (this) {
AppTheme.CRIMSON -> "#E53935"
AppTheme.OCEAN -> "#1E88E5"
AppTheme.VIOLET -> "#8E24AA"
AppTheme.EMERALD -> "#43A047"
AppTheme.AMBER -> "#FB8C00"
AppTheme.ROSE -> "#D81B60"
AppTheme.WHITE -> "#F5F5F5"
}
private fun AppTheme.nativeTabAccentHex(): String =
ThemeColors.getColorPalette(this).nativeAccentHex

View file

@ -19,20 +19,21 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.nuvio.app.core.i18n.localizedByteUnit
import com.nuvio.app.core.ui.NuvioTokens
import com.nuvio.app.core.ui.nuvio
import kotlin.math.round
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.streams_size
import org.jetbrains.compose.resources.stringResource
internal object StreamBadgeChipDefaults {
val shape = RoundedCornerShape(6.dp)
val fileSizeHorizontalPadding = 6.dp
val fileSizeFontSize: TextUnit = 12.sp
val fileSizeLineHeight: TextUnit = 16.sp
val fileSizeLetterSpacing: TextUnit = 0.sp
val shape = RoundedCornerShape(NuvioTokens.Radius.sm)
val fileSizeHorizontalPadding = NuvioTokens.Space.s6
val fileSizeFontSize: TextUnit = NuvioTokens.Type.labelSm
val fileSizeLineHeight: TextUnit = NuvioTokens.LineHeight.bodySm
val fileSizeLetterSpacing: TextUnit = NuvioTokens.LetterSpacing.none
}
internal enum class StreamBadgeChipSize(
@ -83,7 +84,7 @@ internal fun StreamBadgeChip(
chipModifier = chipModifier.background(Color(backgroundColorArgb), shape)
}
if (outlineColorArgb != null) {
chipModifier = chipModifier.border(1.dp, Color(outlineColorArgb), shape)
chipModifier = chipModifier.border(NuvioTokens.Border.thin, Color(outlineColorArgb), shape)
}
Box(
@ -117,6 +118,7 @@ internal fun StreamBadgeImage(badge: StreamBadge) {
@Composable
internal fun StreamFileSizeBadge(stream: StreamItem) {
val tokens = MaterialTheme.nuvio
val bytes = stream.behaviorHints.videoSize ?: return
val gib = bytes.toDouble() / (1024.0 * 1024.0 * 1024.0)
val sizeLabel = if (gib >= 1.0) {
@ -132,8 +134,8 @@ internal fun StreamFileSizeBadge(stream: StreamItem) {
modifier = Modifier
.height(StreamBadgeChipSize.STREAM.containerHeight)
.clip(badgeShape)
.background(Color(0xFF0A0C0C))
.border(1.dp, Color(0xFF0A0C0C), badgeShape)
.background(tokens.colors.surfacePopover)
.border(tokens.borders.thin, tokens.colors.borderSubtle, badgeShape)
.padding(horizontal = StreamBadgeChipDefaults.fileSizeHorizontalPadding),
contentAlignment = Alignment.Center,
) {
@ -145,7 +147,7 @@ internal fun StreamFileSizeBadge(stream: StreamItem) {
fontWeight = FontWeight.Bold,
letterSpacing = StreamBadgeChipDefaults.fileSizeLetterSpacing,
),
color = Color.White,
color = tokens.colors.textPrimary,
)
}
}

View file

@ -3,7 +3,7 @@ package com.nuvio.app
import androidx.compose.ui.window.ComposeUIViewController
import platform.UIKit.UIColor
private val nuvioBackgroundColor = UIColor(red = 0.008, green = 0.016, blue = 0.016, alpha = 1.0)
private val nuvioBackgroundColor = UIColor(red = 0.051, green = 0.051, blue = 0.051, alpha = 1.0)
fun MainViewController() = ComposeUIViewController {
App()