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.History
|
||||||
import androidx.compose.material.icons.filled.Home
|
import androidx.compose.material.icons.filled.Home
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.thisux.droidclaw.ui.components.PermissionStatusBar
|
||||||
import com.thisux.droidclaw.ui.screens.HomeScreen
|
import com.thisux.droidclaw.ui.screens.HomeScreen
|
||||||
import com.thisux.droidclaw.ui.screens.LogsScreen
|
import com.thisux.droidclaw.ui.screens.LogsScreen
|
||||||
|
import com.thisux.droidclaw.ui.screens.OnboardingScreen
|
||||||
import com.thisux.droidclaw.ui.screens.SettingsScreen
|
import com.thisux.droidclaw.ui.screens.SettingsScreen
|
||||||
import com.thisux.droidclaw.ui.theme.DroidClawTheme
|
import com.thisux.droidclaw.ui.theme.DroidClawTheme
|
||||||
|
import com.thisux.droidclaw.ui.theme.InstrumentSerif
|
||||||
|
|
||||||
sealed class Screen(val route: String, val label: String) {
|
sealed class Screen(val route: String, val label: String) {
|
||||||
data object Home : Screen("home", "Home")
|
data object Home : Screen("home", "Home")
|
||||||
data object Settings : Screen("settings", "Settings")
|
data object Settings : Screen("settings", "Settings")
|
||||||
data object Logs : Screen("logs", "Logs")
|
data object Logs : Screen("logs", "Logs")
|
||||||
|
data object Onboarding : Screen("onboarding", "Onboarding")
|
||||||
}
|
}
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@@ -47,26 +58,79 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainNavigation() {
|
fun MainNavigation() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val app = context.applicationContext as DroidClawApp
|
||||||
|
val hasOnboarded by app.settingsStore.hasOnboarded.collectAsState(initial = true)
|
||||||
|
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val screens = listOf(Screen.Home, Screen.Settings, Screen.Logs)
|
val bottomNavScreens = listOf(Screen.Home, Screen.Settings)
|
||||||
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
|
val currentRoute = navBackStackEntry?.destination?.route
|
||||||
|
|
||||||
|
val showChrome = currentRoute != Screen.Onboarding.route
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
topBar = {
|
||||||
|
if (showChrome) {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = "DroidClaw",
|
||||||
|
style = MaterialTheme.typography.titleLarge.copy(
|
||||||
|
fontFamily = InstrumentSerif
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.History,
|
||||||
|
contentDescription = "Logs"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
titleContentColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
|
if (showChrome) {
|
||||||
NavigationBar {
|
NavigationBar {
|
||||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
|
||||||
val currentDestination = navBackStackEntry?.destination
|
val currentDestination = navBackStackEntry?.destination
|
||||||
|
|
||||||
screens.forEach { screen ->
|
bottomNavScreens.forEach { screen ->
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
when (screen) {
|
when (screen) {
|
||||||
is Screen.Home -> Icons.Filled.Home
|
is Screen.Home -> Icons.Filled.Home
|
||||||
is Screen.Settings -> Icons.Filled.Settings
|
is Screen.Settings -> Icons.Filled.Settings
|
||||||
is Screen.Logs -> Icons.Filled.History
|
else -> Icons.Filled.Home
|
||||||
},
|
},
|
||||||
contentDescription = screen.label
|
contentDescription = screen.label
|
||||||
)
|
)
|
||||||
@@ -86,12 +150,24 @@ fun MainNavigation() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
|
val startDestination = if (hasOnboarded) Screen.Home.route else Screen.Onboarding.route
|
||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = Screen.Home.route,
|
startDestination = startDestination,
|
||||||
modifier = Modifier.padding(innerPadding)
|
modifier = Modifier.padding(innerPadding)
|
||||||
) {
|
) {
|
||||||
|
composable(Screen.Onboarding.route) {
|
||||||
|
OnboardingScreen(
|
||||||
|
onComplete = {
|
||||||
|
navController.navigate(Screen.Home.route) {
|
||||||
|
popUpTo(Screen.Onboarding.route) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
composable(Screen.Home.route) { HomeScreen() }
|
composable(Screen.Home.route) { HomeScreen() }
|
||||||
composable(Screen.Settings.route) { SettingsScreen() }
|
composable(Screen.Settings.route) { SettingsScreen() }
|
||||||
composable(Screen.Logs.route) { LogsScreen() }
|
composable(Screen.Logs.route) { LogsScreen() }
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ object SettingsKeys {
|
|||||||
val SERVER_URL = stringPreferencesKey("server_url")
|
val SERVER_URL = stringPreferencesKey("server_url")
|
||||||
val DEVICE_NAME = stringPreferencesKey("device_name")
|
val DEVICE_NAME = stringPreferencesKey("device_name")
|
||||||
val AUTO_CONNECT = booleanPreferencesKey("auto_connect")
|
val AUTO_CONNECT = booleanPreferencesKey("auto_connect")
|
||||||
|
val HAS_ONBOARDED = booleanPreferencesKey("has_onboarded")
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsStore(private val context: Context) {
|
class SettingsStore(private val context: Context) {
|
||||||
@@ -52,4 +53,12 @@ class SettingsStore(private val context: Context) {
|
|||||||
suspend fun setAutoConnect(value: Boolean) {
|
suspend fun setAutoConnect(value: Boolean) {
|
||||||
context.dataStore.edit { it[SettingsKeys.AUTO_CONNECT] = value }
|
context.dataStore.edit { it[SettingsKeys.AUTO_CONNECT] = value }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val hasOnboarded: Flow<Boolean> = context.dataStore.data.map { prefs ->
|
||||||
|
prefs[SettingsKeys.HAS_ONBOARDED] ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setHasOnboarded(value: Boolean) {
|
||||||
|
context.dataStore.edit { it[SettingsKeys.HAS_ONBOARDED] = value }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Send
|
||||||
|
import androidx.compose.material.icons.filled.Stop
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -31,10 +43,24 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.thisux.droidclaw.connection.ConnectionService
|
import com.thisux.droidclaw.connection.ConnectionService
|
||||||
|
import com.thisux.droidclaw.model.AgentStep
|
||||||
import com.thisux.droidclaw.model.ConnectionState
|
import com.thisux.droidclaw.model.ConnectionState
|
||||||
import com.thisux.droidclaw.model.GoalStatus
|
import com.thisux.droidclaw.model.GoalStatus
|
||||||
|
import com.thisux.droidclaw.ui.theme.StatusGreen
|
||||||
|
import com.thisux.droidclaw.ui.theme.StatusRed
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
// Represents a message in the chat timeline
|
||||||
|
private sealed class ChatItem {
|
||||||
|
data class GoalMessage(val text: String) : ChatItem()
|
||||||
|
data class StepMessage(val step: AgentStep) : ChatItem()
|
||||||
|
data class StatusMessage(val status: GoalStatus, val stepCount: Int) : ChatItem()
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen() {
|
fun HomeScreen() {
|
||||||
@@ -43,86 +69,87 @@ fun HomeScreen() {
|
|||||||
val goalStatus by ConnectionService.currentGoalStatus.collectAsState()
|
val goalStatus by ConnectionService.currentGoalStatus.collectAsState()
|
||||||
val steps by ConnectionService.currentSteps.collectAsState()
|
val steps by ConnectionService.currentSteps.collectAsState()
|
||||||
val currentGoal by ConnectionService.currentGoal.collectAsState()
|
val currentGoal by ConnectionService.currentGoal.collectAsState()
|
||||||
val errorMessage by ConnectionService.errorMessage.collectAsState()
|
|
||||||
|
|
||||||
var goalInput by remember { mutableStateOf("") }
|
var goalInput by remember { mutableStateOf("") }
|
||||||
|
|
||||||
Column(
|
// Build chat items: goal bubble → step bubbles → status bubble
|
||||||
modifier = Modifier
|
val chatItems = remember(currentGoal, steps, goalStatus) {
|
||||||
.fillMaxSize()
|
buildList {
|
||||||
.padding(16.dp)
|
if (currentGoal.isNotEmpty()) {
|
||||||
) {
|
add(ChatItem.GoalMessage(currentGoal))
|
||||||
// Status Badge
|
}
|
||||||
Row(
|
steps.forEach { add(ChatItem.StepMessage(it)) }
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
if (goalStatus == GoalStatus.Running) {
|
||||||
modifier = Modifier.fillMaxWidth()
|
add(ChatItem.StatusMessage(GoalStatus.Running, steps.size))
|
||||||
) {
|
} else if (goalStatus == GoalStatus.Completed || goalStatus == GoalStatus.Failed) {
|
||||||
|
add(ChatItem.StatusMessage(goalStatus, steps.size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
// Auto-scroll to bottom when new items arrive
|
||||||
|
LaunchedEffect(chatItems.size) {
|
||||||
|
if (chatItems.isNotEmpty()) {
|
||||||
|
listState.animateScrollToItem(chatItems.lastIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
// Chat area
|
||||||
|
if (chatItems.isEmpty()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(12.dp)
|
.weight(1f)
|
||||||
.clip(CircleShape)
|
.fillMaxWidth(),
|
||||||
.background(
|
contentAlignment = Alignment.Center
|
||||||
when (connectionState) {
|
) {
|
||||||
ConnectionState.Connected -> Color(0xFF4CAF50)
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
ConnectionState.Connecting -> Color(0xFFFFC107)
|
|
||||||
ConnectionState.Error -> Color(0xFFF44336)
|
|
||||||
ConnectionState.Disconnected -> Color.Gray
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Text(
|
Text(
|
||||||
text = when (connectionState) {
|
text = "What should I do?",
|
||||||
ConnectionState.Connected -> "Connected to server"
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
ConnectionState.Connecting -> "Connecting..."
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
||||||
ConnectionState.Error -> errorMessage ?: "Connection error"
|
)
|
||||||
ConnectionState.Disconnected -> "Disconnected"
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
},
|
Text(
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
text = "Send a goal to start the agent",
|
||||||
modifier = Modifier.padding(start = 8.dp)
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.35f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
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 {
|
} else {
|
||||||
ConnectionService.ACTION_DISCONNECT
|
LazyColumn(
|
||||||
}
|
state = listState,
|
||||||
}
|
modifier = Modifier
|
||||||
context.startForegroundService(intent)
|
.weight(1f)
|
||||||
},
|
.fillMaxWidth()
|
||||||
modifier = Modifier.fillMaxWidth()
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
contentPadding = androidx.compose.foundation.layout.PaddingValues(vertical = 12.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
items(chatItems, key = { item ->
|
||||||
when (connectionState) {
|
when (item) {
|
||||||
ConnectionState.Disconnected, ConnectionState.Error -> "Connect"
|
is ChatItem.GoalMessage -> "goal_${item.text}"
|
||||||
else -> "Disconnect"
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
// Input bar pinned at bottom
|
||||||
|
InputBar(
|
||||||
// Goal Input
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = goalInput,
|
value = goalInput,
|
||||||
onValueChange = { goalInput = it },
|
onValueChange = { goalInput = it },
|
||||||
label = { Text("Enter a goal...") },
|
onSend = {
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
enabled = connectionState == ConnectionState.Connected && goalStatus != GoalStatus.Running,
|
|
||||||
singleLine = true
|
|
||||||
)
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
if (goalInput.isNotBlank()) {
|
if (goalInput.isNotBlank()) {
|
||||||
val intent = Intent(context, ConnectionService::class.java).apply {
|
val intent = Intent(context, ConnectionService::class.java).apply {
|
||||||
action = ConnectionService.ACTION_SEND_GOAL
|
action = ConnectionService.ACTION_SEND_GOAL
|
||||||
@@ -132,66 +159,214 @@ fun HomeScreen() {
|
|||||||
goalInput = ""
|
goalInput = ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = connectionState == ConnectionState.Connected
|
onStop = { ConnectionService.instance?.stopGoal() },
|
||||||
|
canSend = connectionState == ConnectionState.Connected
|
||||||
&& goalStatus != GoalStatus.Running
|
&& goalStatus != GoalStatus.Running
|
||||||
&& goalInput.isNotBlank()
|
&& goalInput.isNotBlank(),
|
||||||
) {
|
isRunning = goalStatus == GoalStatus.Running,
|
||||||
Text("Send")
|
isConnected = connectionState == ConnectionState.Connected
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
/** User's goal — right-aligned bubble */
|
||||||
|
@Composable
|
||||||
// Step Log
|
private fun GoalBubble(text: String) {
|
||||||
LazyColumn(
|
Row(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
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)
|
||||||
) {
|
) {
|
||||||
items(steps) { step ->
|
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Column(modifier = Modifier.padding(12.dp)) {
|
|
||||||
Text(
|
Text(
|
||||||
text = "Step ${step.step}: ${step.action}",
|
text = text,
|
||||||
style = MaterialTheme.typography.titleSmall
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary
|
||||||
)
|
)
|
||||||
if (step.reasoning.isNotEmpty()) {
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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(
|
||||||
text = step.reasoning,
|
text = "${step.step}",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
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))
|
||||||
// Goal Status
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f))
|
||||||
if (goalStatus == GoalStatus.Completed || goalStatus == GoalStatus.Failed) {
|
.padding(horizontal = 14.dp, vertical = 10.dp)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
) {
|
||||||
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = if (goalStatus == GoalStatus.Completed) {
|
text = step.action,
|
||||||
"Goal completed (${steps.size} steps)"
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
} else {
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
"Goal failed"
|
)
|
||||||
},
|
if (step.reasoning.isNotEmpty()) {
|
||||||
style = MaterialTheme.typography.titleMedium,
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
color = if (goalStatus == GoalStatus.Completed) {
|
Text(
|
||||||
Color(0xFF4CAF50)
|
text = step.reasoning,
|
||||||
} else {
|
style = MaterialTheme.typography.bodySmall,
|
||||||
MaterialTheme.colorScheme.error
|
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
|
package com.thisux.droidclaw.ui.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.thisux.droidclaw.connection.ConnectionService
|
import com.thisux.droidclaw.connection.ConnectionService
|
||||||
import com.thisux.droidclaw.model.GoalStatus
|
import com.thisux.droidclaw.model.GoalStatus
|
||||||
|
import com.thisux.droidclaw.ui.theme.StatusAmber
|
||||||
|
import com.thisux.droidclaw.ui.theme.StatusGreen
|
||||||
|
import com.thisux.droidclaw.ui.theme.StatusRed
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LogsScreen() {
|
fun LogsScreen() {
|
||||||
@@ -33,68 +46,108 @@ fun LogsScreen() {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(16.dp)
|
.padding(horizontal = 20.dp)
|
||||||
) {
|
) {
|
||||||
Text("Logs", style = MaterialTheme.typography.headlineMedium)
|
// Goal banner
|
||||||
|
|
||||||
if (currentGoal.isNotEmpty()) {
|
if (currentGoal.isNotEmpty()) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 8.dp),
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = currentGoal,
|
text = currentGoal,
|
||||||
style = MaterialTheme.typography.titleSmall
|
style = MaterialTheme.typography.titleSmall
|
||||||
)
|
)
|
||||||
|
if (steps.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = when (goalStatus) {
|
text = "Step ${steps.size}",
|
||||||
GoalStatus.Running -> "Running"
|
style = MaterialTheme.typography.bodySmall,
|
||||||
GoalStatus.Completed -> "Completed"
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
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
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
StatusBadge(goalStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
|
||||||
if (steps.isEmpty()) {
|
if (steps.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "No steps recorded yet. Submit a goal to see agent activity here.",
|
text = "No steps recorded yet.\nSubmit a goal to see agent activity here.",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
modifier = Modifier.padding(top = 16.dp)
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
items(steps) { step ->
|
items(steps) { step ->
|
||||||
var expanded by remember { mutableStateOf(false) }
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
shape = RoundedCornerShape(12.dp),
|
||||||
.clickable { expanded = !expanded }
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(14.dp)) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// Step number badge
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.primary),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(12.dp)) {
|
|
||||||
Text(
|
Text(
|
||||||
text = "Step ${step.step}: ${step.action}",
|
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
|
style = MaterialTheme.typography.titleSmall
|
||||||
)
|
)
|
||||||
if (expanded && step.reasoning.isNotEmpty()) {
|
Text(
|
||||||
|
text = formatTimestamp(step.timestamp),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (step.reasoning.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = step.reasoning,
|
text = step.reasoning,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
modifier = Modifier.padding(top = 4.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,3 +157,30 @@ fun LogsScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StatusBadge(status: GoalStatus) {
|
||||||
|
val (text, color) = when (status) {
|
||||||
|
GoalStatus.Running -> "Running" to StatusAmber
|
||||||
|
GoalStatus.Completed -> "Completed" to StatusGreen
|
||||||
|
GoalStatus.Failed -> "Failed" to StatusRed
|
||||||
|
GoalStatus.Idle -> "Idle" to Color.Gray
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(6.dp))
|
||||||
|
.background(color.copy(alpha = 0.15f))
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatTimestamp(timestamp: Long): String {
|
||||||
|
val sdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
||||||
|
return sdf.format(Date(timestamp))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 android.provider.Settings
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -16,11 +18,16 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.material.icons.filled.Error
|
import androidx.compose.material.icons.filled.Error
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -38,16 +45,22 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import com.thisux.droidclaw.DroidClawApp
|
import com.thisux.droidclaw.DroidClawApp
|
||||||
import com.thisux.droidclaw.accessibility.DroidClawAccessibilityService
|
import com.thisux.droidclaw.accessibility.DroidClawAccessibilityService
|
||||||
import com.thisux.droidclaw.capture.ScreenCaptureManager
|
import com.thisux.droidclaw.capture.ScreenCaptureManager
|
||||||
|
import com.thisux.droidclaw.connection.ConnectionService
|
||||||
|
import com.thisux.droidclaw.model.ConnectionState
|
||||||
|
import com.thisux.droidclaw.ui.theme.StatusAmber
|
||||||
|
import com.thisux.droidclaw.ui.theme.StatusGreen
|
||||||
|
import com.thisux.droidclaw.ui.theme.StatusRed
|
||||||
import com.thisux.droidclaw.util.BatteryOptimization
|
import com.thisux.droidclaw.util.BatteryOptimization
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -57,6 +70,9 @@ fun SettingsScreen() {
|
|||||||
val app = context.applicationContext as DroidClawApp
|
val app = context.applicationContext as DroidClawApp
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val connectionState by ConnectionService.connectionState.collectAsState()
|
||||||
|
val errorMessage by ConnectionService.errorMessage.collectAsState()
|
||||||
|
|
||||||
val apiKey by app.settingsStore.apiKey.collectAsState(initial = "")
|
val apiKey by app.settingsStore.apiKey.collectAsState(initial = "")
|
||||||
val serverUrl by app.settingsStore.serverUrl.collectAsState(initial = "wss://tunnel.droidclaw.ai")
|
val serverUrl by app.settingsStore.serverUrl.collectAsState(initial = "wss://tunnel.droidclaw.ai")
|
||||||
|
|
||||||
@@ -74,8 +90,12 @@ fun SettingsScreen() {
|
|||||||
ScreenCaptureManager.restoreConsent(context)
|
ScreenCaptureManager.restoreConsent(context)
|
||||||
mutableStateOf(isCaptureAvailable || ScreenCaptureManager.hasConsent())
|
mutableStateOf(isCaptureAvailable || ScreenCaptureManager.hasConsent())
|
||||||
}
|
}
|
||||||
var isBatteryExempt by remember { mutableStateOf(BatteryOptimization.isIgnoringBatteryOptimizations(context)) }
|
var isBatteryExempt by remember {
|
||||||
var hasOverlayPermission by remember { mutableStateOf(Settings.canDrawOverlays(context)) }
|
mutableStateOf(BatteryOptimization.isIgnoringBatteryOptimizations(context))
|
||||||
|
}
|
||||||
|
var hasOverlayPermission by remember {
|
||||||
|
mutableStateOf(Settings.canDrawOverlays(context))
|
||||||
|
}
|
||||||
|
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
DisposableEffect(lifecycleOwner) {
|
DisposableEffect(lifecycleOwner) {
|
||||||
@@ -104,11 +124,14 @@ fun SettingsScreen() {
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(16.dp)
|
.padding(horizontal = 20.dp)
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Text("Settings", style = MaterialTheme.typography.headlineMedium)
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
// --- Server Section ---
|
||||||
|
SectionHeader("Server")
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = displayApiKey,
|
value = displayApiKey,
|
||||||
@@ -116,7 +139,8 @@ fun SettingsScreen() {
|
|||||||
label = { Text("API Key") },
|
label = { Text("API Key") },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
singleLine = true
|
singleLine = true,
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
)
|
)
|
||||||
if (editingApiKey != null && editingApiKey != apiKey) {
|
if (editingApiKey != null && editingApiKey != apiKey) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
@@ -125,7 +149,8 @@ fun SettingsScreen() {
|
|||||||
app.settingsStore.setApiKey(displayApiKey)
|
app.settingsStore.setApiKey(displayApiKey)
|
||||||
editingApiKey = null
|
editingApiKey = null
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
) {
|
) {
|
||||||
Text("Save API Key")
|
Text("Save API Key")
|
||||||
}
|
}
|
||||||
@@ -136,7 +161,8 @@ fun SettingsScreen() {
|
|||||||
onValueChange = { editingServerUrl = it },
|
onValueChange = { editingServerUrl = it },
|
||||||
label = { Text("Server URL") },
|
label = { Text("Server URL") },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true
|
singleLine = true,
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
)
|
)
|
||||||
if (editingServerUrl != null && editingServerUrl != serverUrl) {
|
if (editingServerUrl != null && editingServerUrl != serverUrl) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
@@ -145,15 +171,89 @@ fun SettingsScreen() {
|
|||||||
app.settingsStore.setServerUrl(displayServerUrl)
|
app.settingsStore.setServerUrl(displayServerUrl)
|
||||||
editingServerUrl = null
|
editingServerUrl = null
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
) {
|
) {
|
||||||
Text("Save Server URL")
|
Text("Save Server URL")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
// --- Connection Section ---
|
||||||
|
SectionHeader("Connection")
|
||||||
|
|
||||||
Text("Setup Checklist", style = MaterialTheme.typography.titleMedium)
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(10.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
when (connectionState) {
|
||||||
|
ConnectionState.Connected -> StatusGreen
|
||||||
|
ConnectionState.Connecting -> StatusAmber
|
||||||
|
ConnectionState.Error -> StatusRed
|
||||||
|
ConnectionState.Disconnected -> Color.Gray
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = when (connectionState) {
|
||||||
|
ConnectionState.Connected -> "Connected to server"
|
||||||
|
ConnectionState.Connecting -> "Connecting..."
|
||||||
|
ConnectionState.Error -> errorMessage ?: "Connection error"
|
||||||
|
ConnectionState.Disconnected -> "Disconnected"
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val intent = Intent(context, ConnectionService::class.java).apply {
|
||||||
|
action = if (connectionState == ConnectionState.Disconnected || connectionState == ConnectionState.Error) {
|
||||||
|
ConnectionService.ACTION_CONNECT
|
||||||
|
} else {
|
||||||
|
ConnectionService.ACTION_DISCONNECT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = if (connectionState == ConnectionState.Connected || connectionState == ConnectionState.Connecting) {
|
||||||
|
MaterialTheme.colorScheme.error
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
when (connectionState) {
|
||||||
|
ConnectionState.Disconnected, ConnectionState.Error -> "Connect"
|
||||||
|
else -> "Disconnect"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Permissions Section ---
|
||||||
|
SectionHeader("Permissions")
|
||||||
|
|
||||||
ChecklistItem(
|
ChecklistItem(
|
||||||
label = "API key configured",
|
label = "API key configured",
|
||||||
@@ -200,9 +300,20 @@ fun SettingsScreen() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SectionHeader(text: String) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChecklistItem(
|
private fun ChecklistItem(
|
||||||
label: String,
|
label: String,
|
||||||
@@ -212,6 +323,7 @@ private fun ChecklistItem(
|
|||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = if (isOk) {
|
containerColor = if (isOk) {
|
||||||
MaterialTheme.colorScheme.secondaryContainer
|
MaterialTheme.colorScheme.secondaryContainer
|
||||||
@@ -234,12 +346,15 @@ private fun ChecklistItem(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isOk) Icons.Filled.CheckCircle else Icons.Filled.Error,
|
imageVector = if (isOk) Icons.Filled.CheckCircle else Icons.Filled.Error,
|
||||||
contentDescription = if (isOk) "OK" else "Missing",
|
contentDescription = if (isOk) "OK" else "Missing",
|
||||||
tint = if (isOk) Color(0xFF4CAF50) else MaterialTheme.colorScheme.error
|
tint = if (isOk) StatusGreen else MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
Text(label)
|
Text(label, style = MaterialTheme.typography.bodyMedium)
|
||||||
}
|
}
|
||||||
if (!isOk && actionLabel != null) {
|
if (!isOk && actionLabel != null) {
|
||||||
OutlinedButton(onClick = onAction) {
|
OutlinedButton(
|
||||||
|
onClick = onAction,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
Text(actionLabel)
|
Text(actionLabel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,38 @@ package com.thisux.droidclaw.ui.theme
|
|||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
val Purple80 = Color(0xFFD0BCFF)
|
// Primary: Crimson Red
|
||||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
val CrimsonRed = Color(0xFFC62828)
|
||||||
val Pink80 = Color(0xFFEFB8C8)
|
val CrimsonRedLight = Color(0xFFEF5350)
|
||||||
|
|
||||||
val Purple40 = Color(0xFF6650a4)
|
// Secondary: Dark Charcoal
|
||||||
val PurpleGrey40 = Color(0xFF625b71)
|
val CharcoalDark = Color(0xFF212121)
|
||||||
val Pink40 = Color(0xFF7D5260)
|
val CharcoalLight = Color(0xFF424242)
|
||||||
|
|
||||||
|
// Tertiary/Accent: Golden
|
||||||
|
val GoldenAccent = Color(0xFFFFB300)
|
||||||
|
val GoldenAccentLight = Color(0xFFFFD54F)
|
||||||
|
|
||||||
|
// Surfaces
|
||||||
|
val SurfaceDark = Color(0xFF1A1A1A)
|
||||||
|
val SurfaceLight = Color(0xFFFAFAFA)
|
||||||
|
val BackgroundDark = Color(0xFF121212)
|
||||||
|
val BackgroundLight = Color(0xFFFFFFFF)
|
||||||
|
|
||||||
|
// Status
|
||||||
|
val StatusGreen = Color(0xFF4CAF50)
|
||||||
|
val StatusRed = Color(0xFFE53935)
|
||||||
|
val StatusAmber = Color(0xFFFFA726)
|
||||||
|
|
||||||
|
// On-colors
|
||||||
|
val OnPrimaryDark = Color.White
|
||||||
|
val OnSecondaryDark = Color.White
|
||||||
|
val OnSurfaceDark = Color(0xFFE0E0E0)
|
||||||
|
val OnSurfaceVariantDark = Color(0xFF9E9E9E)
|
||||||
|
val OnBackgroundDark = Color(0xFFE0E0E0)
|
||||||
|
|
||||||
|
val OnPrimaryLight = Color.White
|
||||||
|
val OnSecondaryLight = Color.White
|
||||||
|
val OnSurfaceLight = Color(0xFF1C1B1F)
|
||||||
|
val OnSurfaceVariantLight = Color(0xFF49454F)
|
||||||
|
val OnBackgroundLight = Color(0xFF1C1B1F)
|
||||||
|
|||||||
@@ -1,54 +1,69 @@
|
|||||||
package com.thisux.droidclaw.ui.theme
|
package com.thisux.droidclaw.ui.theme
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
private val DarkColorScheme = darkColorScheme(
|
||||||
primary = Purple80,
|
primary = CrimsonRed,
|
||||||
secondary = PurpleGrey80,
|
onPrimary = OnPrimaryDark,
|
||||||
tertiary = Pink80
|
primaryContainer = CrimsonRed.copy(alpha = 0.3f),
|
||||||
|
onPrimaryContainer = CrimsonRedLight,
|
||||||
|
secondary = CharcoalLight,
|
||||||
|
onSecondary = OnSecondaryDark,
|
||||||
|
secondaryContainer = CharcoalLight.copy(alpha = 0.3f),
|
||||||
|
onSecondaryContainer = OnSurfaceDark,
|
||||||
|
tertiary = GoldenAccent,
|
||||||
|
onTertiary = CharcoalDark,
|
||||||
|
tertiaryContainer = GoldenAccent.copy(alpha = 0.3f),
|
||||||
|
onTertiaryContainer = GoldenAccentLight,
|
||||||
|
background = BackgroundDark,
|
||||||
|
onBackground = OnBackgroundDark,
|
||||||
|
surface = SurfaceDark,
|
||||||
|
onSurface = OnSurfaceDark,
|
||||||
|
surfaceVariant = CharcoalLight,
|
||||||
|
onSurfaceVariant = OnSurfaceVariantDark,
|
||||||
|
error = StatusRed,
|
||||||
|
onError = OnPrimaryDark,
|
||||||
|
errorContainer = StatusRed.copy(alpha = 0.2f),
|
||||||
|
onErrorContainer = StatusRed,
|
||||||
|
outline = OnSurfaceVariantDark
|
||||||
)
|
)
|
||||||
|
|
||||||
private val LightColorScheme = lightColorScheme(
|
private val LightColorScheme = lightColorScheme(
|
||||||
primary = Purple40,
|
primary = CrimsonRed,
|
||||||
secondary = PurpleGrey40,
|
onPrimary = OnPrimaryLight,
|
||||||
tertiary = Pink40
|
primaryContainer = CrimsonRedLight.copy(alpha = 0.2f),
|
||||||
|
onPrimaryContainer = CrimsonRed,
|
||||||
/* Other default colors to override
|
secondary = CharcoalDark,
|
||||||
background = Color(0xFFFFFBFE),
|
onSecondary = OnSecondaryLight,
|
||||||
surface = Color(0xFFFFFBFE),
|
secondaryContainer = SurfaceLight,
|
||||||
onPrimary = Color.White,
|
onSecondaryContainer = CharcoalDark,
|
||||||
onSecondary = Color.White,
|
tertiary = GoldenAccent,
|
||||||
onTertiary = Color.White,
|
onTertiary = CharcoalDark,
|
||||||
onBackground = Color(0xFF1C1B1F),
|
tertiaryContainer = GoldenAccentLight.copy(alpha = 0.3f),
|
||||||
onSurface = Color(0xFF1C1B1F),
|
onTertiaryContainer = CharcoalDark,
|
||||||
*/
|
background = BackgroundLight,
|
||||||
|
onBackground = OnBackgroundLight,
|
||||||
|
surface = SurfaceLight,
|
||||||
|
onSurface = OnSurfaceLight,
|
||||||
|
surfaceVariant = SurfaceLight,
|
||||||
|
onSurfaceVariant = OnSurfaceVariantLight,
|
||||||
|
error = StatusRed,
|
||||||
|
onError = OnPrimaryLight,
|
||||||
|
errorContainer = StatusRed.copy(alpha = 0.1f),
|
||||||
|
onErrorContainer = StatusRed,
|
||||||
|
outline = OnSurfaceVariantLight
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DroidClawTheme(
|
fun DroidClawTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
// Dynamic color is available on Android 12+
|
|
||||||
dynamicColor: Boolean = true,
|
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = when {
|
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
|
||||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
|
||||||
val context = LocalContext.current
|
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
darkTheme -> DarkColorScheme
|
|
||||||
else -> LightColorScheme
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
|
|||||||
@@ -2,33 +2,125 @@ package com.thisux.droidclaw.ui.theme
|
|||||||
|
|
||||||
import androidx.compose.material3.Typography
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.thisux.droidclaw.R
|
||||||
|
|
||||||
// Set of Material typography styles to start with
|
val InstrumentSerif = FontFamily(
|
||||||
val Typography = Typography(
|
Font(R.font.instrument_serif_regular, FontWeight.Normal)
|
||||||
bodyLarge = TextStyle(
|
|
||||||
fontFamily = FontFamily.Default,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
lineHeight = 24.sp,
|
|
||||||
letterSpacing = 0.5.sp
|
|
||||||
)
|
)
|
||||||
/* Other default text styles to override
|
|
||||||
|
val GoogleSans = FontFamily(
|
||||||
|
Font(R.font.google_sans_regular, FontWeight.Normal),
|
||||||
|
Font(R.font.google_sans_medium, FontWeight.Medium)
|
||||||
|
)
|
||||||
|
|
||||||
|
val Typography = Typography(
|
||||||
|
displayLarge = TextStyle(
|
||||||
|
fontFamily = InstrumentSerif,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
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(
|
titleLarge = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = InstrumentSerif,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 22.sp,
|
fontSize = 22.sp,
|
||||||
lineHeight = 28.sp,
|
lineHeight = 28.sp,
|
||||||
letterSpacing = 0.sp
|
letterSpacing = 0.sp
|
||||||
),
|
),
|
||||||
|
titleMedium = TextStyle(
|
||||||
|
fontFamily = GoogleSans,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = 0.15.sp
|
||||||
|
),
|
||||||
|
titleSmall = TextStyle(
|
||||||
|
fontFamily = GoogleSans,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
letterSpacing = 0.1.sp
|
||||||
|
),
|
||||||
|
bodyLarge = TextStyle(
|
||||||
|
fontFamily = GoogleSans,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
),
|
||||||
|
bodyMedium = TextStyle(
|
||||||
|
fontFamily = GoogleSans,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
letterSpacing = 0.25.sp
|
||||||
|
),
|
||||||
|
bodySmall = TextStyle(
|
||||||
|
fontFamily = GoogleSans,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.4.sp
|
||||||
|
),
|
||||||
|
labelLarge = TextStyle(
|
||||||
|
fontFamily = GoogleSans,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
letterSpacing = 0.1.sp
|
||||||
|
),
|
||||||
|
labelMedium = TextStyle(
|
||||||
|
fontFamily = GoogleSans,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
),
|
||||||
labelSmall = TextStyle(
|
labelSmall = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = GoogleSans,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
letterSpacing = 0.5.sp
|
letterSpacing = 0.5.sp
|
||||||
)
|
)
|
||||||
*/
|
|
||||||
)
|
)
|
||||||
|
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:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#3DDC84"
|
android:fillColor="#121212"
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,0L9,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,0L19,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,0L29,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,0L39,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,0L49,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,0L59,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,0L69,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,0L79,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M89,0L89,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M99,0L99,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,9L108,9"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,19L108,19"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,29L108,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="purple_200">#FFBB86FC</color>
|
<color name="crimson_red">#FFC62828</color>
|
||||||
<color name="purple_500">#FF6200EE</color>
|
<color name="crimson_red_light">#FFEF5350</color>
|
||||||
<color name="purple_700">#FF3700B3</color>
|
<color name="charcoal_dark">#FF212121</color>
|
||||||
<color name="teal_200">#FF03DAC5</color>
|
<color name="charcoal_light">#FF424242</color>
|
||||||
<color name="teal_700">#FF018786</color>
|
<color name="golden_accent">#FFFFB300</color>
|
||||||
<color name="black">#FF000000</color>
|
<color name="surface_dark">#FF1A1A1A</color>
|
||||||
|
<color name="background_dark">#FF121212</color>
|
||||||
<color name="white">#FFFFFFFF</color>
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="ic_launcher_background">#FF121212</color>
|
||||||
</resources>
|
</resources>
|
||||||