feat(android): add HomeScreen, SettingsScreen, LogsScreen with bottom nav
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
100
android/app/src/main/java/com/thisux/droidclaw/MainActivity.kt
Normal file
100
android/app/src/main/java/com/thisux/droidclaw/MainActivity.kt
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package com.thisux.droidclaw
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.History
|
||||||
|
import androidx.compose.material.icons.filled.Home
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.NavigationBar
|
||||||
|
import androidx.compose.material3.NavigationBarItem
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||||
|
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.thisux.droidclaw.ui.screens.HomeScreen
|
||||||
|
import com.thisux.droidclaw.ui.screens.LogsScreen
|
||||||
|
import com.thisux.droidclaw.ui.screens.SettingsScreen
|
||||||
|
import com.thisux.droidclaw.ui.theme.DroidClawTheme
|
||||||
|
|
||||||
|
sealed class Screen(val route: String, val label: String) {
|
||||||
|
data object Home : Screen("home", "Home")
|
||||||
|
data object Settings : Screen("settings", "Settings")
|
||||||
|
data object Logs : Screen("logs", "Logs")
|
||||||
|
}
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
setContent {
|
||||||
|
DroidClawTheme {
|
||||||
|
MainNavigation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MainNavigation() {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
val screens = listOf(Screen.Home, Screen.Settings, Screen.Logs)
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
bottomBar = {
|
||||||
|
NavigationBar {
|
||||||
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
|
val currentDestination = navBackStackEntry?.destination
|
||||||
|
|
||||||
|
screens.forEach { screen ->
|
||||||
|
NavigationBarItem(
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
when (screen) {
|
||||||
|
is Screen.Home -> Icons.Filled.Home
|
||||||
|
is Screen.Settings -> Icons.Filled.Settings
|
||||||
|
is Screen.Logs -> Icons.Filled.History
|
||||||
|
},
|
||||||
|
contentDescription = screen.label
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = { Text(screen.label) },
|
||||||
|
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(screen.route) {
|
||||||
|
popUpTo(navController.graph.findStartDestination().id) {
|
||||||
|
saveState = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = Screen.Home.route,
|
||||||
|
modifier = Modifier.padding(innerPadding)
|
||||||
|
) {
|
||||||
|
composable(Screen.Home.route) { HomeScreen() }
|
||||||
|
composable(Screen.Settings.route) { SettingsScreen() }
|
||||||
|
composable(Screen.Logs.route) { LogsScreen() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
package com.thisux.droidclaw.ui.screens
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.thisux.droidclaw.connection.ConnectionService
|
||||||
|
import com.thisux.droidclaw.model.ConnectionState
|
||||||
|
import com.thisux.droidclaw.model.GoalStatus
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HomeScreen() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val connectionState by ConnectionService.connectionState.collectAsState()
|
||||||
|
val goalStatus by ConnectionService.currentGoalStatus.collectAsState()
|
||||||
|
val steps by ConnectionService.currentSteps.collectAsState()
|
||||||
|
val currentGoal by ConnectionService.currentGoal.collectAsState()
|
||||||
|
val errorMessage by ConnectionService.errorMessage.collectAsState()
|
||||||
|
|
||||||
|
var goalInput by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
// Status Badge
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(12.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
when (connectionState) {
|
||||||
|
ConnectionState.Connected -> Color(0xFF4CAF50)
|
||||||
|
ConnectionState.Connecting -> Color(0xFFFFC107)
|
||||||
|
ConnectionState.Error -> Color(0xFFF44336)
|
||||||
|
ConnectionState.Disconnected -> Color.Gray
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = when (connectionState) {
|
||||||
|
ConnectionState.Connected -> "Connected to server"
|
||||||
|
ConnectionState.Connecting -> "Connecting..."
|
||||||
|
ConnectionState.Error -> errorMessage ?: "Connection error"
|
||||||
|
ConnectionState.Disconnected -> "Disconnected"
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
modifier = Modifier.padding(start = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Connect/Disconnect button
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val intent = Intent(context, ConnectionService::class.java).apply {
|
||||||
|
action = if (connectionState == ConnectionState.Disconnected || connectionState == ConnectionState.Error) {
|
||||||
|
ConnectionService.ACTION_CONNECT
|
||||||
|
} else {
|
||||||
|
ConnectionService.ACTION_DISCONNECT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
when (connectionState) {
|
||||||
|
ConnectionState.Disconnected, ConnectionState.Error -> "Connect"
|
||||||
|
else -> "Disconnect"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Goal Input
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = goalInput,
|
||||||
|
onValueChange = { goalInput = it },
|
||||||
|
label = { Text("Enter a goal...") },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
enabled = connectionState == ConnectionState.Connected && goalStatus != GoalStatus.Running,
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (goalInput.isNotBlank()) {
|
||||||
|
val intent = Intent(context, ConnectionService::class.java).apply {
|
||||||
|
action = ConnectionService.ACTION_SEND_GOAL
|
||||||
|
putExtra(ConnectionService.EXTRA_GOAL, goalInput)
|
||||||
|
}
|
||||||
|
context.startService(intent)
|
||||||
|
goalInput = ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = connectionState == ConnectionState.Connected
|
||||||
|
&& goalStatus != GoalStatus.Running
|
||||||
|
&& goalInput.isNotBlank()
|
||||||
|
) {
|
||||||
|
Text("Run")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentGoal.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Goal: $currentGoal",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Step Log
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
items(steps) { step ->
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
Text(
|
||||||
|
text = "Step ${step.step}: ${step.action}",
|
||||||
|
style = MaterialTheme.typography.titleSmall
|
||||||
|
)
|
||||||
|
if (step.reasoning.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = step.reasoning,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Goal Status
|
||||||
|
if (goalStatus == GoalStatus.Completed || goalStatus == GoalStatus.Failed) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = if (goalStatus == GoalStatus.Completed) {
|
||||||
|
"Goal completed (${steps.size} steps)"
|
||||||
|
} else {
|
||||||
|
"Goal failed"
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = if (goalStatus == GoalStatus.Completed) {
|
||||||
|
Color(0xFF4CAF50)
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package com.thisux.droidclaw.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.thisux.droidclaw.connection.ConnectionService
|
||||||
|
import com.thisux.droidclaw.model.GoalStatus
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LogsScreen() {
|
||||||
|
val steps by ConnectionService.currentSteps.collectAsState()
|
||||||
|
val goalStatus by ConnectionService.currentGoalStatus.collectAsState()
|
||||||
|
val currentGoal by ConnectionService.currentGoal.collectAsState()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text("Logs", style = MaterialTheme.typography.headlineMedium)
|
||||||
|
|
||||||
|
if (currentGoal.isNotEmpty()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = currentGoal,
|
||||||
|
style = MaterialTheme.typography.titleSmall
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = when (goalStatus) {
|
||||||
|
GoalStatus.Running -> "Running"
|
||||||
|
GoalStatus.Completed -> "Completed"
|
||||||
|
GoalStatus.Failed -> "Failed"
|
||||||
|
GoalStatus.Idle -> "Idle"
|
||||||
|
},
|
||||||
|
color = when (goalStatus) {
|
||||||
|
GoalStatus.Running -> Color(0xFFFFC107)
|
||||||
|
GoalStatus.Completed -> Color(0xFF4CAF50)
|
||||||
|
GoalStatus.Failed -> MaterialTheme.colorScheme.error
|
||||||
|
GoalStatus.Idle -> Color.Gray
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (steps.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "No steps recorded yet. Submit a goal to see agent activity here.",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(top = 16.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
items(steps) { step ->
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { expanded = !expanded }
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
Text(
|
||||||
|
text = "Step ${step.step}: ${step.action}",
|
||||||
|
style = MaterialTheme.typography.titleSmall
|
||||||
|
)
|
||||||
|
if (expanded && step.reasoning.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = step.reasoning,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package com.thisux.droidclaw.ui.screens
|
||||||
|
|
||||||
|
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.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.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.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
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.unit.dp
|
||||||
|
import com.thisux.droidclaw.DroidClawApp
|
||||||
|
import com.thisux.droidclaw.accessibility.DroidClawAccessibilityService
|
||||||
|
import com.thisux.droidclaw.capture.ScreenCaptureManager
|
||||||
|
import com.thisux.droidclaw.util.BatteryOptimization
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val app = context.applicationContext as DroidClawApp
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val apiKey by app.settingsStore.apiKey.collectAsState(initial = "")
|
||||||
|
val serverUrl by app.settingsStore.serverUrl.collectAsState(initial = "wss://localhost:8080")
|
||||||
|
|
||||||
|
var editingApiKey by remember(apiKey) { mutableStateOf(apiKey) }
|
||||||
|
var editingServerUrl by remember(serverUrl) { mutableStateOf(serverUrl) }
|
||||||
|
|
||||||
|
val isAccessibilityEnabled by DroidClawAccessibilityService.isRunning.collectAsState()
|
||||||
|
val isCaptureAvailable by ScreenCaptureManager.isAvailable.collectAsState()
|
||||||
|
val isBatteryExempt = remember { BatteryOptimization.isIgnoringBatteryOptimizations(context) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Text("Settings", style = MaterialTheme.typography.headlineMedium)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = editingApiKey,
|
||||||
|
onValueChange = { editingApiKey = it },
|
||||||
|
label = { Text("API Key") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
if (editingApiKey != apiKey) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { scope.launch { app.settingsStore.setApiKey(editingApiKey) } }
|
||||||
|
) {
|
||||||
|
Text("Save API Key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = editingServerUrl,
|
||||||
|
onValueChange = { editingServerUrl = it },
|
||||||
|
label = { Text("Server URL") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
if (editingServerUrl != serverUrl) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { scope.launch { app.settingsStore.setServerUrl(editingServerUrl) } }
|
||||||
|
) {
|
||||||
|
Text("Save Server URL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text("Setup Checklist", style = MaterialTheme.typography.titleMedium)
|
||||||
|
|
||||||
|
ChecklistItem(
|
||||||
|
label = "API key configured",
|
||||||
|
isOk = apiKey.isNotBlank(),
|
||||||
|
actionLabel = null,
|
||||||
|
onAction = {}
|
||||||
|
)
|
||||||
|
|
||||||
|
ChecklistItem(
|
||||||
|
label = "Accessibility service",
|
||||||
|
isOk = isAccessibilityEnabled,
|
||||||
|
actionLabel = "Enable",
|
||||||
|
onAction = { BatteryOptimization.openAccessibilitySettings(context) }
|
||||||
|
)
|
||||||
|
|
||||||
|
ChecklistItem(
|
||||||
|
label = "Screen capture permission",
|
||||||
|
isOk = isCaptureAvailable,
|
||||||
|
actionLabel = null,
|
||||||
|
onAction = {}
|
||||||
|
)
|
||||||
|
|
||||||
|
ChecklistItem(
|
||||||
|
label = "Battery optimization disabled",
|
||||||
|
isOk = isBatteryExempt,
|
||||||
|
actionLabel = "Disable",
|
||||||
|
onAction = { BatteryOptimization.requestExemption(context) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ChecklistItem(
|
||||||
|
label: String,
|
||||||
|
isOk: Boolean,
|
||||||
|
actionLabel: String?,
|
||||||
|
onAction: () -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = if (isOk) {
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isOk) Icons.Filled.CheckCircle else Icons.Filled.Error,
|
||||||
|
contentDescription = if (isOk) "OK" else "Missing",
|
||||||
|
tint = if (isOk) Color(0xFF4CAF50) else MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
Text(label)
|
||||||
|
}
|
||||||
|
if (!isOk && actionLabel != null) {
|
||||||
|
OutlinedButton(onClick = onAction) {
|
||||||
|
Text(actionLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.thisux.droidclaw.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.PowerManager
|
||||||
|
import android.provider.Settings
|
||||||
|
|
||||||
|
object BatteryOptimization {
|
||||||
|
fun isIgnoringBatteryOptimizations(context: Context): Boolean {
|
||||||
|
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
|
return pm.isIgnoringBatteryOptimizations(context.packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestExemption(context: Context) {
|
||||||
|
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
||||||
|
data = Uri.parse("package:${context.packageName}")
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openAccessibilitySettings(context: Context) {
|
||||||
|
context.startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user