11package xyz.block.trailblaze.ui.tabs.devices
22
3+ import androidx.compose.foundation.layout.Arrangement
34import androidx.compose.foundation.layout.Column
5+ import androidx.compose.foundation.layout.Row
46import androidx.compose.foundation.layout.Spacer
57import androidx.compose.foundation.layout.fillMaxWidth
68import androidx.compose.foundation.layout.height
9+ import androidx.compose.foundation.layout.size
10+ import androidx.compose.material.icons.Icons
11+ import androidx.compose.material.icons.filled.Add
12+ import androidx.compose.material.icons.filled.Delete
13+ import androidx.compose.material3.AlertDialog
14+ import androidx.compose.material3.Button
715import androidx.compose.material3.DropdownMenuItem
816import androidx.compose.material3.ExperimentalMaterial3Api
917import androidx.compose.material3.ExposedDropdownMenuBox
1018import androidx.compose.material3.ExposedDropdownMenuDefaults
19+ import androidx.compose.material3.HorizontalDivider
20+ import androidx.compose.material3.Icon
21+ import androidx.compose.material3.IconButton
22+ import androidx.compose.material3.MaterialTheme
23+ import androidx.compose.material3.OutlinedTextField
1124import androidx.compose.material3.Text
25+ import androidx.compose.material3.TextButton
1226import androidx.compose.material3.TextField
1327import androidx.compose.runtime.Composable
28+ import androidx.compose.runtime.collectAsState
1429import androidx.compose.runtime.getValue
1530import androidx.compose.runtime.mutableStateOf
1631import androidx.compose.runtime.remember
1732import androidx.compose.runtime.setValue
33+ import androidx.compose.ui.Alignment
1834import androidx.compose.ui.Modifier
1935import androidx.compose.ui.unit.dp
2036import xyz.block.trailblaze.model.TrailblazeHostAppTarget
@@ -25,6 +41,10 @@ import xyz.block.trailblaze.ui.composables.CodeBlock
2541/* *
2642 * Reusable row component for target app selection.
2743 * Used by DevicesTab for target app configuration.
44+ *
45+ * Shows registered app targets followed by user-added custom package names.
46+ * Includes an "Add Package" option at the bottom of the dropdown to let users
47+ * register new package names which are persisted in app settings.
2848 */
2949@OptIn(ExperimentalMaterial3Api ::class )
3050@Composable
@@ -36,6 +56,19 @@ fun TargetAppConfigRow(
3656 modifier : Modifier = Modifier ,
3757) {
3858 var expanded by remember { mutableStateOf(false ) }
59+ var showAddPackageDialog by remember { mutableStateOf(false ) }
60+
61+ val currentState by settingsRepo.serverStateFlow.collectAsState()
62+ val customPackageNames = currentState.appConfig.customAppPackageNames
63+
64+ // Determine display value: could be a registered target app or a custom package name
65+ val selectedCustomPackage = if (selectedTargetApp == null ) {
66+ currentState.appConfig.selectedTargetAppId?.takeIf { it in customPackageNames }
67+ } else null
68+
69+ val displayValue = selectedTargetApp?.displayName
70+ ? : selectedCustomPackage
71+ ? : " Select target app"
3972
4073 Column (modifier = modifier) {
4174 // Target App Dropdown
@@ -48,7 +81,7 @@ fun TargetAppConfigRow(
4881 ) {
4982 TextField (
5083 readOnly = true ,
51- value = selectedTargetApp?.displayName ? : " Select target app " ,
84+ value = displayValue ,
5285 onValueChange = { },
5386 label = { Text (" Target App" ) },
5487 leadingIcon = {
@@ -67,18 +100,79 @@ fun TargetAppConfigRow(
67100 expanded = false
68101 }
69102 ) {
70- deviceManager.availableAppTargets.forEach { selectedTargetApp ->
103+ // Registered app targets
104+ deviceManager.availableAppTargets.forEach { appTarget ->
71105 DropdownMenuItem (
72106 leadingIcon = {
73- deviceManager.appIconProvider.getIcon(selectedTargetApp )
107+ deviceManager.appIconProvider.getIcon(appTarget )
74108 },
75- text = { Text (selectedTargetApp .displayName) },
109+ text = { Text (appTarget .displayName) },
76110 onClick = {
77- settingsRepo.targetAppSelected(selectedTargetApp )
111+ settingsRepo.targetAppSelected(appTarget )
78112 expanded = false
79113 }
80114 )
81115 }
116+
117+ // User-added custom package names
118+ if (customPackageNames.isNotEmpty()) {
119+ HorizontalDivider ()
120+
121+ customPackageNames.forEach { packageName ->
122+ DropdownMenuItem (
123+ text = {
124+ Row (
125+ modifier = Modifier .fillMaxWidth(),
126+ horizontalArrangement = Arrangement .SpaceBetween ,
127+ verticalAlignment = Alignment .CenterVertically ,
128+ ) {
129+ Text (packageName, modifier = Modifier .weight(1f ))
130+ IconButton (
131+ onClick = {
132+ settingsRepo.removeCustomAppPackageName(packageName)
133+ },
134+ modifier = Modifier .size(24 .dp)
135+ ) {
136+ Icon (
137+ imageVector = Icons .Default .Delete ,
138+ contentDescription = " Remove $packageName " ,
139+ tint = MaterialTheme .colorScheme.error,
140+ modifier = Modifier .size(16 .dp),
141+ )
142+ }
143+ }
144+ },
145+ onClick = {
146+ settingsRepo.updateAppConfig { config ->
147+ config.copy(selectedTargetAppId = packageName)
148+ }
149+ expanded = false
150+ }
151+ )
152+ }
153+ }
154+
155+ // "Add Package" option at the bottom
156+ HorizontalDivider ()
157+ DropdownMenuItem (
158+ leadingIcon = {
159+ Icon (
160+ imageVector = Icons .Default .Add ,
161+ contentDescription = null ,
162+ tint = MaterialTheme .colorScheme.primary,
163+ )
164+ },
165+ text = {
166+ Text (
167+ " Add Package" ,
168+ color = MaterialTheme .colorScheme.primary,
169+ )
170+ },
171+ onClick = {
172+ expanded = false
173+ showAddPackageDialog = true
174+ }
175+ )
82176 }
83177 }
84178
@@ -92,4 +186,73 @@ fun TargetAppConfigRow(
92186 )
93187 }
94188 }
189+
190+ // Add Package Dialog
191+ if (showAddPackageDialog) {
192+ AddPackageDialog (
193+ onDismiss = { showAddPackageDialog = false },
194+ onAdd = { packageName ->
195+ settingsRepo.addCustomAppPackageName(packageName)
196+ // Auto-select the newly added package
197+ settingsRepo.updateAppConfig { config ->
198+ config.copy(selectedTargetAppId = packageName.trim())
199+ }
200+ showAddPackageDialog = false
201+ },
202+ existingPackages = customPackageNames,
203+ )
204+ }
205+ }
206+
207+ /* *
208+ * Dialog for adding a new custom application package name.
209+ */
210+ @Composable
211+ private fun AddPackageDialog (
212+ onDismiss : () -> Unit ,
213+ onAdd : (String ) -> Unit ,
214+ existingPackages : List <String >,
215+ ) {
216+ var packageName by remember { mutableStateOf(" " ) }
217+ val trimmed = packageName.trim()
218+ val isDuplicate = trimmed in existingPackages
219+ val isValid = trimmed.isNotEmpty() && ! isDuplicate
220+
221+ AlertDialog (
222+ onDismissRequest = onDismiss,
223+ title = { Text (" Add Application Package" ) },
224+ text = {
225+ Column (verticalArrangement = Arrangement .spacedBy(12 .dp)) {
226+ Text (
227+ text = " Enter the package name of the application you want to test." ,
228+ style = MaterialTheme .typography.bodyMedium,
229+ )
230+ OutlinedTextField (
231+ value = packageName,
232+ onValueChange = { packageName = it },
233+ label = { Text (" Package Name" ) },
234+ placeholder = { Text (" com.example.myapp" ) },
235+ modifier = Modifier .fillMaxWidth(),
236+ singleLine = true ,
237+ isError = isDuplicate,
238+ supportingText = if (isDuplicate) {
239+ { Text (" This package has already been added" ) }
240+ } else null ,
241+ )
242+ }
243+ },
244+ confirmButton = {
245+ Button (
246+ onClick = { onAdd(trimmed) },
247+ enabled = isValid,
248+ ) {
249+ Text (" Add" )
250+ }
251+ },
252+ dismissButton = {
253+ TextButton (onClick = onDismiss) {
254+ Text (" Cancel" )
255+ }
256+ },
257+ )
95258}
0 commit comments