Redesign: Permissions screen, battery optimization, location sheet (#415)

* update permissions screen design

* battery optimization screen

* remove init screen

* skip init screen on load

* skip on load only

* location sheet design wip

* denser location sheet

* location sheet layout fix
This commit is contained in:
callebtc
2025-09-14 04:22:56 +02:00
committed by GitHub
parent 861eaaeaef
commit 1a398b16ef
5 changed files with 659 additions and 482 deletions
@@ -25,6 +25,7 @@ import com.bitchat.android.onboarding.BluetoothCheckScreen
import com.bitchat.android.onboarding.BluetoothStatus import com.bitchat.android.onboarding.BluetoothStatus
import com.bitchat.android.onboarding.BluetoothStatusManager import com.bitchat.android.onboarding.BluetoothStatusManager
import com.bitchat.android.onboarding.BatteryOptimizationManager import com.bitchat.android.onboarding.BatteryOptimizationManager
import com.bitchat.android.onboarding.BatteryOptimizationPreferenceManager
import com.bitchat.android.onboarding.BatteryOptimizationScreen import com.bitchat.android.onboarding.BatteryOptimizationScreen
import com.bitchat.android.onboarding.BatteryOptimizationStatus import com.bitchat.android.onboarding.BatteryOptimizationStatus
import com.bitchat.android.onboarding.InitializationErrorScreen import com.bitchat.android.onboarding.InitializationErrorScreen
@@ -163,7 +164,7 @@ class MainActivity : ComponentActivity() {
} }
when (onboardingState) { when (onboardingState) {
OnboardingState.CHECKING -> { OnboardingState.PERMISSION_REQUESTING -> {
InitializingScreen(modifier) InitializingScreen(modifier)
} }
@@ -227,15 +228,7 @@ class MainActivity : ComponentActivity() {
) )
} }
OnboardingState.PERMISSION_REQUESTING -> { OnboardingState.CHECKING, OnboardingState.INITIALIZING, OnboardingState.COMPLETE -> {
InitializingScreen(modifier)
}
OnboardingState.INITIALIZING -> {
InitializingScreen(modifier)
}
OnboardingState.COMPLETE -> {
// Set up back navigation handling for the chat screen // Set up back navigation handling for the chat screen
val backCallback = object : OnBackPressedCallback(true) { val backCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
@@ -533,6 +526,13 @@ class MainActivity : ComponentActivity() {
return return
} }
// Check if user has previously skipped battery optimization
if (BatteryOptimizationPreferenceManager.isSkipped(this)) {
android.util.Log.d("MainActivity", "User previously skipped battery optimization, proceeding to permissions")
proceedWithPermissionCheck()
return
}
// For existing users, check battery optimization status // For existing users, check battery optimization status
batteryOptimizationManager.logBatteryOptimizationStatus() batteryOptimizationManager.logBatteryOptimizationStatus()
val currentBatteryOptimizationStatus = when { val currentBatteryOptimizationStatus = when {
@@ -0,0 +1,33 @@
package com.bitchat.android.onboarding
import android.content.Context
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* Preference manager for battery optimization skip choice
*/
object BatteryOptimizationPreferenceManager {
private const val PREFS_NAME = "bitchat_settings"
private const val KEY_BATTERY_SKIP = "battery_optimization_skipped"
private val _skipFlow = MutableStateFlow(false)
val skipFlow: StateFlow<Boolean> = _skipFlow
fun init(context: Context) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val skipped = prefs.getBoolean(KEY_BATTERY_SKIP, false)
_skipFlow.value = skipped
}
fun setSkipped(context: Context, skipped: Boolean) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit().putBoolean(KEY_BATTERY_SKIP, skipped).apply()
_skipFlow.value = skipped
}
fun isSkipped(context: Context): Boolean {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getBoolean(KEY_BATTERY_SKIP, false)
}
}
@@ -3,6 +3,7 @@ package com.bitchat.android.onboarding
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
@@ -12,11 +13,13 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.bitchat.android.R import com.bitchat.android.R
/** /**
@@ -32,10 +35,16 @@ fun BatteryOptimizationScreen(
onSkip: () -> Unit, onSkip: () -> Unit,
isLoading: Boolean = false isLoading: Boolean = false
) { ) {
val context = LocalContext.current
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
// Initialize preference manager
LaunchedEffect(Unit) {
BatteryOptimizationPreferenceManager.init(context)
}
Box( Box(
modifier = modifier.padding(32.dp), modifier = modifier.padding(24.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
when (status) { when (status) {
@@ -73,6 +82,8 @@ private fun BatteryOptimizationEnabledContent(
colorScheme: ColorScheme, colorScheme: ColorScheme,
isLoading: Boolean isLoading: Boolean
) { ) {
val context = LocalContext.current
Column( Column(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
@@ -82,78 +93,112 @@ private fun BatteryOptimizationEnabledContent(
.weight(1f) .weight(1f)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(bottom = 16.dp), .padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( // Header Section - matching AboutSheet style
text = "bitchat", Column(
style = MaterialTheme.typography.headlineLarge.copy( modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "bitchat",
style = MaterialTheme.typography.headlineLarge.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
fontSize = 32.sp
),
color = colorScheme.onBackground
)
Text(
text = "battery optimization detected",
fontSize = 12.sp,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold, color = colorScheme.onBackground.copy(alpha = 0.7f)
color = colorScheme.primary
) )
) }
Spacer(modifier = Modifier.height(16.dp)) // Battery optimization info section
Surface(
Icon(
imageVector = Icons.Outlined.BatteryAlert,
contentDescription = "Battery Optimization",
modifier = Modifier.size(64.dp),
tint = colorScheme.error
)
Text(
text = stringResource(R.string.battery_optimization_detected),
style = MaterialTheme.typography.headlineSmall.copy(
fontWeight = FontWeight.SemiBold,
color = colorScheme.onSurface
),
textAlign = TextAlign.Center
)
Text(
text = stringResource(R.string.battery_optimization_explanation),
style = MaterialTheme.typography.bodyLarge.copy(
color = colorScheme.onSurfaceVariant
),
textAlign = TextAlign.Center
)
Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( color = colorScheme.surfaceVariant.copy(alpha = 0.25f),
containerColor = colorScheme.surfaceVariant.copy(alpha = 0.5f) shape = RoundedCornerShape(12.dp)
)
) { ) {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Text( Row(
text = stringResource(R.string.battery_optimization_why_disable), verticalAlignment = Alignment.Top,
style = MaterialTheme.typography.titleSmall.copy( horizontalArrangement = Arrangement.spacedBy(12.dp)
fontWeight = FontWeight.SemiBold, ) {
color = colorScheme.onSurface Icon(
imageVector = Icons.Filled.Power,
contentDescription = "Battery Optimization",
tint = colorScheme.primary,
modifier = Modifier
.padding(top = 2.dp)
.size(20.dp)
) )
) Column {
Text(
Text( text = "Battery Optimization Enabled",
text = stringResource(R.string.battery_optimization_benefits), style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.bodyMedium.copy( fontWeight = FontWeight.Medium,
color = colorScheme.onSurfaceVariant color = colorScheme.onBackground
) )
) Spacer(modifier = Modifier.height(4.dp))
Text(
text = "bitchat needs to run in the background to maintain mesh connections. battery optimization can interrupt these connections.",
style = MaterialTheme.typography.bodySmall,
color = colorScheme.onBackground.copy(alpha = 0.8f)
)
}
}
} }
} }
Text( // Benefits section
text = stringResource(R.string.battery_optimization_note), Surface(
style = MaterialTheme.typography.bodySmall.copy( modifier = Modifier.fillMaxWidth(),
color = colorScheme.onSurfaceVariant color = colorScheme.surfaceVariant.copy(alpha = 0.25f),
), shape = RoundedCornerShape(12.dp)
textAlign = TextAlign.Center ) {
) Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = Icons.Filled.CheckCircle,
contentDescription = "Benefits",
tint = colorScheme.primary,
modifier = Modifier
.padding(top = 2.dp)
.size(20.dp)
)
Column {
Text(
text = "Benefits of Disabling",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = colorScheme.onBackground
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "• reliable message delivery\n• maintains mesh connectivity\n• prevents connection drops",
style = MaterialTheme.typography.bodySmall,
color = colorScheme.onBackground.copy(alpha = 0.8f)
)
}
}
}
}
} }
// Fixed buttons at the bottom // Fixed buttons at the bottom
@@ -164,7 +209,10 @@ private fun BatteryOptimizationEnabledContent(
Button( Button(
onClick = onDisableBatteryOptimization, onClick = onDisableBatteryOptimization,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled = !isLoading enabled = !isLoading,
colors = ButtonDefaults.buttonColors(
containerColor = colorScheme.primary
)
) { ) {
if (isLoading) { if (isLoading) {
CircularProgressIndicator( CircularProgressIndicator(
@@ -174,7 +222,13 @@ private fun BatteryOptimizationEnabledContent(
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
} }
Text(stringResource(R.string.battery_optimization_disable_button)) Text(
text = "Disable Battery Optimization",
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold
)
)
} }
Row( Row(
@@ -186,15 +240,28 @@ private fun BatteryOptimizationEnabledContent(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
enabled = !isLoading enabled = !isLoading
) { ) {
Text(stringResource(R.string.battery_optimization_check_again)) Text(
text = "Check Again",
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace
)
)
} }
TextButton( TextButton(
onClick = onSkip, onClick = {
BatteryOptimizationPreferenceManager.setSkipped(context, true)
onSkip()
},
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
enabled = !isLoading enabled = !isLoading
) { ) {
Text(stringResource(R.string.battery_optimization_skip)) Text(
text = "Skip for Now",
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace
)
)
} }
} }
} }
@@ -206,17 +273,31 @@ private fun BatteryOptimizationCheckingContent(
colorScheme: ColorScheme colorScheme: ColorScheme
) { ) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(32.dp), verticalArrangement = Arrangement.spacedBy(24.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( // Header Section - matching AboutSheet style
text = "bitchat", Column(
style = MaterialTheme.typography.headlineLarge.copy( verticalArrangement = Arrangement.spacedBy(8.dp),
fontFamily = FontFamily.Monospace, horizontalAlignment = Alignment.CenterHorizontally
fontWeight = FontWeight.Bold, ) {
color = colorScheme.primary Text(
text = "bitchat",
style = MaterialTheme.typography.headlineLarge.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
fontSize = 32.sp
),
color = colorScheme.onBackground
) )
)
Text(
text = "battery optimization disabled",
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onBackground.copy(alpha = 0.7f)
)
}
val infiniteTransition = rememberInfiniteTransition(label = "rotation") val infiniteTransition = rememberInfiniteTransition(label = "rotation")
val rotation by infiniteTransition.animateFloat( val rotation by infiniteTransition.animateFloat(
@@ -239,18 +320,10 @@ private fun BatteryOptimizationCheckingContent(
) )
Text( Text(
text = stringResource(R.string.battery_optimization_disabled), text = "bitchat can run reliably in the background",
style = MaterialTheme.typography.headlineSmall.copy( style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = FontWeight.SemiBold, fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface color = colorScheme.onBackground.copy(alpha = 0.8f)
),
textAlign = TextAlign.Center
)
Text(
text = stringResource(R.string.battery_optimization_success_message),
style = MaterialTheme.typography.bodyLarge.copy(
color = colorScheme.onSurfaceVariant
), ),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
@@ -266,14 +339,28 @@ private fun BatteryOptimizationNotSupportedContent(
verticalArrangement = Arrangement.spacedBy(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( // Header Section - matching AboutSheet style
text = "bitchat", Column(
style = MaterialTheme.typography.headlineLarge.copy( verticalArrangement = Arrangement.spacedBy(8.dp),
fontFamily = FontFamily.Monospace, horizontalAlignment = Alignment.CenterHorizontally
fontWeight = FontWeight.Bold, ) {
color = colorScheme.primary Text(
text = "bitchat",
style = MaterialTheme.typography.headlineLarge.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
fontSize = 32.sp
),
color = colorScheme.onBackground
) )
)
Text(
text = "battery optimization not required",
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onBackground.copy(alpha = 0.7f)
)
}
Icon( Icon(
imageVector = Icons.Filled.CheckCircle, imageVector = Icons.Filled.CheckCircle,
@@ -283,27 +370,28 @@ private fun BatteryOptimizationNotSupportedContent(
) )
Text( Text(
text = stringResource(R.string.battery_optimization_not_required), text = "your device doesn't require battery optimization settings. bitchat will run normally.",
style = MaterialTheme.typography.headlineSmall.copy( style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = FontWeight.SemiBold, fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface color = colorScheme.onBackground.copy(alpha = 0.8f)
),
textAlign = TextAlign.Center
)
Text(
text = stringResource(R.string.battery_optimization_not_supported_message),
style = MaterialTheme.typography.bodyLarge.copy(
color = colorScheme.onSurfaceVariant
), ),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Button( Button(
onClick = onRetry, onClick = onRetry,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = colorScheme.primary
)
) { ) {
Text(stringResource(R.string.battery_optimization_continue)) Text(
text = "Continue",
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold
)
)
} }
} }
} }
@@ -2,12 +2,22 @@ package com.bitchat.android.onboarding
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bluetooth
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Power
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -40,86 +50,87 @@ fun PermissionExplanationScreen(
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
// Header
// Header Section - matching AboutSheet style
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
horizontalAlignment = Alignment.CenterHorizontally .fillMaxWidth()
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Text( Row(
text = "Welcome to bitchat", horizontalArrangement = Arrangement.spacedBy(8.dp),
style = MaterialTheme.typography.headlineMedium.copy( verticalAlignment = Alignment.Bottom,
fontFamily = FontFamily.Monospace, modifier = Modifier.fillMaxWidth()
fontWeight = FontWeight.Bold, ) {
color = colorScheme.primary Text(
), text = "bitchat",
textAlign = TextAlign.Center style = MaterialTheme.typography.headlineLarge.copy(
) fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
Spacer(modifier = Modifier.height(8.dp)) fontSize = 32.sp
),
color = colorScheme.onBackground
)
}
Text( Text(
text = "Decentralized mesh messaging over Bluetooth", text = "decentralized mesh messaging with end-to-end encryption",
style = MaterialTheme.typography.bodyMedium.copy( fontSize = 12.sp,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.7f) color = colorScheme.onBackground.copy(alpha = 0.7f)
),
textAlign = TextAlign.Center
) )
} }
Spacer(modifier = Modifier.height(16.dp)) // Privacy assurance section - matching AboutSheet card style
Surface(
// Privacy assurance section
Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( color = colorScheme.surfaceVariant.copy(alpha = 0.25f),
containerColor = colorScheme.surfaceVariant.copy(alpha = 0.3f) shape = RoundedCornerShape(12.dp)
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) { ) {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
Text( Icon(
text = "🔒", imageVector = Icons.Filled.Security,
style = MaterialTheme.typography.titleMedium, contentDescription = "Privacy Protected",
modifier = Modifier.size(20.dp) tint = colorScheme.primary,
modifier = Modifier
.padding(top = 2.dp)
.size(20.dp)
) )
Text( Column {
text = "Your Privacy is Protected", Text(
style = MaterialTheme.typography.titleSmall.copy( text = "Your Privacy is Protected",
fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium,
color = colorScheme.onSurface fontWeight = FontWeight.Medium,
color = colorScheme.onBackground
) )
) Spacer(modifier = Modifier.height(4.dp))
Text(
text = "• no tracking or data collection\n" +
"• Bluetooth mesh chats are fully offline\n" +
"• Geohash chats use the internet",
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
color = colorScheme.onBackground.copy(alpha = 0.8f)
)
}
} }
Text(
text = "• bitchat doesn't track you or collect personal data\n" +
"• Bluetooth mesh chats are fully offline and require no internet\n" +
"• Geohash chats use the internet but your location is generalized\n" +
"• Your messages stay on your device and peer devices only",
style = MaterialTheme.typography.bodySmall.copy(
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.8f)
)
)
} }
} }
Spacer(modifier = Modifier.height(8.dp)) // Section header
Text( Text(
text = "To work properly, bitchat needs these permissions:", text = "permissions",
style = MaterialTheme.typography.bodyMedium.copy( style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Medium, color = colorScheme.onBackground.copy(alpha = 0.7f),
color = colorScheme.onSurface modifier = Modifier.padding(top = 8.dp, bottom = 8.dp)
)
) )
// Permission categories // Permission categories
@@ -168,55 +179,46 @@ private fun PermissionCategoryCard(
category: PermissionCategory, category: PermissionCategory,
colorScheme: ColorScheme colorScheme: ColorScheme
) { ) {
Card( Row(
modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top,
colors = CardDefaults.cardColors( modifier = Modifier
containerColor = colorScheme.surface .fillMaxWidth()
), .padding(vertical = 8.dp)
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) { ) {
Column( Icon(
modifier = Modifier.padding(16.dp), imageVector = getPermissionIcon(category.type),
verticalArrangement = Arrangement.spacedBy(8.dp) contentDescription = category.type.nameValue,
) { tint = colorScheme.primary,
Row( modifier = Modifier
verticalAlignment = Alignment.CenterVertically, .padding(top = 2.dp)
horizontalArrangement = Arrangement.spacedBy(12.dp) .size(20.dp)
) { )
Text( Spacer(modifier = Modifier.width(16.dp))
text = getPermissionEmoji(category.type), Column {
style = MaterialTheme.typography.titleLarge, Text(
color = getPermissionIconColor(category.type), text = category.type.nameValue,
modifier = Modifier.size(24.dp) style = MaterialTheme.typography.titleMedium,
) fontWeight = FontWeight.Medium,
color = colorScheme.onBackground
Text( )
text = category.type.nameValue, Spacer(modifier = Modifier.height(4.dp))
style = MaterialTheme.typography.titleSmall.copy(
fontWeight = FontWeight.Bold,
color = colorScheme.onSurface
)
)
}
Text( Text(
text = category.description, text = category.description,
style = MaterialTheme.typography.bodySmall.copy( style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace, color = colorScheme.onBackground.copy(alpha = 0.8f)
color = colorScheme.onSurface.copy(alpha = 0.8f),
lineHeight = 18.sp
)
) )
if (category.type == PermissionType.PRECISE_LOCATION) { if (category.type == PermissionType.PRECISE_LOCATION) {
// Extra emphasis for location permission // Extra emphasis for location permission
Spacer(modifier = Modifier.height(4.dp))
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp) horizontalArrangement = Arrangement.spacedBy(6.dp)
) { ) {
Text( Icon(
text = "⚠️", imageVector = Icons.Filled.Warning,
style = MaterialTheme.typography.bodyMedium, contentDescription = "Warning",
tint = Color(0xFFFF9800),
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
Text( Text(
@@ -233,22 +235,12 @@ private fun PermissionCategoryCard(
} }
} }
private fun getPermissionEmoji(permissionType: PermissionType): String { private fun getPermissionIcon(permissionType: PermissionType): ImageVector {
return when (permissionType) { return when (permissionType) {
PermissionType.NEARBY_DEVICES -> "📱" PermissionType.NEARBY_DEVICES -> Icons.Filled.Bluetooth
PermissionType.PRECISE_LOCATION -> "📍" PermissionType.PRECISE_LOCATION -> Icons.Filled.LocationOn
PermissionType.NOTIFICATIONS -> "🔔" PermissionType.NOTIFICATIONS -> Icons.Filled.Notifications
PermissionType.BATTERY_OPTIMIZATION -> "🔋" PermissionType.BATTERY_OPTIMIZATION -> Icons.Filled.Power
PermissionType.OTHER -> "🔧" PermissionType.OTHER -> Icons.Filled.Settings
}
}
private fun getPermissionIconColor(permissionType: PermissionType): Color {
return when (permissionType) {
PermissionType.NEARBY_DEVICES -> Color(0xFF2196F3) // Blue
PermissionType.PRECISE_LOCATION -> Color(0xFFFF9800) // Orange
PermissionType.NOTIFICATIONS -> Color(0xFF4CAF50) // Green
PermissionType.BATTERY_OPTIMIZATION -> Color(0xFFF44336) // Red
PermissionType.OTHER -> Color(0xFF9C27B0) // Purple
} }
} }
@@ -3,6 +3,8 @@ package com.bitchat.android.ui
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.provider.Settings import android.provider.Settings
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -73,12 +75,21 @@ fun LocationChannelsSheet(
// Bottom sheet state // Bottom sheet state
val sheetState = rememberModalBottomSheetState( val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = isInputFocused skipPartiallyExpanded = true
) )
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
// Scroll state for LazyColumn // Scroll state for LazyColumn with animated top bar
val listState = rememberLazyListState() val listState = rememberLazyListState()
val isScrolled by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0
}
}
val topBarAlpha by animateFloatAsState(
targetValue = if (isScrolled) 0.95f else 0f,
label = "topBarAlpha"
)
val mapPickerLauncher = rememberLauncherForActivityResult( val mapPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult() contract = ActivityResultContracts.StartActivityForResult()
@@ -100,114 +111,125 @@ fun LocationChannelsSheet(
if (isPresented) { if (isPresented) {
ModalBottomSheet( ModalBottomSheet(
modifier = modifier.statusBarsPadding(),
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
sheetState = sheetState, sheetState = sheetState,
modifier = modifier containerColor = MaterialTheme.colorScheme.background,
dragHandle = null
) { ) {
Column( Box(modifier = Modifier.fillMaxWidth()) {
modifier = Modifier LazyColumn(
.fillMaxWidth() state = listState,
.then( modifier = Modifier.fillMaxSize(),
if (isInputFocused) { contentPadding = PaddingValues(top = 48.dp, bottom = 16.dp)
Modifier.fillMaxHeight().padding(horizontal = 16.dp, vertical = 24.dp) ) {
} else { // Header Section
Modifier.padding(horizontal = 16.dp, vertical = 12.dp) item(key = "header") {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(bottom = 8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "#location channels",
style = MaterialTheme.typography.headlineSmall,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground
)
Text(
text = "chat with people near you using geohash channels. only a coarse geohash is shared, never exact gps. do not screenshot or share this screen to protect your privacy.",
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f)
)
} }
), }
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Header
Text(
text = "#location channels",
fontSize = 18.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface
)
Text( // Permission controls if services enabled
text = "chat with people near you using geohash channels. only a coarse geohash is shared, never exact gps. do not screenshot or share this screen to protect your privacy.", if (locationServicesEnabled) {
fontSize = 12.sp, item(key = "permissions") {
fontFamily = FontFamily.Monospace, Column(
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) modifier = Modifier
) .fillMaxWidth()
.padding(horizontal = 24.dp)
// Permission controls if services enabled .padding(bottom = 8.dp),
if (locationServicesEnabled) { verticalArrangement = Arrangement.spacedBy(4.dp)
when (permissionState) {
LocationChannelManager.PermissionState.NOT_DETERMINED -> {
Button(
onClick = { locationManager.enableLocationChannels() },
colors = ButtonDefaults.buttonColors(
containerColor = standardGreen.copy(alpha = 0.12f),
contentColor = standardGreen
),
modifier = Modifier.fillMaxWidth()
) { ) {
Text( when (permissionState) {
text = "grant location permission", LocationChannelManager.PermissionState.NOT_DETERMINED -> {
fontSize = 12.sp, Button(
fontFamily = FontFamily.Monospace onClick = { locationManager.enableLocationChannels() },
) colors = ButtonDefaults.buttonColors(
} containerColor = standardGreen.copy(alpha = 0.12f),
} contentColor = standardGreen
LocationChannelManager.PermissionState.DENIED, ),
LocationChannelManager.PermissionState.RESTRICTED -> { modifier = Modifier.fillMaxWidth()
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { ) {
Text( Text(
text = "location permission denied. enable in settings to use location channels.", text = "grant location permission",
fontSize = 11.sp, fontSize = 12.sp,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace
color = Color.Red.copy(alpha = 0.8f) )
) }
TextButton( }
onClick = { LocationChannelManager.PermissionState.DENIED,
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { LocationChannelManager.PermissionState.RESTRICTED -> {
data = Uri.fromParts("package", context.packageName, null) Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
text = "location permission denied. enable in settings to use location channels.",
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = Color.Red.copy(alpha = 0.8f)
)
TextButton(
onClick = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)
}
) {
Text(
text = "open settings",
fontSize = 11.sp,
fontFamily = FontFamily.Monospace
)
}
}
}
LocationChannelManager.PermissionState.AUTHORIZED -> {
Text(
text = "✓ location permission granted",
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = standardGreen
)
}
null -> {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
CircularProgressIndicator(modifier = Modifier.size(12.dp))
Text(
text = "checking permissions...",
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
} }
context.startActivity(intent)
} }
) {
Text(
text = "open settings",
fontSize = 11.sp,
fontFamily = FontFamily.Monospace
)
} }
} }
} }
LocationChannelManager.PermissionState.AUTHORIZED -> {
Text(
text = "✓ location permission granted",
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = standardGreen
)
}
null -> {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
CircularProgressIndicator(modifier = Modifier.size(12.dp))
Text(
text = "checking permissions...",
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
}
}
} }
}
// Channel list (iOS-style plain list)
LazyColumn(
state = listState,
modifier = Modifier.weight(1f)
) {
// Mesh option first // Mesh option first
item { item(key = "mesh") {
ChannelRow( ChannelRow(
title = meshTitleWithCount(viewModel), title = meshTitleWithCount(viewModel),
subtitle = "#bluetooth • ${bluetoothRangeString()}", subtitle = "#bluetooth • ${bluetoothRangeString()}",
@@ -259,7 +281,7 @@ fun LocationChannelsSheet(
} else if (permissionState == LocationChannelManager.PermissionState.AUTHORIZED && locationServicesEnabled) { } else if (permissionState == LocationChannelManager.PermissionState.AUTHORIZED && locationServicesEnabled) {
item { item {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
CircularProgressIndicator(modifier = Modifier.size(16.dp)) CircularProgressIndicator(modifier = Modifier.size(16.dp))
@@ -274,13 +296,16 @@ fun LocationChannelsSheet(
// Bookmarked geohashes // Bookmarked geohashes
if (bookmarks.isNotEmpty()) { if (bookmarks.isNotEmpty()) {
item { item(key = "bookmarked_header") {
Text( Text(
text = "bookmarked", text = "bookmarked",
fontSize = 12.sp, style = MaterialTheme.typography.labelLarge,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f),
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp) modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(top = 8.dp, bottom = 4.dp)
) )
} }
items(bookmarks) { gh -> items(bookmarks) { gh ->
@@ -325,182 +350,219 @@ fun LocationChannelsSheet(
} }
// Custom geohash teleport (iOS-style inline form) // Custom geohash teleport (iOS-style inline form)
item { item(key = "custom_geohash") {
Surface( Surface(
color = Color.Transparent,
shape = MaterialTheme.shapes.medium,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 6.dp), .padding(horizontal = 24.dp, vertical = 2.dp)
color = Color.Transparent
) { ) {
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { Row(
Row( modifier = Modifier
horizontalArrangement = Arrangement.spacedBy(1.dp), .fillMaxWidth()
verticalAlignment = Alignment.CenterVertically .padding(horizontal = 16.dp, vertical = 6.dp),
) { horizontalArrangement = Arrangement.spacedBy(8.dp),
Text( verticalAlignment = Alignment.CenterVertically
text = "#", ) {
Text(
text = "#",
fontSize = BASE_FONT_SIZE.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
BasicTextField(
value = customGeohash,
onValueChange = { newValue ->
// iOS-style geohash validation (base32 characters only)
val allowed = "0123456789bcdefghjkmnpqrstuvwxyz".toSet()
val filtered = newValue
.lowercase()
.replace("#", "")
.filter { it in allowed }
.take(12)
customGeohash = filtered
customError = null
},
textStyle = androidx.compose.ui.text.TextStyle(
fontSize = BASE_FONT_SIZE.sp, fontSize = BASE_FONT_SIZE.sp,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) color = MaterialTheme.colorScheme.onSurface
) ),
modifier = Modifier
BasicTextField( .weight(1f)
value = customGeohash, .onFocusChanged { focusState ->
onValueChange = { newValue -> isInputFocused = focusState.isFocused
// iOS-style geohash validation (base32 characters only) if (focusState.isFocused) {
val allowed = "0123456789bcdefghjkmnpqrstuvwxyz".toSet() coroutineScope.launch {
val filtered = newValue sheetState.expand()
.lowercase() // Scroll to bottom to show input and remove button
.replace("#", "") listState.animateScrollToItem(
.filter { it in allowed } index = listState.layoutInfo.totalItemsCount - 1
.take(12) )
customGeohash = filtered
customError = null
},
textStyle = androidx.compose.ui.text.TextStyle(
fontSize = BASE_FONT_SIZE.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface
),
modifier = Modifier
.weight(1f)
.onFocusChanged { focusState ->
isInputFocused = focusState.isFocused
if (focusState.isFocused) {
coroutineScope.launch {
sheetState.expand()
// Scroll to bottom to show input and remove button
listState.animateScrollToItem(
index = listState.layoutInfo.totalItemsCount - 1
)
}
} }
},
singleLine = true,
decorationBox = { innerTextField ->
if (customGeohash.isEmpty()) {
Text(
text = "geohash",
fontSize = BASE_FONT_SIZE.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
)
}
innerTextField()
}
)
val normalized = customGeohash.trim().lowercase().replace("#", "")
// Map picker button
IconButton(onClick = {
val initial = when {
normalized.isNotBlank() -> normalized
selectedChannel is ChannelID.Location -> (selectedChannel as ChannelID.Location).channel.geohash
else -> ""
}
val intent = Intent(context, GeohashPickerActivity::class.java).apply {
putExtra(GeohashPickerActivity.EXTRA_INITIAL_GEOHASH, initial)
}
mapPickerLauncher.launch(intent)
}) {
Icon(
imageVector = Icons.Filled.Map,
contentDescription = "Open map",
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
)
}
val isValid = validateGeohash(normalized)
// iOS-style teleport button
Button(
onClick = {
if (isValid) {
val level = levelForLength(normalized.length)
val channel = GeohashChannel(level = level, geohash = normalized)
// Mark this selection as a manual teleport
locationManager.setTeleported(true)
locationManager.select(ChannelID.Location(channel))
onDismiss()
} else {
customError = "invalid geohash"
} }
}, },
enabled = isValid, singleLine = true,
colors = ButtonDefaults.buttonColors( decorationBox = { innerTextField ->
containerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.12f), if (customGeohash.isEmpty()) {
contentColor = MaterialTheme.colorScheme.onSurface
)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text( Text(
text = "teleport", text = "geohash",
fontSize = BASE_FONT_SIZE.sp, fontSize = BASE_FONT_SIZE.sp,
fontFamily = FontFamily.Monospace fontFamily = FontFamily.Monospace,
) color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
// iOS has a face.dashed icon, use closest Material equivalent
Icon(
imageVector = Icons.Filled.PinDrop,
contentDescription = "Teleport",
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.onSurface
) )
} }
innerTextField()
} }
)
val normalized = customGeohash.trim().lowercase().replace("#", "")
// Map picker button
IconButton(onClick = {
val initial = when {
normalized.isNotBlank() -> normalized
selectedChannel is ChannelID.Location -> (selectedChannel as ChannelID.Location).channel.geohash
else -> ""
}
val intent = Intent(context, GeohashPickerActivity::class.java).apply {
putExtra(GeohashPickerActivity.EXTRA_INITIAL_GEOHASH, initial)
}
mapPickerLauncher.launch(intent)
}) {
Icon(
imageVector = Icons.Filled.Map,
contentDescription = "Open map",
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
)
} }
customError?.let { error -> val isValid = validateGeohash(normalized)
Text(
text = error, // iOS-style teleport button
fontSize = 12.sp, Button(
fontFamily = FontFamily.Monospace, onClick = {
color = Color.Red if (isValid) {
val level = levelForLength(normalized.length)
val channel = GeohashChannel(level = level, geohash = normalized)
// Mark this selection as a manual teleport
locationManager.setTeleported(true)
locationManager.select(ChannelID.Location(channel))
onDismiss()
} else {
customError = "invalid geohash"
}
},
enabled = isValid,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.12f),
contentColor = MaterialTheme.colorScheme.onSurface
) )
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "teleport",
fontSize = BASE_FONT_SIZE.sp,
fontFamily = FontFamily.Monospace
)
// iOS has a face.dashed icon, use closest Material equivalent
Icon(
imageVector = Icons.Filled.PinDrop,
contentDescription = "Teleport",
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.onSurface
)
}
} }
} }
} }
} }
// Location services toggle button // Error message for custom geohash
item { if (customError != null) {
Button( item(key = "geohash_error") {
onClick = {
if (locationServicesEnabled) {
locationManager.disableLocationServices()
} else {
locationManager.enableLocationServices()
}
},
colors = ButtonDefaults.buttonColors(
containerColor = if (locationServicesEnabled) {
Color.Red.copy(alpha = 0.08f)
} else {
standardGreen.copy(alpha = 0.12f)
},
contentColor = if (locationServicesEnabled) {
Color(0xFFBF1A1A)
} else {
standardGreen
}
),
modifier = Modifier.fillMaxWidth()
) {
Text( Text(
text = if (locationServicesEnabled) { text = customError!!,
"disable location services"
} else {
"enable location services"
},
fontSize = 12.sp, fontSize = 12.sp,
fontFamily = FontFamily.Monospace fontFamily = FontFamily.Monospace,
color = Color.Red,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
) )
} }
} }
// Location services toggle button
item(key = "location_toggle") {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(top = 8.dp)
) {
Button(
onClick = {
if (locationServicesEnabled) {
locationManager.disableLocationServices()
} else {
locationManager.enableLocationServices()
}
},
colors = ButtonDefaults.buttonColors(
containerColor = if (locationServicesEnabled) {
Color.Red.copy(alpha = 0.08f)
} else {
standardGreen.copy(alpha = 0.12f)
},
contentColor = if (locationServicesEnabled) {
Color(0xFFBF1A1A)
} else {
standardGreen
}
),
modifier = Modifier.fillMaxWidth()
) {
Text(
text = if (locationServicesEnabled) {
"disable location services"
} else {
"enable location services"
},
fontSize = 12.sp,
fontFamily = FontFamily.Monospace
)
}
}
}
}
// TopBar (animated)
Box(
modifier = Modifier
.align(Alignment.TopCenter)
.fillMaxWidth()
.height(56.dp)
.background(MaterialTheme.colorScheme.background.copy(alpha = topBarAlpha))
) {
TextButton(
onClick = onDismiss,
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(horizontal = 16.dp)
) {
Text(
text = "Close",
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold),
color = MaterialTheme.colorScheme.onBackground
)
}
} }
} }
} }
@@ -557,18 +619,20 @@ private fun ChannelRow(
Color.Transparent Color.Transparent
}, },
shape = MaterialTheme.shapes.medium, shape = MaterialTheme.shapes.medium,
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 2.dp)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = 16.dp, vertical = 6.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column( Column(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp) verticalArrangement = Arrangement.spacedBy(2.dp)
) { ) {
// Split title to handle count part with smaller font (iOS style) // Split title to handle count part with smaller font (iOS style)
val (baseTitle, countSuffix) = splitTitleAndCount(title) val (baseTitle, countSuffix) = splitTitleAndCount(title)