feat(ui): дизайн-система radiOLA — палитра, тема, типографика, бренд, motion, pill таб-бар
- цветовые токены тёмно-зелёной темы + RadiolaColors (CompositionLocal) - darkColorScheme + всегда тёмная тема, фирменные shapes - типографика с весами/размерами под макет - Brand: AppMark (градиентный R), RadiolaWordmark, MonoMark - Motion: спеки движения, pressScale, живой эквалайзер - pill-таб-бар с анимированной активной вкладкой
This commit is contained in:
@@ -1,22 +1,63 @@
|
||||
package com.radiola.ui.navigation
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandHorizontally
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkHorizontally
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import com.radiola.ui.theme.Motion
|
||||
import com.radiola.ui.theme.RadiolaTheme
|
||||
|
||||
@Composable
|
||||
fun BottomNavBar(navController: NavController) {
|
||||
val colors = RadiolaTheme.colors
|
||||
val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
|
||||
NavigationBar {
|
||||
NavDestinations.items.filter { it.showInBottomBar }.forEach { destination ->
|
||||
NavigationBarItem(
|
||||
icon = { Icon(destination.icon, contentDescription = destination.labelRes) },
|
||||
label = { Text(destination.labelRes) },
|
||||
selected = currentRoute == destination.route,
|
||||
val items = NavDestinations.items.filter { it.showInBottomBar }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 16.dp)
|
||||
.clip(RoundedCornerShape(36.dp))
|
||||
.background(colors.surface2)
|
||||
.border(1.dp, colors.border, RoundedCornerShape(36.dp))
|
||||
.height(62.dp)
|
||||
.padding(6.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
items.forEach { destination ->
|
||||
val selected = currentRoute == destination.route
|
||||
PillTab(
|
||||
label = destination.labelRes,
|
||||
icon = destination.icon,
|
||||
selected = selected,
|
||||
modifier = Modifier.weight(if (selected) 1.9f else 1f),
|
||||
onClick = {
|
||||
if (currentRoute != destination.route) {
|
||||
navController.navigate(destination.route) {
|
||||
@@ -30,3 +71,60 @@ fun BottomNavBar(navController: NavController) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PillTab(
|
||||
label: String,
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val colors = RadiolaTheme.colors
|
||||
val bg by animateColorAsState(
|
||||
targetValue = if (selected) colors.accent else androidx.compose.ui.graphics.Color.Transparent,
|
||||
animationSpec = tween(Motion.Medium),
|
||||
label = "tabBg"
|
||||
)
|
||||
val content by animateColorAsState(
|
||||
targetValue = if (selected) colors.bgBase else colors.textSecondary,
|
||||
animationSpec = tween(Motion.Medium),
|
||||
label = "tabFg"
|
||||
)
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp)
|
||||
.clip(RoundedCornerShape(26.dp))
|
||||
.background(bg)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = onClick
|
||||
),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = label,
|
||||
tint = content,
|
||||
modifier = Modifier.height(18.dp).width(18.dp)
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = selected,
|
||||
enter = fadeIn(tween(Motion.Medium)) + expandHorizontally(tween(Motion.Medium)),
|
||||
exit = fadeOut(tween(Motion.Fast)) + shrinkHorizontally(tween(Motion.Fast))
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = label.uppercase(),
|
||||
color = content,
|
||||
style = androidx.compose.material3.MaterialTheme.typography.labelSmall,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
95
app/src/main/java/com/radiola/ui/theme/Brand.kt
Normal file
95
app/src/main/java/com/radiola/ui/theme/Brand.kt
Normal file
@@ -0,0 +1,95 @@
|
||||
package com.radiola.ui.theme
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.material3.Text
|
||||
|
||||
/** Брендовый градиент иконки. */
|
||||
fun brandGradient(): Brush = Brush.linearGradient(listOf(BrandGradientStart, BrandGradientEnd))
|
||||
|
||||
/**
|
||||
* Иконка-марка приложения: градиентный squircle с монограммой «R».
|
||||
*/
|
||||
@Composable
|
||||
fun AppMark(
|
||||
size: Dp = 76.dp,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val radius = (size.value * 0.29f).dp
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(size)
|
||||
.clip(RoundedCornerShape(radius))
|
||||
.background(brandGradient()),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "R",
|
||||
color = BgBase,
|
||||
fontWeight = FontWeight.Black,
|
||||
fontSize = (size.value * 0.62f).sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Текстовый логотип «radiOLA»: «radi» основным цветом, «OLA» акцентом.
|
||||
*/
|
||||
@Composable
|
||||
fun RadiolaWordmark(
|
||||
fontSize: Int = 26,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "radi",
|
||||
color = TextPrimary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = fontSize.sp
|
||||
)
|
||||
Text(
|
||||
text = "OLA",
|
||||
color = Accent,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = fontSize.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Монохромная марка для нейтральных подложек (уведомления, виджет). */
|
||||
@Composable
|
||||
fun MonoMark(
|
||||
size: Dp = 24.dp,
|
||||
color: Color = TextPrimary,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(size)
|
||||
.clip(RoundedCornerShape((size.value * 0.27f).dp))
|
||||
.border(1.5.dp, color, RoundedCornerShape((size.value * 0.27f).dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "R",
|
||||
color = color,
|
||||
fontWeight = FontWeight.Black,
|
||||
fontSize = (size.value * 0.6f).sp
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,23 @@ package com.radiola.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Primary = Color(0xFF6200EE)
|
||||
val PrimaryDark = Color(0xFF3700B3)
|
||||
val Secondary = Color(0xFF03DAC6)
|
||||
val Background = Color(0xFF121212)
|
||||
val Surface = Color(0xFF1E1E1E)
|
||||
val OnPrimary = Color.White
|
||||
val OnSecondary = Color.Black
|
||||
val OnBackground = Color.White
|
||||
val OnSurface = Color.White
|
||||
// Базовая палитра radiOLA (тёмно-зелёная тема)
|
||||
val BgBase = Color(0xFF0C1410)
|
||||
val BgSurface = Color(0xFF16201A)
|
||||
val BgSurface2 = Color(0xFF1E2A23)
|
||||
val BgElevated = Color(0xFF243029)
|
||||
|
||||
val Accent = Color(0xFFA8E05F)
|
||||
val AccentDim = Color(0xFF6FA53C)
|
||||
|
||||
val TextPrimary = Color(0xFFFFFFFF)
|
||||
val TextSecondary = Color(0xFF8FA396)
|
||||
val TextMuted = Color(0xFF5C6E63)
|
||||
|
||||
val BorderColor = Color(0xFF2A352E)
|
||||
val LiveRed = Color(0xFFFF5252)
|
||||
val LiveRedSoft = Color(0xFFFF6B6B)
|
||||
|
||||
// Брендовый градиент (иконка приложения, акцентные элементы)
|
||||
val BrandGradientStart = Color(0xFFC2F25B)
|
||||
val BrandGradientEnd = Color(0xFF6FA53C)
|
||||
|
||||
102
app/src/main/java/com/radiola/ui/theme/Motion.kt
Normal file
102
app/src/main/java/com/radiola/ui/theme/Motion.kt
Normal file
@@ -0,0 +1,102 @@
|
||||
package com.radiola.ui.theme
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.abs
|
||||
|
||||
/** Стандартные длительности и кривые движения приложения. */
|
||||
object Motion {
|
||||
const val Fast = 120
|
||||
const val Medium = 220
|
||||
const val Slow = 360
|
||||
|
||||
fun <T> snappy() = spring<T>(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessMediumLow
|
||||
)
|
||||
|
||||
fun <T> gentle() = tween<T>(durationMillis = Medium, easing = FastOutSlowInEasing)
|
||||
}
|
||||
|
||||
/** Лёгкое нажатие: плавное уменьшение масштаба, пока палец на элементе. */
|
||||
@Composable
|
||||
fun Modifier.pressScale(
|
||||
pressedScale: Float = 0.94f,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
|
||||
): Modifier {
|
||||
val pressed by interactionSource.collectIsPressedAsState()
|
||||
val scale by androidx.compose.animation.core.animateFloatAsState(
|
||||
targetValue = if (pressed) pressedScale else 1f,
|
||||
animationSpec = tween(Motion.Fast, easing = FastOutSlowInEasing),
|
||||
label = "pressScale"
|
||||
)
|
||||
return this.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Живой эквалайзер для прямого эфира — декоративный, без перемотки.
|
||||
* Полоски плавно «дышат» при воспроизведении.
|
||||
*/
|
||||
@Composable
|
||||
fun LiveEqualizer(
|
||||
modifier: Modifier = Modifier,
|
||||
barCount: Int = 36,
|
||||
color: Color = Accent,
|
||||
playing: Boolean = true
|
||||
) {
|
||||
val transition = rememberInfiniteTransition(label = "eq")
|
||||
val phase by transition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = (Math.PI * 2).toFloat(),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(1400, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart
|
||||
),
|
||||
label = "eqPhase"
|
||||
)
|
||||
Canvas(modifier = modifier) {
|
||||
val gap = 3.dp.toPx()
|
||||
val barWidth = (size.width - gap * (barCount - 1)) / barCount
|
||||
val maxH = size.height
|
||||
for (i in 0 until barCount) {
|
||||
val seed = (i * 0.7f)
|
||||
val wave = if (playing) {
|
||||
0.45f + 0.55f * abs(kotlin.math.sin(phase + seed))
|
||||
} else 0.25f
|
||||
val h = maxH * wave
|
||||
val x = i * (barWidth + gap)
|
||||
val y = (maxH - h) / 2f
|
||||
drawRoundRect(
|
||||
color = color,
|
||||
topLeft = Offset(x, y),
|
||||
size = Size(barWidth, h),
|
||||
cornerRadius = CornerRadius(barWidth / 2f, barWidth / 2f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
13
app/src/main/java/com/radiola/ui/theme/Shape.kt
Normal file
13
app/src/main/java/com/radiola/ui/theme/Shape.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.radiola.ui.theme
|
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Shapes
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val RadiolaShapes = Shapes(
|
||||
extraSmall = RoundedCornerShape(8.dp),
|
||||
small = RoundedCornerShape(12.dp),
|
||||
medium = RoundedCornerShape(16.dp),
|
||||
large = RoundedCornerShape(20.dp),
|
||||
extraLarge = RoundedCornerShape(28.dp)
|
||||
)
|
||||
@@ -1,38 +1,69 @@
|
||||
package com.radiola.ui.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Расширенные токены, которых нет в Material ColorScheme.
|
||||
* Доступ через MaterialTheme-стиль: `RadiolaTheme.colors`.
|
||||
*/
|
||||
data class RadiolaColors(
|
||||
val bgBase: Color = BgBase,
|
||||
val surface: Color = BgSurface,
|
||||
val surface2: Color = BgSurface2,
|
||||
val elevated: Color = BgElevated,
|
||||
val accent: Color = Accent,
|
||||
val accentDim: Color = AccentDim,
|
||||
val textPrimary: Color = TextPrimary,
|
||||
val textSecondary: Color = TextSecondary,
|
||||
val textMuted: Color = TextMuted,
|
||||
val border: Color = BorderColor,
|
||||
val live: Color = LiveRed,
|
||||
) {
|
||||
val brandGradient: Brush
|
||||
get() = Brush.linearGradient(listOf(BrandGradientStart, BrandGradientEnd))
|
||||
}
|
||||
|
||||
val LocalRadiolaColors = staticCompositionLocalOf { RadiolaColors() }
|
||||
|
||||
object RadiolaTheme {
|
||||
val colors: RadiolaColors
|
||||
@Composable get() = LocalRadiolaColors.current
|
||||
}
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Primary,
|
||||
secondary = Secondary,
|
||||
background = Background,
|
||||
surface = Surface,
|
||||
onPrimary = OnPrimary,
|
||||
onSecondary = OnSecondary,
|
||||
onBackground = OnBackground,
|
||||
onSurface = OnSurface
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Primary,
|
||||
secondary = Secondary,
|
||||
onPrimary = OnPrimary,
|
||||
onSecondary = OnSecondary
|
||||
primary = Accent,
|
||||
onPrimary = BgBase,
|
||||
secondary = AccentDim,
|
||||
onSecondary = BgBase,
|
||||
background = BgBase,
|
||||
onBackground = TextPrimary,
|
||||
surface = BgSurface,
|
||||
onSurface = TextPrimary,
|
||||
surfaceVariant = BgSurface2,
|
||||
onSurfaceVariant = TextSecondary,
|
||||
outline = BorderColor,
|
||||
outlineVariant = BorderColor,
|
||||
error = LiveRed,
|
||||
onError = BgBase,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun RadiolaTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
|
||||
// Приложение всегда в тёмной фирменной теме.
|
||||
CompositionLocalProvider(LocalRadiolaColors provides RadiolaColors()) {
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
colorScheme = DarkColorScheme,
|
||||
typography = Typography,
|
||||
shapes = RadiolaShapes,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,30 +6,55 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Системный гротеск; брендовый wordmark рисуется отдельно (Brand.kt).
|
||||
private val AppFont = FontFamily.Default
|
||||
|
||||
val Typography = Typography(
|
||||
headlineLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontFamily = AppFont,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 28.sp
|
||||
fontSize = 30.sp,
|
||||
letterSpacing = (-0.5).sp
|
||||
),
|
||||
headlineMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 22.sp
|
||||
fontFamily = AppFont,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 26.sp
|
||||
),
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = AppFont,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = AppFont,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 16.sp
|
||||
),
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = AppFont,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 15.sp
|
||||
),
|
||||
bodyMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontFamily = AppFont,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp
|
||||
),
|
||||
labelLarge = TextStyle(
|
||||
fontFamily = AppFont,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 13.sp
|
||||
),
|
||||
labelMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontFamily = AppFont,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 12.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = AppFont,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 10.sp,
|
||||
letterSpacing = 1.sp
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user