diff --git a/android/app/src/main/java/com/thisux/droidclaw/MainActivity.kt b/android/app/src/main/java/com/thisux/droidclaw/MainActivity.kt index e4e6147..6f0e56b 100644 --- a/android/app/src/main/java/com/thisux/droidclaw/MainActivity.kt +++ b/android/app/src/main/java/com/thisux/droidclaw/MainActivity.kt @@ -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() } diff --git a/android/app/src/main/java/com/thisux/droidclaw/data/SettingsStore.kt b/android/app/src/main/java/com/thisux/droidclaw/data/SettingsStore.kt index 68f046c..d141a10 100644 --- a/android/app/src/main/java/com/thisux/droidclaw/data/SettingsStore.kt +++ b/android/app/src/main/java/com/thisux/droidclaw/data/SettingsStore.kt @@ -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 = context.dataStore.data.map { prefs -> + prefs[SettingsKeys.HAS_ONBOARDED] ?: false + } + + suspend fun setHasOnboarded(value: Boolean) { + context.dataStore.edit { it[SettingsKeys.HAS_ONBOARDED] = value } + } } diff --git a/android/app/src/main/java/com/thisux/droidclaw/ui/components/PermissionStatusBar.kt b/android/app/src/main/java/com/thisux/droidclaw/ui/components/PermissionStatusBar.kt new file mode 100644 index 0000000..0cb6773 --- /dev/null +++ b/android/app/src/main/java/com/thisux/droidclaw/ui/components/PermissionStatusBar.kt @@ -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) + ) +} diff --git a/android/app/src/main/java/com/thisux/droidclaw/ui/screens/HomeScreen.kt b/android/app/src/main/java/com/thisux/droidclaw/ui/screens/HomeScreen.kt index 47bea8f..7ad924f 100644 --- a/android/app/src/main/java/com/thisux/droidclaw/ui/screens/HomeScreen.kt +++ b/android/app/src/main/java/com/thisux/droidclaw/ui/screens/HomeScreen.kt @@ -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)) +} diff --git a/android/app/src/main/java/com/thisux/droidclaw/ui/screens/LogsScreen.kt b/android/app/src/main/java/com/thisux/droidclaw/ui/screens/LogsScreen.kt index cda0cc2..722b154 100644 --- a/android/app/src/main/java/com/thisux/droidclaw/ui/screens/LogsScreen.kt +++ b/android/app/src/main/java/com/thisux/droidclaw/ui/screens/LogsScreen.kt @@ -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)) +} diff --git a/android/app/src/main/java/com/thisux/droidclaw/ui/screens/OnboardingScreen.kt b/android/app/src/main/java/com/thisux/droidclaw/ui/screens/OnboardingScreen.kt new file mode 100644 index 0000000..7dd4782 --- /dev/null +++ b/android/app/src/main/java/com/thisux/droidclaw/ui/screens/OnboardingScreen.kt @@ -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) + } + } + } + } +} diff --git a/android/app/src/main/java/com/thisux/droidclaw/ui/screens/SettingsScreen.kt b/android/app/src/main/java/com/thisux/droidclaw/ui/screens/SettingsScreen.kt index aa9444a..07ffbca 100644 --- a/android/app/src/main/java/com/thisux/droidclaw/ui/screens/SettingsScreen.kt +++ b/android/app/src/main/java/com/thisux/droidclaw/ui/screens/SettingsScreen.kt @@ -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) } } diff --git a/android/app/src/main/java/com/thisux/droidclaw/ui/theme/Color.kt b/android/app/src/main/java/com/thisux/droidclaw/ui/theme/Color.kt index 1ed9732..10b0b9f 100644 --- a/android/app/src/main/java/com/thisux/droidclaw/ui/theme/Color.kt +++ b/android/app/src/main/java/com/thisux/droidclaw/ui/theme/Color.kt @@ -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) \ No newline at end of file +// 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) diff --git a/android/app/src/main/java/com/thisux/droidclaw/ui/theme/Theme.kt b/android/app/src/main/java/com/thisux/droidclaw/ui/theme/Theme.kt index a474458..d6a8d9b 100644 --- a/android/app/src/main/java/com/thisux/droidclaw/ui/theme/Theme.kt +++ b/android/app/src/main/java/com/thisux/droidclaw/ui/theme/Theme.kt @@ -1,58 +1,73 @@ 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, typography = Typography, content = content ) -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/thisux/droidclaw/ui/theme/Type.kt b/android/app/src/main/java/com/thisux/droidclaw/ui/theme/Type.kt index e19dcf0..b6a6d5b 100644 --- a/android/app/src/main/java/com/thisux/droidclaw/ui/theme/Type.kt +++ b/android/app/src/main/java/com/thisux/droidclaw/ui/theme/Type.kt @@ -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 ) - */ -) \ No newline at end of file +) diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..7b96381 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..788dd57 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..aa30a4c Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..c54b279 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2d0a723 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml index 07d5da9..70dae43 100644 --- a/android/app/src/main/res/drawable/ic_launcher_background.xml +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -5,166 +5,6 @@ android:viewportWidth="108" android:viewportHeight="108"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d1..0000000 --- a/android/app/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/font/google_sans_medium.ttf b/android/app/src/main/res/font/google_sans_medium.ttf new file mode 100644 index 0000000..4a57a1a Binary files /dev/null and b/android/app/src/main/res/font/google_sans_medium.ttf differ diff --git a/android/app/src/main/res/font/google_sans_regular.ttf b/android/app/src/main/res/font/google_sans_regular.ttf new file mode 100644 index 0000000..399a6e0 Binary files /dev/null and b/android/app/src/main/res/font/google_sans_regular.ttf differ diff --git a/android/app/src/main/res/font/instrument_serif_regular.ttf b/android/app/src/main/res/font/instrument_serif_regular.ttf new file mode 100644 index 0000000..8794c69 Binary files /dev/null and b/android/app/src/main/res/font/instrument_serif_regular.ttf differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..96206bd Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..96206bd Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d..0000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..4f730ad Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..4f730ad Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611d..0000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..d83d619 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..d83d619 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a695..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..cc6a9b2 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..cc6a9b2 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f50..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..30fe7ac Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..30fe7ac Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae3..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index f8c6127..75e7b59 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1,10 +1,13 @@ - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 + #FFC62828 + #FFEF5350 + #FF212121 + #FF424242 + #FFFFB300 + #FF1A1A1A + #FF121212 #FFFFFFFF - \ No newline at end of file + #FF000000 + #FF121212 +