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.Home
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.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
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.LogsScreen
import com.thisux.droidclaw.ui.screens.OnboardingScreen
import com.thisux.droidclaw.ui.screens.SettingsScreen
import com.thisux.droidclaw.ui.theme.DroidClawTheme
import com.thisux.droidclaw.ui.theme.InstrumentSerif
sealed class Screen(val route: String, val label: String) {
data object Home : Screen("home", "Home")
data object Settings : Screen("settings", "Settings")
data object Logs : Screen("logs", "Logs")
data object Onboarding : Screen("onboarding", "Onboarding")
}
class MainActivity : ComponentActivity() {
@@ -47,51 +58,116 @@ class MainActivity : ComponentActivity() {
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
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 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(
modifier = Modifier.fillMaxSize(),
bottomBar = {
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
screens.forEach { screen ->
NavigationBarItem(
icon = {
Icon(
when (screen) {
is Screen.Home -> Icons.Filled.Home
is Screen.Settings -> Icons.Filled.Settings
is Screen.Logs -> Icons.Filled.History
},
contentDescription = screen.label
topBar = {
if (showChrome) {
CenterAlignedTopAppBar(
title = {
Text(
text = "DroidClaw",
style = MaterialTheme.typography.titleLarge.copy(
fontFamily = InstrumentSerif
)
},
label = { Text(screen.label) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
)
},
actions = {
PermissionStatusBar(
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) {
saveState = 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 ->
val startDestination = if (hasOnboarded) Screen.Home.route else Screen.Onboarding.route
NavHost(
navController = navController,
startDestination = Screen.Home.route,
startDestination = startDestination,
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.Settings.route) { SettingsScreen() }
composable(Screen.Logs.route) { LogsScreen() }

View File

@@ -17,6 +17,7 @@ object SettingsKeys {
val SERVER_URL = stringPreferencesKey("server_url")
val DEVICE_NAME = stringPreferencesKey("device_name")
val AUTO_CONNECT = booleanPreferencesKey("auto_connect")
val HAS_ONBOARDED = booleanPreferencesKey("has_onboarded")
}
class SettingsStore(private val context: Context) {
@@ -52,4 +53,12 @@ class SettingsStore(private val context: Context) {
suspend fun setAutoConnect(value: Boolean) {
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.foundation.shape.RoundedCornerShape
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.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -31,10 +43,24 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.thisux.droidclaw.connection.ConnectionService
import com.thisux.droidclaw.model.AgentStep
import com.thisux.droidclaw.model.ConnectionState
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
fun HomeScreen() {
@@ -43,155 +69,304 @@ fun HomeScreen() {
val goalStatus by ConnectionService.currentGoalStatus.collectAsState()
val steps by ConnectionService.currentSteps.collectAsState()
val currentGoal by ConnectionService.currentGoal.collectAsState()
val errorMessage by ConnectionService.errorMessage.collectAsState()
var goalInput by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
// Status Badge
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
// Build chat items: goal bubble → step bubbles → status bubble
val chatItems = remember(currentGoal, steps, goalStatus) {
buildList {
if (currentGoal.isNotEmpty()) {
add(ChatItem.GoalMessage(currentGoal))
}
steps.forEach { add(ChatItem.StepMessage(it)) }
if (goalStatus == GoalStatus.Running) {
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(
modifier = Modifier
.size(12.dp)
.clip(CircleShape)
.background(
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()
.weight(1f)
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
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)
)
}
}
}
if (currentGoal.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Goal: $currentGoal",
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary
)
}
Spacer(modifier = Modifier.height(16.dp))
// Step Log
LazyColumn(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(steps) { step ->
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(12.dp)) {
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
)
}
} else {
LazyColumn(
state = listState,
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = androidx.compose.foundation.layout.PaddingValues(vertical = 12.dp)
) {
items(chatItems, key = { item ->
when (item) {
is ChatItem.GoalMessage -> "goal_${item.text}"
is ChatItem.StepMessage -> "step_${item.step.step}"
is ChatItem.StatusMessage -> "status_${item.status}"
}
}) { item ->
when (item) {
is ChatItem.GoalMessage -> GoalBubble(item.text)
is ChatItem.StepMessage -> AgentBubble(item.step)
is ChatItem.StatusMessage -> StatusBubble(item.status, item.stepCount)
}
}
}
}
// Goal Status
if (goalStatus == GoalStatus.Completed || goalStatus == GoalStatus.Failed) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = if (goalStatus == GoalStatus.Completed) {
"Goal completed (${steps.size} steps)"
} else {
"Goal failed"
},
style = MaterialTheme.typography.titleMedium,
color = if (goalStatus == GoalStatus.Completed) {
Color(0xFF4CAF50)
} else {
MaterialTheme.colorScheme.error
// Input bar pinned at bottom
InputBar(
value = goalInput,
onValueChange = { goalInput = it },
onSend = {
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 = ""
}
)
}
},
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
import androidx.compose.foundation.clickable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
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.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.graphics.Color
import androidx.compose.ui.unit.dp
import com.thisux.droidclaw.connection.ConnectionService
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
fun LogsScreen() {
@@ -33,68 +46,108 @@ fun LogsScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.padding(horizontal = 20.dp)
) {
Text("Logs", style = MaterialTheme.typography.headlineMedium)
// Goal banner
if (currentGoal.isNotEmpty()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
)
) {
Text(
text = currentGoal,
style = MaterialTheme.typography.titleSmall
)
Text(
text = when (goalStatus) {
GoalStatus.Running -> "Running"
GoalStatus.Completed -> "Completed"
GoalStatus.Failed -> "Failed"
GoalStatus.Idle -> "Idle"
},
color = when (goalStatus) {
GoalStatus.Running -> Color(0xFFFFC107)
GoalStatus.Completed -> Color(0xFF4CAF50)
GoalStatus.Failed -> MaterialTheme.colorScheme.error
GoalStatus.Idle -> Color.Gray
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = currentGoal,
style = MaterialTheme.typography.titleSmall
)
if (steps.isNotEmpty()) {
Text(
text = "Step ${steps.size}",
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()) {
Text(
text = "No steps recorded yet. Submit a goal to see agent activity here.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 16.dp)
)
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
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 {
LazyColumn(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(steps) { step ->
var expanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { expanded = !expanded }
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f)
)
) {
Column(modifier = Modifier.padding(12.dp)) {
Text(
text = "Step ${step.step}: ${step.action}",
style = MaterialTheme.typography.titleSmall
)
if (expanded && step.reasoning.isNotEmpty()) {
Column(modifier = Modifier.padding(14.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
// Step number badge
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 = step.reasoning,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp)
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@@ -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 androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.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
@@ -38,16 +45,22 @@ import androidx.compose.runtime.rememberCoroutineScope
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.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.compose.ui.text.input.PasswordVisualTransformation
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.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 kotlinx.coroutines.launch
@@ -57,6 +70,9 @@ fun SettingsScreen() {
val app = context.applicationContext as DroidClawApp
val scope = rememberCoroutineScope()
val connectionState by ConnectionService.connectionState.collectAsState()
val errorMessage by ConnectionService.errorMessage.collectAsState()
val apiKey by app.settingsStore.apiKey.collectAsState(initial = "")
val serverUrl by app.settingsStore.serverUrl.collectAsState(initial = "wss://tunnel.droidclaw.ai")
@@ -74,8 +90,12 @@ fun SettingsScreen() {
ScreenCaptureManager.restoreConsent(context)
mutableStateOf(isCaptureAvailable || ScreenCaptureManager.hasConsent())
}
var isBatteryExempt by remember { mutableStateOf(BatteryOptimization.isIgnoringBatteryOptimizations(context)) }
var hasOverlayPermission by remember { mutableStateOf(Settings.canDrawOverlays(context)) }
var isBatteryExempt by remember {
mutableStateOf(BatteryOptimization.isIgnoringBatteryOptimizations(context))
}
var hasOverlayPermission by remember {
mutableStateOf(Settings.canDrawOverlays(context))
}
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
@@ -104,11 +124,14 @@ fun SettingsScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.padding(horizontal = 20.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("Settings", style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(4.dp))
// --- Server Section ---
SectionHeader("Server")
OutlinedTextField(
value = displayApiKey,
@@ -116,7 +139,8 @@ fun SettingsScreen() {
label = { Text("API Key") },
modifier = Modifier.fillMaxWidth(),
visualTransformation = PasswordVisualTransformation(),
singleLine = true
singleLine = true,
shape = RoundedCornerShape(12.dp)
)
if (editingApiKey != null && editingApiKey != apiKey) {
OutlinedButton(
@@ -125,7 +149,8 @@ fun SettingsScreen() {
app.settingsStore.setApiKey(displayApiKey)
editingApiKey = null
}
}
},
shape = RoundedCornerShape(8.dp)
) {
Text("Save API Key")
}
@@ -136,7 +161,8 @@ fun SettingsScreen() {
onValueChange = { editingServerUrl = it },
label = { Text("Server URL") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
singleLine = true,
shape = RoundedCornerShape(12.dp)
)
if (editingServerUrl != null && editingServerUrl != serverUrl) {
OutlinedButton(
@@ -145,15 +171,89 @@ fun SettingsScreen() {
app.settingsStore.setServerUrl(displayServerUrl)
editingServerUrl = null
}
}
},
shape = RoundedCornerShape(8.dp)
) {
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(
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
private fun ChecklistItem(
label: String,
@@ -212,6 +323,7 @@ private fun ChecklistItem(
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = if (isOk) {
MaterialTheme.colorScheme.secondaryContainer
@@ -234,12 +346,15 @@ private fun ChecklistItem(
Icon(
imageVector = if (isOk) Icons.Filled.CheckCircle else Icons.Filled.Error,
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) {
OutlinedButton(onClick = onAction) {
OutlinedButton(
onClick = onAction,
shape = RoundedCornerShape(8.dp)
) {
Text(actionLabel)
}
}

View File

@@ -2,10 +2,38 @@ package com.thisux.droidclaw.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
// Primary: Crimson Red
val CrimsonRed = Color(0xFFC62828)
val CrimsonRedLight = Color(0xFFEF5350)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
// Secondary: Dark Charcoal
val CharcoalDark = Color(0xFF212121)
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
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
primary = CrimsonRed,
onPrimary = OnPrimaryDark,
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(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
primary = CrimsonRed,
onPrimary = OnPrimaryLight,
primaryContainer = CrimsonRedLight.copy(alpha = 0.2f),
onPrimaryContainer = CrimsonRed,
secondary = CharcoalDark,
onSecondary = OnSecondaryLight,
secondaryContainer = SurfaceLight,
onSecondaryContainer = CharcoalDark,
tertiary = GoldenAccent,
onTertiary = CharcoalDark,
tertiaryContainer = GoldenAccentLight.copy(alpha = 0.3f),
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
fun DroidClawTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
MaterialTheme(
colorScheme = colorScheme,

View File

@@ -2,33 +2,125 @@ package com.thisux.droidclaw.ui.theme
import androidx.compose.material3.Typography
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.FontWeight
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(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
displayLarge = TextStyle(
fontFamily = InstrumentSerif,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
fontSize = 57.sp,
lineHeight = 64.sp,
letterSpacing = (-0.25).sp
),
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(
fontFamily = FontFamily.Default,
fontFamily = InstrumentSerif,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.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(
fontFamily = FontFamily.Default,
fontFamily = GoogleSans,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.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:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:fillColor="#121212"
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>

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"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="crimson_red">#FFC62828</color>
<color name="crimson_red_light">#FFEF5350</color>
<color name="charcoal_dark">#FF212121</color>
<color name="charcoal_light">#FF424242</color>
<color name="golden_accent">#FFFFB300</color>
<color name="surface_dark">#FF1A1A1A</color>
<color name="background_dark">#FF121212</color>
<color name="white">#FFFFFFFF</color>
<color name="black">#FF000000</color>
<color name="ic_launcher_background">#FF121212</color>
</resources>