feat(android): UI overhaul — branded theme, onboarding, chat-style home

- Replace default purple Material 3 theme with logo-derived palette
  (crimson red primary, dark charcoal surfaces, golden accent)
- Add Instrument Serif + Inter fonts for custom typography scale
- Add first-launch onboarding flow (API key + server URL, permissions)
- Restructure navigation: 2-tab bottom nav (Home + Settings), logs
  via top bar icon, permission status indicators in top bar
- Redesign HomeScreen as chat-style interface with goal/step bubbles,
  auto-scroll, bottom input bar with send/stop buttons
- Redesign SettingsScreen with Server → Connection → Permissions sections
- Redesign LogsScreen with goal banner, step badges, timestamps
- Replace default Android icon with DroidClaw logo at all densities
- Add hasOnboarded DataStore flag with auto-connect on completion
- Fix logs navigation not clearing when switching bottom tabs
This commit is contained in:
Somasundaram Mahesh
2026-02-18 22:09:32 +05:30
parent 59ee665088
commit 4199143de8
41 changed files with 1381 additions and 473 deletions

View File

@@ -10,29 +10,40 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.History import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.thisux.droidclaw.ui.components.PermissionStatusBar
import com.thisux.droidclaw.ui.screens.HomeScreen import com.thisux.droidclaw.ui.screens.HomeScreen
import com.thisux.droidclaw.ui.screens.LogsScreen import com.thisux.droidclaw.ui.screens.LogsScreen
import com.thisux.droidclaw.ui.screens.OnboardingScreen
import com.thisux.droidclaw.ui.screens.SettingsScreen import com.thisux.droidclaw.ui.screens.SettingsScreen
import com.thisux.droidclaw.ui.theme.DroidClawTheme import com.thisux.droidclaw.ui.theme.DroidClawTheme
import com.thisux.droidclaw.ui.theme.InstrumentSerif
sealed class Screen(val route: String, val label: String) { sealed class Screen(val route: String, val label: String) {
data object Home : Screen("home", "Home") data object Home : Screen("home", "Home")
data object Settings : Screen("settings", "Settings") data object Settings : Screen("settings", "Settings")
data object Logs : Screen("logs", "Logs") data object Logs : Screen("logs", "Logs")
data object Onboarding : Screen("onboarding", "Onboarding")
} }
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@@ -47,51 +58,116 @@ class MainActivity : ComponentActivity() {
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MainNavigation() { fun MainNavigation() {
val context = LocalContext.current
val app = context.applicationContext as DroidClawApp
val hasOnboarded by app.settingsStore.hasOnboarded.collectAsState(initial = true)
val navController = rememberNavController() val navController = rememberNavController()
val screens = listOf(Screen.Home, Screen.Settings, Screen.Logs) val bottomNavScreens = listOf(Screen.Home, Screen.Settings)
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
val showChrome = currentRoute != Screen.Onboarding.route
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
bottomBar = { topBar = {
NavigationBar { if (showChrome) {
val navBackStackEntry by navController.currentBackStackEntryAsState() CenterAlignedTopAppBar(
val currentDestination = navBackStackEntry?.destination title = {
Text(
screens.forEach { screen -> text = "DroidClaw",
NavigationBarItem( style = MaterialTheme.typography.titleLarge.copy(
icon = { fontFamily = InstrumentSerif
Icon(
when (screen) {
is Screen.Home -> Icons.Filled.Home
is Screen.Settings -> Icons.Filled.Settings
is Screen.Logs -> Icons.Filled.History
},
contentDescription = screen.label
) )
}, )
label = { Text(screen.label) }, },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true, actions = {
onClick = { PermissionStatusBar(
navController.navigate(screen.route) { onNavigateToSettings = {
navController.navigate(Screen.Settings.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
IconButton(onClick = {
navController.navigate(Screen.Logs.route) {
popUpTo(navController.graph.findStartDestination().id) { popUpTo(navController.graph.findStartDestination().id) {
saveState = true saveState = true
} }
launchSingleTop = true launchSingleTop = true
restoreState = true
} }
}) {
Icon(
Icons.Filled.History,
contentDescription = "Logs"
)
} }
},
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
titleContentColor = MaterialTheme.colorScheme.onSurface
) )
)
}
},
bottomBar = {
if (showChrome) {
NavigationBar {
val currentDestination = navBackStackEntry?.destination
bottomNavScreens.forEach { screen ->
NavigationBarItem(
icon = {
Icon(
when (screen) {
is Screen.Home -> Icons.Filled.Home
is Screen.Settings -> Icons.Filled.Settings
else -> Icons.Filled.Home
},
contentDescription = screen.label
)
},
label = { Text(screen.label) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
} }
} }
} }
) { innerPadding -> ) { innerPadding ->
val startDestination = if (hasOnboarded) Screen.Home.route else Screen.Onboarding.route
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = Screen.Home.route, startDestination = startDestination,
modifier = Modifier.padding(innerPadding) modifier = Modifier.padding(innerPadding)
) { ) {
composable(Screen.Onboarding.route) {
OnboardingScreen(
onComplete = {
navController.navigate(Screen.Home.route) {
popUpTo(Screen.Onboarding.route) { inclusive = true }
}
}
)
}
composable(Screen.Home.route) { HomeScreen() } composable(Screen.Home.route) { HomeScreen() }
composable(Screen.Settings.route) { SettingsScreen() } composable(Screen.Settings.route) { SettingsScreen() }
composable(Screen.Logs.route) { LogsScreen() } composable(Screen.Logs.route) { LogsScreen() }

View File

@@ -17,6 +17,7 @@ object SettingsKeys {
val SERVER_URL = stringPreferencesKey("server_url") val SERVER_URL = stringPreferencesKey("server_url")
val DEVICE_NAME = stringPreferencesKey("device_name") val DEVICE_NAME = stringPreferencesKey("device_name")
val AUTO_CONNECT = booleanPreferencesKey("auto_connect") val AUTO_CONNECT = booleanPreferencesKey("auto_connect")
val HAS_ONBOARDED = booleanPreferencesKey("has_onboarded")
} }
class SettingsStore(private val context: Context) { class SettingsStore(private val context: Context) {
@@ -52,4 +53,12 @@ class SettingsStore(private val context: Context) {
suspend fun setAutoConnect(value: Boolean) { suspend fun setAutoConnect(value: Boolean) {
context.dataStore.edit { it[SettingsKeys.AUTO_CONNECT] = value } context.dataStore.edit { it[SettingsKeys.AUTO_CONNECT] = value }
} }
val hasOnboarded: Flow<Boolean> = context.dataStore.data.map { prefs ->
prefs[SettingsKeys.HAS_ONBOARDED] ?: false
}
suspend fun setHasOnboarded(value: Boolean) {
context.dataStore.edit { it[SettingsKeys.HAS_ONBOARDED] = value }
}
} }

View File

@@ -0,0 +1,112 @@
package com.thisux.droidclaw.ui.components
import android.app.Activity
import android.content.Context
import android.media.projection.MediaProjectionManager
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.thisux.droidclaw.DroidClawApp
import com.thisux.droidclaw.accessibility.DroidClawAccessibilityService
import com.thisux.droidclaw.capture.ScreenCaptureManager
import com.thisux.droidclaw.ui.theme.StatusGreen
import com.thisux.droidclaw.ui.theme.StatusRed
import com.thisux.droidclaw.util.BatteryOptimization
@Composable
fun PermissionStatusBar(onNavigateToSettings: () -> Unit) {
val context = LocalContext.current
val app = context.applicationContext as DroidClawApp
val apiKey by app.settingsStore.apiKey.collectAsState(initial = "")
val isCaptureAvailable by ScreenCaptureManager.isAvailable.collectAsState()
var isAccessibilityEnabled by remember {
mutableStateOf(DroidClawAccessibilityService.isEnabledOnDevice(context))
}
var hasCaptureConsent by remember {
ScreenCaptureManager.restoreConsent(context)
mutableStateOf(isCaptureAvailable || ScreenCaptureManager.hasConsent())
}
var isBatteryExempt by remember {
mutableStateOf(BatteryOptimization.isIgnoringBatteryOptimizations(context))
}
var hasOverlayPermission by remember {
mutableStateOf(Settings.canDrawOverlays(context))
}
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
isAccessibilityEnabled = DroidClawAccessibilityService.isEnabledOnDevice(context)
ScreenCaptureManager.restoreConsent(context)
hasCaptureConsent = isCaptureAvailable || ScreenCaptureManager.hasConsent()
isBatteryExempt = BatteryOptimization.isIgnoringBatteryOptimizations(context)
hasOverlayPermission = Settings.canDrawOverlays(context)
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
val allOk = apiKey.isNotBlank() && isAccessibilityEnabled && hasCaptureConsent
&& isBatteryExempt && hasOverlayPermission
if (allOk) {
Icon(
imageVector = Icons.Filled.CheckCircle,
contentDescription = "All permissions OK",
tint = StatusGreen,
modifier = Modifier
.size(24.dp)
.clickable { onNavigateToSettings() }
)
} else {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable { onNavigateToSettings() }
) {
if (apiKey.isBlank()) StatusDot(StatusRed)
if (!isAccessibilityEnabled) StatusDot(StatusRed)
if (!hasCaptureConsent) StatusDot(StatusRed)
if (!isBatteryExempt) StatusDot(StatusRed)
if (!hasOverlayPermission) StatusDot(StatusRed)
}
}
}
@Composable
private fun StatusDot(color: androidx.compose.ui.graphics.Color) {
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(color)
)
}

View File

@@ -12,15 +12,27 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size 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.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.Stop
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -31,10 +43,24 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.thisux.droidclaw.connection.ConnectionService import com.thisux.droidclaw.connection.ConnectionService
import com.thisux.droidclaw.model.AgentStep
import com.thisux.droidclaw.model.ConnectionState import com.thisux.droidclaw.model.ConnectionState
import com.thisux.droidclaw.model.GoalStatus import com.thisux.droidclaw.model.GoalStatus
import com.thisux.droidclaw.ui.theme.StatusGreen
import com.thisux.droidclaw.ui.theme.StatusRed
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
// Represents a message in the chat timeline
private sealed class ChatItem {
data class GoalMessage(val text: String) : ChatItem()
data class StepMessage(val step: AgentStep) : ChatItem()
data class StatusMessage(val status: GoalStatus, val stepCount: Int) : ChatItem()
}
@Composable @Composable
fun HomeScreen() { fun HomeScreen() {
@@ -43,155 +69,304 @@ fun HomeScreen() {
val goalStatus by ConnectionService.currentGoalStatus.collectAsState() val goalStatus by ConnectionService.currentGoalStatus.collectAsState()
val steps by ConnectionService.currentSteps.collectAsState() val steps by ConnectionService.currentSteps.collectAsState()
val currentGoal by ConnectionService.currentGoal.collectAsState() val currentGoal by ConnectionService.currentGoal.collectAsState()
val errorMessage by ConnectionService.errorMessage.collectAsState()
var goalInput by remember { mutableStateOf("") } var goalInput by remember { mutableStateOf("") }
Column( // Build chat items: goal bubble → step bubbles → status bubble
modifier = Modifier val chatItems = remember(currentGoal, steps, goalStatus) {
.fillMaxSize() buildList {
.padding(16.dp) if (currentGoal.isNotEmpty()) {
) { add(ChatItem.GoalMessage(currentGoal))
// Status Badge }
Row( steps.forEach { add(ChatItem.StepMessage(it)) }
verticalAlignment = Alignment.CenterVertically, if (goalStatus == GoalStatus.Running) {
modifier = Modifier.fillMaxWidth() add(ChatItem.StatusMessage(GoalStatus.Running, steps.size))
) { } else if (goalStatus == GoalStatus.Completed || goalStatus == GoalStatus.Failed) {
add(ChatItem.StatusMessage(goalStatus, steps.size))
}
}
}
val listState = rememberLazyListState()
// Auto-scroll to bottom when new items arrive
LaunchedEffect(chatItems.size) {
if (chatItems.isNotEmpty()) {
listState.animateScrollToItem(chatItems.lastIndex)
}
}
Column(modifier = Modifier.fillMaxSize()) {
// Chat area
if (chatItems.isEmpty()) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(12.dp) .weight(1f)
.clip(CircleShape) .fillMaxWidth(),
.background( contentAlignment = Alignment.Center
when (connectionState) {
ConnectionState.Connected -> Color(0xFF4CAF50)
ConnectionState.Connecting -> Color(0xFFFFC107)
ConnectionState.Error -> Color(0xFFF44336)
ConnectionState.Disconnected -> Color.Gray
}
)
)
Text(
text = when (connectionState) {
ConnectionState.Connected -> "Connected to server"
ConnectionState.Connecting -> "Connecting..."
ConnectionState.Error -> errorMessage ?: "Connection error"
ConnectionState.Disconnected -> "Disconnected"
},
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 8.dp)
)
}
Spacer(modifier = Modifier.height(8.dp))
// Connect/Disconnect button
Button(
onClick = {
val intent = Intent(context, ConnectionService::class.java).apply {
action = if (connectionState == ConnectionState.Disconnected || connectionState == ConnectionState.Error) {
ConnectionService.ACTION_CONNECT
} else {
ConnectionService.ACTION_DISCONNECT
}
}
context.startForegroundService(intent)
},
modifier = Modifier.fillMaxWidth()
) {
Text(
when (connectionState) {
ConnectionState.Disconnected, ConnectionState.Error -> "Connect"
else -> "Disconnect"
}
)
}
Spacer(modifier = Modifier.height(16.dp))
// Goal Input
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = goalInput,
onValueChange = { goalInput = it },
label = { Text("Enter a goal...") },
modifier = Modifier.weight(1f),
enabled = connectionState == ConnectionState.Connected && goalStatus != GoalStatus.Running,
singleLine = true
)
Button(
onClick = {
if (goalInput.isNotBlank()) {
val intent = Intent(context, ConnectionService::class.java).apply {
action = ConnectionService.ACTION_SEND_GOAL
putExtra(ConnectionService.EXTRA_GOAL, goalInput)
}
context.startService(intent)
goalInput = ""
}
},
enabled = connectionState == ConnectionState.Connected
&& goalStatus != GoalStatus.Running
&& goalInput.isNotBlank()
) { ) {
Text("Send") Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "What should I do?",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Send a goal to start the agent",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.35f)
)
}
} }
} } else {
LazyColumn(
if (currentGoal.isNotEmpty()) { state = listState,
Spacer(modifier = Modifier.height(8.dp)) modifier = Modifier
Text( .weight(1f)
text = "Goal: $currentGoal", .fillMaxWidth()
style = MaterialTheme.typography.titleSmall, .padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.primary verticalArrangement = Arrangement.spacedBy(8.dp),
) contentPadding = androidx.compose.foundation.layout.PaddingValues(vertical = 12.dp)
} ) {
items(chatItems, key = { item ->
Spacer(modifier = Modifier.height(16.dp)) when (item) {
is ChatItem.GoalMessage -> "goal_${item.text}"
// Step Log is ChatItem.StepMessage -> "step_${item.step.step}"
LazyColumn( is ChatItem.StatusMessage -> "status_${item.status}"
modifier = Modifier.weight(1f), }
verticalArrangement = Arrangement.spacedBy(8.dp) }) { item ->
) { when (item) {
items(steps) { step -> is ChatItem.GoalMessage -> GoalBubble(item.text)
Card(modifier = Modifier.fillMaxWidth()) { is ChatItem.StepMessage -> AgentBubble(item.step)
Column(modifier = Modifier.padding(12.dp)) { is ChatItem.StatusMessage -> StatusBubble(item.status, item.stepCount)
Text(
text = "Step ${step.step}: ${step.action}",
style = MaterialTheme.typography.titleSmall
)
if (step.reasoning.isNotEmpty()) {
Text(
text = step.reasoning,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} }
} }
} }
} }
// Goal Status // Input bar pinned at bottom
if (goalStatus == GoalStatus.Completed || goalStatus == GoalStatus.Failed) { InputBar(
Spacer(modifier = Modifier.height(8.dp)) value = goalInput,
Text( onValueChange = { goalInput = it },
text = if (goalStatus == GoalStatus.Completed) { onSend = {
"Goal completed (${steps.size} steps)" if (goalInput.isNotBlank()) {
} else { val intent = Intent(context, ConnectionService::class.java).apply {
"Goal failed" action = ConnectionService.ACTION_SEND_GOAL
}, putExtra(ConnectionService.EXTRA_GOAL, goalInput)
style = MaterialTheme.typography.titleMedium, }
color = if (goalStatus == GoalStatus.Completed) { context.startService(intent)
Color(0xFF4CAF50) goalInput = ""
} else {
MaterialTheme.colorScheme.error
} }
) },
} onStop = { ConnectionService.instance?.stopGoal() },
canSend = connectionState == ConnectionState.Connected
&& goalStatus != GoalStatus.Running
&& goalInput.isNotBlank(),
isRunning = goalStatus == GoalStatus.Running,
isConnected = connectionState == ConnectionState.Connected
)
} }
} }
/** User's goal — right-aligned bubble */
@Composable
private fun GoalBubble(text: String) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
Box(
modifier = Modifier
.widthIn(max = 300.dp)
.clip(RoundedCornerShape(18.dp, 18.dp, 4.dp, 18.dp))
.background(MaterialTheme.colorScheme.primary)
.padding(horizontal = 14.dp, vertical = 10.dp)
) {
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimary
)
}
}
}
/** Agent step — left-aligned bubble */
@Composable
private fun AgentBubble(step: AgentStep) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start
) {
// Step number badge
Box(
modifier = Modifier
.padding(top = 4.dp)
.size(24.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center
) {
Text(
text = "${step.step}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier = Modifier
.widthIn(max = 280.dp)
.clip(RoundedCornerShape(4.dp, 18.dp, 18.dp, 18.dp))
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f))
.padding(horizontal = 14.dp, vertical = 10.dp)
) {
Column {
Text(
text = step.action,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface
)
if (step.reasoning.isNotEmpty()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = step.reasoning,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
Text(
text = formatTime(step.timestamp),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
modifier = Modifier.padding(top = 2.dp)
)
}
}
}
}
/** Status indicator — centered */
@Composable
private fun StatusBubble(status: GoalStatus, stepCount: Int) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
val (text, color) = when (status) {
GoalStatus.Running -> "Thinking..." to MaterialTheme.colorScheme.onSurfaceVariant
GoalStatus.Completed -> "Done — $stepCount steps" to StatusGreen
GoalStatus.Failed -> "Failed" to StatusRed
GoalStatus.Idle -> "" to Color.Transparent
}
Row(
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(color.copy(alpha = 0.1f))
.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
if (status == GoalStatus.Running) {
CircularProgressIndicator(
modifier = Modifier.size(12.dp),
strokeWidth = 1.5.dp,
color = color
)
}
Text(
text = text,
style = MaterialTheme.typography.labelMedium,
color = color
)
}
}
}
/** Bottom input bar */
@Composable
private fun InputBar(
value: String,
onValueChange: (String) -> Unit,
onSend: () -> Unit,
onStop: () -> Unit,
canSend: Boolean,
isRunning: Boolean,
isConnected: Boolean
) {
Surface(
tonalElevation = 2.dp,
color = MaterialTheme.colorScheme.surface
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TextField(
value = value,
onValueChange = onValueChange,
placeholder = {
Text(
if (!isConnected) "Not connected"
else if (isRunning) "Agent is working..."
else "Enter a goal...",
style = MaterialTheme.typography.bodyMedium
)
},
modifier = Modifier.weight(1f),
enabled = isConnected && !isRunning,
singleLine = true,
shape = RoundedCornerShape(24.dp),
colors = TextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f),
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.1f),
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent
)
)
if (isRunning) {
IconButton(
onClick = onStop,
colors = IconButtonDefaults.iconButtonColors(
containerColor = StatusRed.copy(alpha = 0.15f)
)
) {
Icon(
Icons.Filled.Stop,
contentDescription = "Stop",
tint = StatusRed
)
}
} else {
IconButton(
onClick = onSend,
enabled = canSend,
colors = IconButtonDefaults.iconButtonColors(
containerColor = if (canSend) MaterialTheme.colorScheme.primary else Color.Transparent
)
) {
Icon(
Icons.AutoMirrored.Filled.Send,
contentDescription = "Send",
tint = if (canSend) MaterialTheme.colorScheme.onPrimary
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f)
)
}
}
}
}
}
private fun formatTime(timestamp: Long): String {
val sdf = SimpleDateFormat("HH:mm", Locale.getDefault())
return sdf.format(Date(timestamp))
}

View File

@@ -1,28 +1,41 @@
package com.thisux.droidclaw.ui.screens package com.thisux.droidclaw.ui.screens
import androidx.compose.foundation.clickable import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Alignment
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.thisux.droidclaw.connection.ConnectionService import com.thisux.droidclaw.connection.ConnectionService
import com.thisux.droidclaw.model.GoalStatus import com.thisux.droidclaw.model.GoalStatus
import com.thisux.droidclaw.ui.theme.StatusAmber
import com.thisux.droidclaw.ui.theme.StatusGreen
import com.thisux.droidclaw.ui.theme.StatusRed
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Composable @Composable
fun LogsScreen() { fun LogsScreen() {
@@ -33,68 +46,108 @@ fun LogsScreen() {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp) .padding(horizontal = 20.dp)
) { ) {
Text("Logs", style = MaterialTheme.typography.headlineMedium) // Goal banner
if (currentGoal.isNotEmpty()) { if (currentGoal.isNotEmpty()) {
Row( Card(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth() shape = RoundedCornerShape(12.dp),
.padding(vertical = 8.dp), colors = CardDefaults.cardColors(
horizontalArrangement = Arrangement.SpaceBetween containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
)
) { ) {
Text( Row(
text = currentGoal, modifier = Modifier
style = MaterialTheme.typography.titleSmall .fillMaxWidth()
) .padding(16.dp),
Text( verticalAlignment = Alignment.CenterVertically,
text = when (goalStatus) { horizontalArrangement = Arrangement.SpaceBetween
GoalStatus.Running -> "Running" ) {
GoalStatus.Completed -> "Completed" Column(modifier = Modifier.weight(1f)) {
GoalStatus.Failed -> "Failed" Text(
GoalStatus.Idle -> "Idle" text = currentGoal,
}, style = MaterialTheme.typography.titleSmall
color = when (goalStatus) { )
GoalStatus.Running -> Color(0xFFFFC107) if (steps.isNotEmpty()) {
GoalStatus.Completed -> Color(0xFF4CAF50) Text(
GoalStatus.Failed -> MaterialTheme.colorScheme.error text = "Step ${steps.size}",
GoalStatus.Idle -> Color.Gray style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} }
) Spacer(modifier = Modifier.width(8.dp))
StatusBadge(goalStatus)
}
} }
Spacer(modifier = Modifier.height(12.dp))
} }
if (steps.isEmpty()) { if (steps.isEmpty()) {
Text( Box(
text = "No steps recorded yet. Submit a goal to see agent activity here.", modifier = Modifier
style = MaterialTheme.typography.bodyMedium, .weight(1f)
color = MaterialTheme.colorScheme.onSurfaceVariant, .fillMaxWidth(),
modifier = Modifier.padding(top = 16.dp) contentAlignment = Alignment.Center
) ) {
Text(
text = "No steps recorded yet.\nSubmit a goal to see agent activity here.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
items(steps) { step -> items(steps) { step ->
var expanded by remember { mutableStateOf(false) }
Card( Card(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth() shape = RoundedCornerShape(12.dp),
.clickable { expanded = !expanded } colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f)
)
) { ) {
Column(modifier = Modifier.padding(12.dp)) { Column(modifier = Modifier.padding(14.dp)) {
Text( Row(
text = "Step ${step.step}: ${step.action}", verticalAlignment = Alignment.CenterVertically
style = MaterialTheme.typography.titleSmall ) {
) // Step number badge
if (expanded && step.reasoning.isNotEmpty()) { Box(
modifier = Modifier
.size(28.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.Center
) {
Text(
text = "${step.step}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onPrimary
)
}
Spacer(modifier = Modifier.width(10.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = step.action,
style = MaterialTheme.typography.titleSmall
)
Text(
text = formatTimestamp(step.timestamp),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
if (step.reasoning.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = step.reasoning, text = step.reasoning,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant
modifier = Modifier.padding(top = 4.dp)
) )
} }
} }
@@ -104,3 +157,30 @@ fun LogsScreen() {
} }
} }
} }
@Composable
private fun StatusBadge(status: GoalStatus) {
val (text, color) = when (status) {
GoalStatus.Running -> "Running" to StatusAmber
GoalStatus.Completed -> "Completed" to StatusGreen
GoalStatus.Failed -> "Failed" to StatusRed
GoalStatus.Idle -> "Idle" to Color.Gray
}
Box(
modifier = Modifier
.clip(RoundedCornerShape(6.dp))
.background(color.copy(alpha = 0.15f))
.padding(horizontal = 8.dp, vertical = 4.dp)
) {
Text(
text = text,
style = MaterialTheme.typography.labelSmall,
color = color
)
}
}
private fun formatTimestamp(timestamp: Long): String {
val sdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
return sdf.format(Date(timestamp))
}

View File

@@ -0,0 +1,393 @@
package com.thisux.droidclaw.ui.screens
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.media.projection.MediaProjectionManager
import android.net.Uri
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Error
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.thisux.droidclaw.DroidClawApp
import com.thisux.droidclaw.accessibility.DroidClawAccessibilityService
import com.thisux.droidclaw.capture.ScreenCaptureManager
import com.thisux.droidclaw.connection.ConnectionService
import com.thisux.droidclaw.ui.theme.StatusGreen
import com.thisux.droidclaw.util.BatteryOptimization
import kotlinx.coroutines.launch
@Composable
fun OnboardingScreen(onComplete: () -> Unit) {
val context = LocalContext.current
val app = context.applicationContext as DroidClawApp
val scope = rememberCoroutineScope()
var currentStep by remember { mutableIntStateOf(0) }
val apiKey by app.settingsStore.apiKey.collectAsState(initial = "")
val serverUrl by app.settingsStore.serverUrl.collectAsState(initial = "wss://tunnel.droidclaw.ai")
var editingApiKey by remember { mutableStateOf("") }
var editingServerUrl by remember { mutableStateOf("wss://tunnel.droidclaw.ai") }
// Sync from datastore when loaded
var initialized by remember { mutableStateOf(false) }
if (!initialized && apiKey.isNotEmpty()) {
editingApiKey = apiKey
initialized = true
}
if (serverUrl != "wss://tunnel.droidclaw.ai" || editingServerUrl == "wss://tunnel.droidclaw.ai") {
editingServerUrl = serverUrl
}
AnimatedContent(targetState = currentStep, label = "onboarding_step") { step ->
when (step) {
0 -> OnboardingStepOne(
apiKey = editingApiKey,
serverUrl = editingServerUrl,
onApiKeyChange = { editingApiKey = it },
onServerUrlChange = { editingServerUrl = it },
onContinue = {
scope.launch {
app.settingsStore.setApiKey(editingApiKey)
app.settingsStore.setServerUrl(editingServerUrl)
currentStep = 1
}
}
)
1 -> OnboardingStepTwo(
onGetStarted = {
scope.launch {
app.settingsStore.setHasOnboarded(true)
// Auto-connect
val intent = Intent(context, ConnectionService::class.java).apply {
action = ConnectionService.ACTION_CONNECT
}
context.startForegroundService(intent)
onComplete()
}
}
)
}
}
}
@Composable
private fun OnboardingStepOne(
apiKey: String,
serverUrl: String,
onApiKeyChange: (String) -> Unit,
onServerUrlChange: (String) -> Unit,
onContinue: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 24.dp, vertical = 48.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(48.dp))
Text(
text = "Welcome to",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground
)
Text(
text = "DroidClaw",
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "AI-powered Android automation",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(48.dp))
OutlinedTextField(
value = apiKey,
onValueChange = onApiKeyChange,
label = { Text("API Key") },
modifier = Modifier.fillMaxWidth(),
visualTransformation = PasswordVisualTransformation(),
singleLine = true,
shape = RoundedCornerShape(12.dp)
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = serverUrl,
onValueChange = onServerUrlChange,
label = { Text("Server URL") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
shape = RoundedCornerShape(12.dp)
)
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = onContinue,
enabled = apiKey.isNotBlank(),
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
shape = RoundedCornerShape(12.dp)
) {
Text("Continue", style = MaterialTheme.typography.labelLarge)
}
}
}
@Composable
private fun OnboardingStepTwo(onGetStarted: () -> Unit) {
val context = LocalContext.current
val isCaptureAvailable by ScreenCaptureManager.isAvailable.collectAsState()
var isAccessibilityEnabled by remember {
mutableStateOf(DroidClawAccessibilityService.isEnabledOnDevice(context))
}
var hasCaptureConsent by remember {
ScreenCaptureManager.restoreConsent(context)
mutableStateOf(isCaptureAvailable || ScreenCaptureManager.hasConsent())
}
var isBatteryExempt by remember {
mutableStateOf(BatteryOptimization.isIgnoringBatteryOptimizations(context))
}
var hasOverlayPermission by remember {
mutableStateOf(Settings.canDrawOverlays(context))
}
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
isAccessibilityEnabled = DroidClawAccessibilityService.isEnabledOnDevice(context)
ScreenCaptureManager.restoreConsent(context)
hasCaptureConsent = isCaptureAvailable || ScreenCaptureManager.hasConsent()
isBatteryExempt = BatteryOptimization.isIgnoringBatteryOptimizations(context)
hasOverlayPermission = Settings.canDrawOverlays(context)
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
val projectionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
ScreenCaptureManager.storeConsent(context, result.resultCode, result.data)
hasCaptureConsent = true
}
}
val allGranted = isAccessibilityEnabled && hasCaptureConsent && isBatteryExempt && hasOverlayPermission
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 24.dp, vertical = 48.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(32.dp))
Text(
text = "Setup Permissions",
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "DroidClaw needs these permissions to control your device",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(32.dp))
OnboardingChecklistItem(
label = "Accessibility Service",
description = "Required to read screen content and perform actions",
isOk = isAccessibilityEnabled,
actionLabel = "Enable",
onAction = { BatteryOptimization.openAccessibilitySettings(context) }
)
Spacer(modifier = Modifier.height(12.dp))
OnboardingChecklistItem(
label = "Screen Capture",
description = "Required to capture screenshots for visual analysis",
isOk = hasCaptureConsent,
actionLabel = "Grant",
onAction = {
val mgr = context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
projectionLauncher.launch(mgr.createScreenCaptureIntent())
}
)
Spacer(modifier = Modifier.height(12.dp))
OnboardingChecklistItem(
label = "Battery Optimization",
description = "Prevents the system from killing the background service",
isOk = isBatteryExempt,
actionLabel = "Disable",
onAction = { BatteryOptimization.requestExemption(context) }
)
Spacer(modifier = Modifier.height(12.dp))
OnboardingChecklistItem(
label = "Overlay Permission",
description = "Shows agent status indicator over other apps",
isOk = hasOverlayPermission,
actionLabel = "Grant",
onAction = {
context.startActivity(
Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:${context.packageName}")
)
)
}
)
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = onGetStarted,
enabled = allGranted,
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
shape = RoundedCornerShape(12.dp)
) {
Text("Get Started", style = MaterialTheme.typography.labelLarge)
}
if (!allGranted) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Grant all permissions to continue",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
}
}
@Composable
private fun OnboardingChecklistItem(
label: String,
description: String,
isOk: Boolean,
actionLabel: String,
onAction: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = if (isOk) {
MaterialTheme.colorScheme.secondaryContainer
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
}
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.weight(1f)
) {
Icon(
imageVector = if (isOk) Icons.Filled.CheckCircle else Icons.Filled.Error,
contentDescription = if (isOk) "Granted" else "Not granted",
tint = if (isOk) StatusGreen else MaterialTheme.colorScheme.error
)
Column {
Text(
text = label,
style = MaterialTheme.typography.titleSmall
)
Text(
text = description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
if (!isOk) {
OutlinedButton(onClick = onAction) {
Text(actionLabel)
}
}
}
}
}

View File

@@ -8,7 +8,9 @@ import android.net.Uri
import android.provider.Settings import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -16,11 +18,16 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
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.CheckCircle import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.Error
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -38,16 +45,22 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.thisux.droidclaw.DroidClawApp import com.thisux.droidclaw.DroidClawApp
import com.thisux.droidclaw.accessibility.DroidClawAccessibilityService import com.thisux.droidclaw.accessibility.DroidClawAccessibilityService
import com.thisux.droidclaw.capture.ScreenCaptureManager import com.thisux.droidclaw.capture.ScreenCaptureManager
import com.thisux.droidclaw.connection.ConnectionService
import com.thisux.droidclaw.model.ConnectionState
import com.thisux.droidclaw.ui.theme.StatusAmber
import com.thisux.droidclaw.ui.theme.StatusGreen
import com.thisux.droidclaw.ui.theme.StatusRed
import com.thisux.droidclaw.util.BatteryOptimization import com.thisux.droidclaw.util.BatteryOptimization
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -57,6 +70,9 @@ fun SettingsScreen() {
val app = context.applicationContext as DroidClawApp val app = context.applicationContext as DroidClawApp
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val connectionState by ConnectionService.connectionState.collectAsState()
val errorMessage by ConnectionService.errorMessage.collectAsState()
val apiKey by app.settingsStore.apiKey.collectAsState(initial = "") val apiKey by app.settingsStore.apiKey.collectAsState(initial = "")
val serverUrl by app.settingsStore.serverUrl.collectAsState(initial = "wss://tunnel.droidclaw.ai") val serverUrl by app.settingsStore.serverUrl.collectAsState(initial = "wss://tunnel.droidclaw.ai")
@@ -74,8 +90,12 @@ fun SettingsScreen() {
ScreenCaptureManager.restoreConsent(context) ScreenCaptureManager.restoreConsent(context)
mutableStateOf(isCaptureAvailable || ScreenCaptureManager.hasConsent()) mutableStateOf(isCaptureAvailable || ScreenCaptureManager.hasConsent())
} }
var isBatteryExempt by remember { mutableStateOf(BatteryOptimization.isIgnoringBatteryOptimizations(context)) } var isBatteryExempt by remember {
var hasOverlayPermission by remember { mutableStateOf(Settings.canDrawOverlays(context)) } mutableStateOf(BatteryOptimization.isIgnoringBatteryOptimizations(context))
}
var hasOverlayPermission by remember {
mutableStateOf(Settings.canDrawOverlays(context))
}
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) { DisposableEffect(lifecycleOwner) {
@@ -104,11 +124,14 @@ fun SettingsScreen() {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp) .padding(horizontal = 20.dp)
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Text("Settings", style = MaterialTheme.typography.headlineMedium) Spacer(modifier = Modifier.height(4.dp))
// --- Server Section ---
SectionHeader("Server")
OutlinedTextField( OutlinedTextField(
value = displayApiKey, value = displayApiKey,
@@ -116,7 +139,8 @@ fun SettingsScreen() {
label = { Text("API Key") }, label = { Text("API Key") },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
visualTransformation = PasswordVisualTransformation(), visualTransformation = PasswordVisualTransformation(),
singleLine = true singleLine = true,
shape = RoundedCornerShape(12.dp)
) )
if (editingApiKey != null && editingApiKey != apiKey) { if (editingApiKey != null && editingApiKey != apiKey) {
OutlinedButton( OutlinedButton(
@@ -125,7 +149,8 @@ fun SettingsScreen() {
app.settingsStore.setApiKey(displayApiKey) app.settingsStore.setApiKey(displayApiKey)
editingApiKey = null editingApiKey = null
} }
} },
shape = RoundedCornerShape(8.dp)
) { ) {
Text("Save API Key") Text("Save API Key")
} }
@@ -136,7 +161,8 @@ fun SettingsScreen() {
onValueChange = { editingServerUrl = it }, onValueChange = { editingServerUrl = it },
label = { Text("Server URL") }, label = { Text("Server URL") },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true singleLine = true,
shape = RoundedCornerShape(12.dp)
) )
if (editingServerUrl != null && editingServerUrl != serverUrl) { if (editingServerUrl != null && editingServerUrl != serverUrl) {
OutlinedButton( OutlinedButton(
@@ -145,15 +171,89 @@ fun SettingsScreen() {
app.settingsStore.setServerUrl(displayServerUrl) app.settingsStore.setServerUrl(displayServerUrl)
editingServerUrl = null editingServerUrl = null
} }
} },
shape = RoundedCornerShape(8.dp)
) { ) {
Text("Save Server URL") Text("Save Server URL")
} }
} }
Spacer(modifier = Modifier.height(8.dp)) // --- Connection Section ---
SectionHeader("Connection")
Text("Setup Checklist", style = MaterialTheme.typography.titleMedium) Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Box(
modifier = Modifier
.size(10.dp)
.clip(CircleShape)
.background(
when (connectionState) {
ConnectionState.Connected -> StatusGreen
ConnectionState.Connecting -> StatusAmber
ConnectionState.Error -> StatusRed
ConnectionState.Disconnected -> Color.Gray
}
)
)
Text(
text = when (connectionState) {
ConnectionState.Connected -> "Connected to server"
ConnectionState.Connecting -> "Connecting..."
ConnectionState.Error -> errorMessage ?: "Connection error"
ConnectionState.Disconnected -> "Disconnected"
},
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(start = 8.dp)
)
}
Button(
onClick = {
val intent = Intent(context, ConnectionService::class.java).apply {
action = if (connectionState == ConnectionState.Disconnected || connectionState == ConnectionState.Error) {
ConnectionService.ACTION_CONNECT
} else {
ConnectionService.ACTION_DISCONNECT
}
}
context.startForegroundService(intent)
},
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = if (connectionState == ConnectionState.Connected || connectionState == ConnectionState.Connecting) {
MaterialTheme.colorScheme.error
} else {
MaterialTheme.colorScheme.primary
}
)
) {
Text(
when (connectionState) {
ConnectionState.Disconnected, ConnectionState.Error -> "Connect"
else -> "Disconnect"
}
)
}
}
}
// --- Permissions Section ---
SectionHeader("Permissions")
ChecklistItem( ChecklistItem(
label = "API key configured", label = "API key configured",
@@ -200,9 +300,20 @@ fun SettingsScreen() {
} }
) )
Spacer(modifier = Modifier.height(16.dp))
} }
} }
@Composable
private fun SectionHeader(text: String) {
Text(
text = text,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 4.dp)
)
}
@Composable @Composable
private fun ChecklistItem( private fun ChecklistItem(
label: String, label: String,
@@ -212,6 +323,7 @@ private fun ChecklistItem(
) { ) {
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = if (isOk) { containerColor = if (isOk) {
MaterialTheme.colorScheme.secondaryContainer MaterialTheme.colorScheme.secondaryContainer
@@ -234,12 +346,15 @@ private fun ChecklistItem(
Icon( Icon(
imageVector = if (isOk) Icons.Filled.CheckCircle else Icons.Filled.Error, imageVector = if (isOk) Icons.Filled.CheckCircle else Icons.Filled.Error,
contentDescription = if (isOk) "OK" else "Missing", contentDescription = if (isOk) "OK" else "Missing",
tint = if (isOk) Color(0xFF4CAF50) else MaterialTheme.colorScheme.error tint = if (isOk) StatusGreen else MaterialTheme.colorScheme.error
) )
Text(label) Text(label, style = MaterialTheme.typography.bodyMedium)
} }
if (!isOk && actionLabel != null) { if (!isOk && actionLabel != null) {
OutlinedButton(onClick = onAction) { OutlinedButton(
onClick = onAction,
shape = RoundedCornerShape(8.dp)
) {
Text(actionLabel) Text(actionLabel)
} }
} }

View File

@@ -2,10 +2,38 @@ package com.thisux.droidclaw.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF) // Primary: Crimson Red
val PurpleGrey80 = Color(0xFFCCC2DC) val CrimsonRed = Color(0xFFC62828)
val Pink80 = Color(0xFFEFB8C8) val CrimsonRedLight = Color(0xFFEF5350)
val Purple40 = Color(0xFF6650a4) // Secondary: Dark Charcoal
val PurpleGrey40 = Color(0xFF625b71) val CharcoalDark = Color(0xFF212121)
val Pink40 = Color(0xFF7D5260) val CharcoalLight = Color(0xFF424242)
// Tertiary/Accent: Golden
val GoldenAccent = Color(0xFFFFB300)
val GoldenAccentLight = Color(0xFFFFD54F)
// Surfaces
val SurfaceDark = Color(0xFF1A1A1A)
val SurfaceLight = Color(0xFFFAFAFA)
val BackgroundDark = Color(0xFF121212)
val BackgroundLight = Color(0xFFFFFFFF)
// Status
val StatusGreen = Color(0xFF4CAF50)
val StatusRed = Color(0xFFE53935)
val StatusAmber = Color(0xFFFFA726)
// On-colors
val OnPrimaryDark = Color.White
val OnSecondaryDark = Color.White
val OnSurfaceDark = Color(0xFFE0E0E0)
val OnSurfaceVariantDark = Color(0xFF9E9E9E)
val OnBackgroundDark = Color(0xFFE0E0E0)
val OnPrimaryLight = Color.White
val OnSecondaryLight = Color.White
val OnSurfaceLight = Color(0xFF1C1B1F)
val OnSurfaceVariantLight = Color(0xFF49454F)
val OnBackgroundLight = Color(0xFF1C1B1F)

View File

@@ -1,54 +1,69 @@
package com.thisux.droidclaw.ui.theme package com.thisux.droidclaw.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme( private val DarkColorScheme = darkColorScheme(
primary = Purple80, primary = CrimsonRed,
secondary = PurpleGrey80, onPrimary = OnPrimaryDark,
tertiary = Pink80 primaryContainer = CrimsonRed.copy(alpha = 0.3f),
onPrimaryContainer = CrimsonRedLight,
secondary = CharcoalLight,
onSecondary = OnSecondaryDark,
secondaryContainer = CharcoalLight.copy(alpha = 0.3f),
onSecondaryContainer = OnSurfaceDark,
tertiary = GoldenAccent,
onTertiary = CharcoalDark,
tertiaryContainer = GoldenAccent.copy(alpha = 0.3f),
onTertiaryContainer = GoldenAccentLight,
background = BackgroundDark,
onBackground = OnBackgroundDark,
surface = SurfaceDark,
onSurface = OnSurfaceDark,
surfaceVariant = CharcoalLight,
onSurfaceVariant = OnSurfaceVariantDark,
error = StatusRed,
onError = OnPrimaryDark,
errorContainer = StatusRed.copy(alpha = 0.2f),
onErrorContainer = StatusRed,
outline = OnSurfaceVariantDark
) )
private val LightColorScheme = lightColorScheme( private val LightColorScheme = lightColorScheme(
primary = Purple40, primary = CrimsonRed,
secondary = PurpleGrey40, onPrimary = OnPrimaryLight,
tertiary = Pink40 primaryContainer = CrimsonRedLight.copy(alpha = 0.2f),
onPrimaryContainer = CrimsonRed,
/* Other default colors to override secondary = CharcoalDark,
background = Color(0xFFFFFBFE), onSecondary = OnSecondaryLight,
surface = Color(0xFFFFFBFE), secondaryContainer = SurfaceLight,
onPrimary = Color.White, onSecondaryContainer = CharcoalDark,
onSecondary = Color.White, tertiary = GoldenAccent,
onTertiary = Color.White, onTertiary = CharcoalDark,
onBackground = Color(0xFF1C1B1F), tertiaryContainer = GoldenAccentLight.copy(alpha = 0.3f),
onSurface = Color(0xFF1C1B1F), onTertiaryContainer = CharcoalDark,
*/ background = BackgroundLight,
onBackground = OnBackgroundLight,
surface = SurfaceLight,
onSurface = OnSurfaceLight,
surfaceVariant = SurfaceLight,
onSurfaceVariant = OnSurfaceVariantLight,
error = StatusRed,
onError = OnPrimaryLight,
errorContainer = StatusRed.copy(alpha = 0.1f),
onErrorContainer = StatusRed,
outline = OnSurfaceVariantLight
) )
@Composable @Composable
fun DroidClawTheme( fun DroidClawTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val colorScheme = when { val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,

View File

@@ -2,33 +2,125 @@ package com.thisux.droidclaw.ui.theme
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
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.unit.sp import androidx.compose.ui.unit.sp
import com.thisux.droidclaw.R
val InstrumentSerif = FontFamily(
Font(R.font.instrument_serif_regular, FontWeight.Normal)
)
val GoogleSans = FontFamily(
Font(R.font.google_sans_regular, FontWeight.Normal),
Font(R.font.google_sans_medium, FontWeight.Medium)
)
// Set of Material typography styles to start with
val Typography = Typography( val Typography = Typography(
bodyLarge = TextStyle( displayLarge = TextStyle(
fontFamily = FontFamily.Default, fontFamily = InstrumentSerif,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 16.sp, fontSize = 57.sp,
lineHeight = 24.sp, lineHeight = 64.sp,
letterSpacing = 0.5.sp letterSpacing = (-0.25).sp
) ),
/* Other default text styles to override displayMedium = TextStyle(
fontFamily = InstrumentSerif,
fontWeight = FontWeight.Normal,
fontSize = 45.sp,
lineHeight = 52.sp,
letterSpacing = 0.sp
),
displaySmall = TextStyle(
fontFamily = InstrumentSerif,
fontWeight = FontWeight.Normal,
fontSize = 36.sp,
lineHeight = 44.sp,
letterSpacing = 0.sp
),
headlineLarge = TextStyle(
fontFamily = InstrumentSerif,
fontWeight = FontWeight.Normal,
fontSize = 32.sp,
lineHeight = 40.sp,
letterSpacing = 0.sp
),
headlineMedium = TextStyle(
fontFamily = InstrumentSerif,
fontWeight = FontWeight.Normal,
fontSize = 28.sp,
lineHeight = 36.sp,
letterSpacing = 0.sp
),
headlineSmall = TextStyle(
fontFamily = InstrumentSerif,
fontWeight = FontWeight.Normal,
fontSize = 24.sp,
lineHeight = 32.sp,
letterSpacing = 0.sp
),
titleLarge = TextStyle( titleLarge = TextStyle(
fontFamily = FontFamily.Default, fontFamily = InstrumentSerif,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 22.sp, fontSize = 22.sp,
lineHeight = 28.sp, lineHeight = 28.sp,
letterSpacing = 0.sp letterSpacing = 0.sp
), ),
titleMedium = TextStyle(
fontFamily = GoogleSans,
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.15.sp
),
titleSmall = TextStyle(
fontFamily = GoogleSans,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
bodyLarge = TextStyle(
fontFamily = GoogleSans,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
bodyMedium = TextStyle(
fontFamily = GoogleSans,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.25.sp
),
bodySmall = TextStyle(
fontFamily = GoogleSans,
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.4.sp
),
labelLarge = TextStyle(
fontFamily = GoogleSans,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
labelMedium = TextStyle(
fontFamily = GoogleSans,
fontWeight = FontWeight.Medium,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
),
labelSmall = TextStyle( labelSmall = TextStyle(
fontFamily = FontFamily.Default, fontFamily = GoogleSans,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 11.sp, fontSize = 11.sp,
lineHeight = 16.sp, lineHeight = 16.sp,
letterSpacing = 0.5.sp letterSpacing = 0.5.sp
) )
*/
) )

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

View File

@@ -5,166 +5,6 @@
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="108"> android:viewportHeight="108">
<path <path
android:fillColor="#3DDC84" android:fillColor="#121212"
android:pathData="M0,0h108v108h-108z" /> android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector> </vector>

View File

@@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -1,10 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="purple_200">#FFBB86FC</color> <color name="crimson_red">#FFC62828</color>
<color name="purple_500">#FF6200EE</color> <color name="crimson_red_light">#FFEF5350</color>
<color name="purple_700">#FF3700B3</color> <color name="charcoal_dark">#FF212121</color>
<color name="teal_200">#FF03DAC5</color> <color name="charcoal_light">#FF424242</color>
<color name="teal_700">#FF018786</color> <color name="golden_accent">#FFFFB300</color>
<color name="black">#FF000000</color> <color name="surface_dark">#FF1A1A1A</color>
<color name="background_dark">#FF121212</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="black">#FF000000</color>
<color name="ic_launcher_background">#FF121212</color>
</resources> </resources>