feat: header gradiente, scrollable tabs, logout en perfil

- Dashboard: header con gradiente, avatar circular con inicial
- ServerDetail: ScrollableTabRow, header gradiente
- Notifications: header gradiente
- Profile: boton Sign Out que redirige a Login
- Bottom nav simplificado (3 items)
- Version 1.3.0
This commit is contained in:
2026-06-09 18:30:23 -04:00
parent e6717d0578
commit eaf3045d13
6 changed files with 142 additions and 137 deletions

View File

@@ -299,7 +299,14 @@ fun AppRoot() {
composable(Screen.Profile.route) {
ProfileScreen(
onBack = { navController.popBackStack() }
onBack = { navController.popBackStack() },
onLogout = {
loginViewModel.logout()
isLoggedIn = false
navController.navigate(Screen.Login.route) {
popUpTo(0) { inclusive = true }
}
}
)
}

View File

@@ -1,7 +1,9 @@
package com.devlab.app.ui.dashboard
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
@@ -9,7 +11,8 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -28,33 +31,63 @@ fun DashboardScreen(
viewModel: DashboardViewModel = viewModel()
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
val context = androidx.compose.ui.platform.LocalContext.current
val appVersion = remember {
try {
context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "1.2.0"
} catch (_: Exception) { "1.2.0" }
context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "1.3.0"
} catch (_: Exception) { "1.3.0" }
}
val initial = state.username.firstOrNull()?.uppercase() ?: "?"
Scaffold(
topBar = {
TopAppBar(
title = {
Column {
Text("Dashboard")
if (state.username.isNotBlank()) {
Box(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
Brush.verticalGradient(
colors = listOf(
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.primary.copy(alpha = 0.85f)
)
)
)
.padding(top = 48.dp, start = 16.dp, end = 16.dp, bottom = 24.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Surface(
modifier = Modifier.size(44.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.2f)
) {
Box(contentAlignment = Alignment.Center) {
Text(
text = "Welcome, ${state.username}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
text = initial,
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
}
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Dashboard",
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
if (state.username.isNotBlank()) {
Text(
text = "Welcome, ${state.username}",
color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f),
style = MaterialTheme.typography.bodyMedium
)
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.onPrimary
),
actions = {
IconButton(onClick = { onProfileClick() }) {
Icon(
Icons.Default.AccountCircle,
@@ -70,125 +103,50 @@ fun DashboardScreen(
)
}
}
)
}
) { padding ->
when {
state.isLoading -> {
LoadingIndicator(modifier = Modifier.padding(padding))
}
state.error != null -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
Icons.Default.ErrorOutline,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = state.error ?: "",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedButton(onClick = { viewModel.loadStats() }) {
Text("Retry")
when {
state.isLoading -> LoadingIndicator()
state.error != null -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(Icons.Default.ErrorOutline, contentDescription = null, modifier = Modifier.size(48.dp), tint = MaterialTheme.colorScheme.error)
Spacer(modifier = Modifier.height(16.dp))
Text(text = state.error ?: "", color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodyLarge)
Spacer(modifier = Modifier.height(16.dp))
OutlinedButton(onClick = { viewModel.loadStats() }) { Text("Retry") }
}
}
}
}
else -> {
val dashboardStats = state.stats
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
Text(
text = "ServerManager v$appVersion",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Servers",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
else -> {
val dashboardStats = state.stats
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
StatCard(
title = "Total",
value = "${dashboardStats?.stats?.total_servers ?: 0}",
color = MaterialTheme.colorScheme.primary,
icon = Icons.Default.Dns,
modifier = Modifier.weight(1f)
)
StatCard(
title = "Online",
value = "${dashboardStats?.stats?.online_servers ?: 0}",
color = Green500,
icon = Icons.Default.CheckCircle,
modifier = Modifier.weight(1f)
)
StatCard(
title = "Offline",
value = "${dashboardStats?.stats?.offline_servers ?: 0}",
color = Red500,
icon = Icons.Default.Cancel,
modifier = Modifier.weight(1f)
)
}
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Average Usage",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
StatCard(
title = "CPU",
value = "${dashboardStats?.stats?.avg_cpu?.toInt() ?: 0}%",
color = Orange500,
icon = Icons.Default.Memory,
modifier = Modifier.weight(1f)
)
StatCard(
title = "RAM",
value = "${dashboardStats?.stats?.avg_ram?.toInt() ?: 0}%",
color = Blue700,
icon = Icons.Default.Storage,
modifier = Modifier.weight(1f)
)
StatCard(
title = "Disk",
value = "${dashboardStats?.stats?.avg_disk?.toInt() ?: 0}%",
color = Green500,
icon = Icons.Default.SdStorage,
modifier = Modifier.weight(1f)
Text(
text = "ServerManager v$appVersion",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
Text(text = "Servers", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
Spacer(modifier = Modifier.height(12.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
StatCard(title = "Total", value = "${dashboardStats?.stats?.total_servers ?: 0}", color = MaterialTheme.colorScheme.primary, icon = Icons.Default.Dns, modifier = Modifier.weight(1f))
StatCard(title = "Online", value = "${dashboardStats?.stats?.online_servers ?: 0}", color = Green500, icon = Icons.Default.CheckCircle, modifier = Modifier.weight(1f))
StatCard(title = "Offline", value = "${dashboardStats?.stats?.offline_servers ?: 0}", color = Red500, icon = Icons.Default.Cancel, modifier = Modifier.weight(1f))
}
Spacer(modifier = Modifier.height(24.dp))
Text(text = "Average Usage", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
Spacer(modifier = Modifier.height(12.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
StatCard(title = "CPU", value = "${dashboardStats?.stats?.avg_cpu?.toInt() ?: 0}%", color = Orange500, icon = Icons.Default.Memory, modifier = Modifier.weight(1f))
StatCard(title = "RAM", value = "${dashboardStats?.stats?.avg_ram?.toInt() ?: 0}%", color = Blue700, icon = Icons.Default.Storage, modifier = Modifier.weight(1f))
StatCard(title = "Disk", value = "${dashboardStats?.stats?.avg_disk?.toInt() ?: 0}%", color = Green500, icon = Icons.Default.SdStorage, modifier = Modifier.weight(1f))
}
}
}
}

View File

@@ -4,6 +4,7 @@ import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.ui.graphics.Brush
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
@@ -69,8 +70,16 @@ fun NotificationsScreen(
}
}
},
modifier = Modifier.background(
androidx.compose.ui.graphics.Brush.horizontalGradient(
colors = listOf(
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.primary.copy(alpha = 0.85f)
)
)
),
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
containerColor = androidx.compose.ui.graphics.Color.Transparent,
titleContentColor = MaterialTheme.colorScheme.onPrimary,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary
)

View File

@@ -24,6 +24,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun ProfileScreen(
onBack: () -> Unit,
onLogout: () -> Unit = {},
viewModel: ProfileViewModel = viewModel()
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
@@ -299,6 +300,26 @@ fun ProfileScreen(
Text("Save Changes", fontWeight = FontWeight.SemiBold)
}
}
Spacer(modifier = Modifier.height(32.dp))
OutlinedButton(
onClick = onLogout,
modifier = Modifier.fillMaxWidth().height(50.dp),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.error
)
) {
Icon(
Icons.Default.Logout,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text("Sign Out", fontWeight = FontWeight.SemiBold)
}
Spacer(modifier = Modifier.height(24.dp))
}
}
}

View File

@@ -3,6 +3,7 @@ package com.devlab.app.ui.servers
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.ui.graphics.Brush
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
@@ -48,6 +49,14 @@ fun ServerDetailScreen(
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.onPrimary,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary
),
modifier = Modifier.background(
Brush.horizontalGradient(
colors = listOf(
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.primary.copy(alpha = 0.85f)
)
)
)
)
}
@@ -72,11 +81,12 @@ fun ServerDetailScreen(
}
else -> {
Column(modifier = Modifier.fillMaxSize().padding(padding)) {
TabRow(
ScrollableTabRow(
selectedTabIndex = state.selectedTab,
containerColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.primary,
divider = {}
divider = {},
edgePadding = 8.dp
) {
Tab(
selected = state.selectedTab == 0,

Binary file not shown.