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
@@ -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() }
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
*/
|
||||
)
|
||||
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 210 KiB |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
BIN
android/app/src/main/res/font/google_sans_medium.ttf
Normal file
BIN
android/app/src/main/res/font/google_sans_regular.ttf
Normal file
BIN
android/app/src/main/res/font/instrument_serif_regular.ttf
Normal file
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 982 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -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>
|
||||