From 9c64c54520593d0f3e7290ff119d47445bfb1ed1 Mon Sep 17 00:00:00 2001 From: Sumit6307 Date: Wed, 28 Jan 2026 02:09:11 +0530 Subject: [PATCH] feat: complete UI config tools Adds file picker + live color preview for plugin config fields, job filtering UI, and bulk plugin operations. --- .../example/printer/ui/ConfigurationField.kt | 57 +++- .../example/printer/ui/JobManagementScreen.kt | 279 +++++++++++++++++- .../printer/ui/PluginManagementScreen.kt | 109 ++++++- 3 files changed, 428 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/example/printer/ui/ConfigurationField.kt b/app/src/main/java/com/example/printer/ui/ConfigurationField.kt index 45b94bd..c6e1b00 100644 --- a/app/src/main/java/com/example/printer/ui/ConfigurationField.kt +++ b/app/src/main/java/com/example/printer/ui/ConfigurationField.kt @@ -1,5 +1,8 @@ package com.example.printer.ui +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -10,6 +13,8 @@ import androidx.compose.material3.* import androidx.compose.runtime.* 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.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp @@ -157,6 +162,15 @@ fun ConfigurationField( } FieldType.FILE -> { + val filePickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent() + ) { uri: Uri? -> + uri?.let { + val filePath = it.toString() + onValueChange(filePath) + } + } + OutlinedTextField( value = currentValue?.toString() ?: field.defaultValue?.toString() ?: "", onValueChange = { onValueChange(it) }, @@ -166,7 +180,9 @@ fun ConfigurationField( }, trailingIcon = { TextButton( - onClick = { /* TODO: File picker */ } + onClick = { + filePickerLauncher.launch("*/*") + } ) { Text("Browse") } @@ -175,14 +191,31 @@ fun ConfigurationField( } FieldType.COLOR -> { + val colorString = currentValue?.toString() ?: field.defaultValue?.toString() ?: "#000000" + val parsedColor = remember(colorString) { + try { + parseHexColor(colorString) + } catch (e: Exception) { + Color.Gray + } + } + Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically ) { OutlinedTextField( - value = currentValue?.toString() ?: field.defaultValue?.toString() ?: "#000000", - onValueChange = { onValueChange(it) }, + value = colorString, + onValueChange = { + val trimmed = it.trim() + if (trimmed.startsWith("#") || trimmed.matches(Regex("^[0-9A-Fa-f]{6}$"))) { + val formatted = if (trimmed.startsWith("#")) trimmed else "#$trimmed" + onValueChange(formatted) + } else if (trimmed.isEmpty()) { + onValueChange("#000000") + } + }, modifier = Modifier.weight(1f), placeholder = { Text("#RRGGBB") @@ -192,7 +225,7 @@ fun ConfigurationField( modifier = Modifier .size(40.dp) .background( - color = androidx.compose.ui.graphics.Color.Gray, // TODO: Parse color + color = parsedColor, shape = RoundedCornerShape(8.dp) ) ) @@ -200,4 +233,20 @@ fun ConfigurationField( } } } +} + +/** + * Parses a hex color string (e.g., "#RRGGBB" or "RRGGBB") into a Color object + */ +private fun parseHexColor(hex: String): Color { + val cleanHex = hex.trim().removePrefix("#") + if (cleanHex.length != 6) { + throw IllegalArgumentException("Invalid hex color format: $hex") + } + + val r = cleanHex.substring(0, 2).toInt(16) + val g = cleanHex.substring(2, 4).toInt(16) + val b = cleanHex.substring(4, 6).toInt(16) + + return Color(r, g, b) } \ No newline at end of file diff --git a/app/src/main/java/com/example/printer/ui/JobManagementScreen.kt b/app/src/main/java/com/example/printer/ui/JobManagementScreen.kt index 4e09328..70495f2 100644 --- a/app/src/main/java/com/example/printer/ui/JobManagementScreen.kt +++ b/app/src/main/java/com/example/printer/ui/JobManagementScreen.kt @@ -53,6 +53,20 @@ fun JobManagementScreen( var showSimulationConfig by remember { mutableStateOf(false) } var selectedJobForAction by remember { mutableStateOf(null) } var showActionDialog by remember { mutableStateOf(false) } + var showFilterDialog by remember { mutableStateOf(false) } + var filterState by remember { mutableStateOf(JobFilterState()) } + + // Filter jobs based on filter state + val filteredJobs = remember(jobs, filterState) { + jobs.filter { job -> + val matchesState = filterState.selectedStates.isEmpty() || filterState.selectedStates.contains(job.state) + val matchesFormat = filterState.selectedFormat == null || job.documentFormat.contains(filterState.selectedFormat!!, ignoreCase = true) + val matchesUser = filterState.userFilter.isEmpty() || job.jobOriginatingUserName.contains(filterState.userFilter, ignoreCase = true) + val matchesName = filterState.nameFilter.isEmpty() || job.getReadableName().contains(filterState.nameFilter, ignoreCase = true) + + matchesState && matchesFormat && matchesUser && matchesName + } + } Scaffold( topBar = { @@ -115,20 +129,64 @@ fun JobManagementScreen( fontWeight = FontWeight.Bold ) - // Filter/Sort options could go here - IconButton(onClick = { /* TODO: Add filtering */ }) { - Icon(Icons.Default.Settings, "Filter") + // Filter/Sort options + IconButton(onClick = { showFilterDialog = true }) { + Icon( + Icons.Default.FilterList, + "Filter", + tint = if (filterState.hasActiveFilters()) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface + } + ) } } } // Job List - if (jobs.isEmpty()) { + if (filteredJobs.isEmpty()) { item { - EmptyJobsCard() + if (jobs.isEmpty()) { + EmptyJobsCard() + } else { + Card( + modifier = Modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(32.dp), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + Icons.Default.FilterList, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.outline + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "No jobs match the current filters", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.outline + ) + Text( + text = "Try adjusting your filter criteria", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline + ) + } + } + } + } } } else { - items(jobs) { job -> + items(filteredJobs) { job -> JobCard( job = job, onJobClick = { @@ -202,6 +260,40 @@ fun JobManagementScreen( onDismiss = { showSimulationConfig = false } ) } + + // Filter Dialog + if (showFilterDialog) { + JobFilterDialog( + filterState = filterState, + availableFormats = jobs.map { it.documentFormat }.distinct(), + onDismiss = { showFilterDialog = false }, + onApplyFilters = { newState -> + filterState = newState + showFilterDialog = false + }, + onClearFilters = { + filterState = JobFilterState() + showFilterDialog = false + } + ) + } +} + +/** + * State class for job filtering + */ +data class JobFilterState( + val selectedStates: Set = emptySet(), + val selectedFormat: String? = null, + val userFilter: String = "", + val nameFilter: String = "" +) { + fun hasActiveFilters(): Boolean { + return selectedStates.isNotEmpty() || + selectedFormat != null || + userFilter.isNotEmpty() || + nameFilter.isNotEmpty() + } } @Composable @@ -831,4 +923,177 @@ fun SimulationConfigDialog( private fun formatTimestamp(timestamp: Long): String { return SimpleDateFormat("MMM dd, HH:mm", Locale.getDefault()).format(Date(timestamp)) -} \ No newline at end of file +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun JobFilterDialog( + filterState: JobFilterState, + availableFormats: List, + onDismiss: () -> Unit, + onApplyFilters: (JobFilterState) -> Unit, + onClearFilters: () -> Unit +) { + var selectedStates by remember { mutableStateOf(filterState.selectedStates.toMutableSet()) } + var selectedFormat by remember { mutableStateOf(filterState.selectedFormat) } + var userFilter by remember { mutableStateOf(filterState.userFilter) } + var nameFilter by remember { mutableStateOf(filterState.nameFilter) } + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text("Filter Jobs") + }, + text = { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .height(400.dp) + ) { + // State filter + Text( + text = "Job State:", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 8.dp) + ) + PrintJobState.values().forEach { state -> + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = selectedStates.contains(state), + onCheckedChange = { checked -> + if (checked) { + selectedStates.add(state) + } else { + selectedStates.remove(state) + } + } + ) + Text( + text = when (state) { + PrintJobState.PENDING -> "Pending" + PrintJobState.HELD -> "Held" + PrintJobState.PROCESSING -> "Processing" + PrintJobState.STOPPED -> "Stopped" + PrintJobState.CANCELED -> "Canceled" + PrintJobState.ABORTED -> "Aborted" + PrintJobState.COMPLETED -> "Completed" + }, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Format filter + Text( + text = "Document Format:", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 8.dp) + ) + var formatExpanded by remember { mutableStateOf(false) } + ExposedDropdownMenuBox( + expanded = formatExpanded, + onExpandedChange = { formatExpanded = it } + ) { + OutlinedTextField( + value = selectedFormat ?: "All Formats", + onValueChange = { }, + modifier = Modifier + .fillMaxWidth() + .menuAnchor(), + readOnly = true, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = formatExpanded) + } + ) + ExposedDropdownMenu( + expanded = formatExpanded, + onDismissRequest = { formatExpanded = false } + ) { + DropdownMenuItem( + text = { Text("All Formats") }, + onClick = { + selectedFormat = null + formatExpanded = false + } + ) + availableFormats.forEach { format -> + DropdownMenuItem( + text = { Text(format) }, + onClick = { + selectedFormat = format + formatExpanded = false + } + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // User filter + Text( + text = "User Name:", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 8.dp) + ) + OutlinedTextField( + value = userFilter, + onValueChange = { userFilter = it }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("Filter by user name...") } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Name filter + Text( + text = "Job Name:", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 8.dp) + ) + OutlinedTextField( + value = nameFilter, + onValueChange = { nameFilter = it }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("Filter by job name...") } + ) + } + }, + confirmButton = { + Row { + TextButton(onClick = onClearFilters) { + Text("Clear") + } + TextButton( + onClick = { + onApplyFilters( + JobFilterState( + selectedStates = selectedStates, + selectedFormat = selectedFormat, + userFilter = userFilter, + nameFilter = nameFilter + ) + ) + } + ) { + Text("Apply") + } + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + } + ) +} diff --git a/app/src/main/java/com/example/printer/ui/PluginManagementScreen.kt b/app/src/main/java/com/example/printer/ui/PluginManagementScreen.kt index 706c8f6..a0fb14d 100644 --- a/app/src/main/java/com/example/printer/ui/PluginManagementScreen.kt +++ b/app/src/main/java/com/example/printer/ui/PluginManagementScreen.kt @@ -51,6 +51,8 @@ fun PluginManagementScreen( var showPluginDetails by remember { mutableStateOf(false) } var showConfigDialog by remember { mutableStateOf(false) } var pluginToConfig by remember { mutableStateOf(null) } + var showBulkOperationsMenu by remember { mutableStateOf(false) } + var selectedPluginsForBulk by remember { mutableStateOf>(emptySet()) } Scaffold( topBar = { @@ -152,9 +154,76 @@ fun PluginManagementScreen( modifier = Modifier.padding(top = if (loadedPluginIds.isNotEmpty()) 16.dp else 0.dp) ) - // Bulk actions could go here - IconButton(onClick = { /* TODO: Bulk operations */ }) { - Icon(Icons.Default.MoreVert, "More Options") + // Bulk actions + Box { + IconButton(onClick = { showBulkOperationsMenu = true }) { + Icon(Icons.Default.MoreVert, "More Options") + } + DropdownMenu( + expanded = showBulkOperationsMenu, + onDismissRequest = { showBulkOperationsMenu = false } + ) { + DropdownMenuItem( + text = { Text("Select All") }, + onClick = { + selectedPluginsForBulk = availablePlugins.map { it.id }.toSet() + showBulkOperationsMenu = false + } + ) + DropdownMenuItem( + text = { Text("Deselect All") }, + onClick = { + selectedPluginsForBulk = emptySet() + showBulkOperationsMenu = false + } + ) + DropdownMenuItem( + text = { Text("Load Selected (${selectedPluginsForBulk.size})") }, + onClick = { + coroutineScope.launch { + selectedPluginsForBulk.forEach { pluginId -> + val plugin = availablePlugins.find { it.id == pluginId } + if (plugin != null && plugin.id !in loadedPluginIds) { + pluginFramework.loadPlugin(pluginId) + logger.i(LogCategory.USER_ACTION, "PluginManagementScreen", + "Bulk loaded plugin: ${plugin.name}") + } + } + android.widget.Toast.makeText( + context, + "Loaded ${selectedPluginsForBulk.size} plugin(s)", + android.widget.Toast.LENGTH_SHORT + ).show() + selectedPluginsForBulk = emptySet() + } + showBulkOperationsMenu = false + }, + enabled = selectedPluginsForBulk.isNotEmpty() + ) + DropdownMenuItem( + text = { Text("Unload Selected (${selectedPluginsForBulk.size})") }, + onClick = { + coroutineScope.launch { + selectedPluginsForBulk.forEach { pluginId -> + val plugin = availablePlugins.find { it.id == pluginId } + if (plugin != null && plugin.id in loadedPluginIds) { + pluginFramework.unloadPlugin(pluginId) + logger.i(LogCategory.USER_ACTION, "PluginManagementScreen", + "Bulk unloaded plugin: ${plugin.name}") + } + } + android.widget.Toast.makeText( + context, + "Unloaded ${selectedPluginsForBulk.size} plugin(s)", + android.widget.Toast.LENGTH_SHORT + ).show() + selectedPluginsForBulk = emptySet() + } + showBulkOperationsMenu = false + }, + enabled = selectedPluginsForBulk.isNotEmpty() + ) + } } } } @@ -170,6 +239,14 @@ fun PluginManagementScreen( PluginCard( plugin = plugin, isLoaded = plugin.id in loadedPluginIds, + isSelected = plugin.id in selectedPluginsForBulk, + onToggleSelection = { pluginId -> + selectedPluginsForBulk = if (pluginId in selectedPluginsForBulk) { + selectedPluginsForBulk - pluginId + } else { + selectedPluginsForBulk + pluginId + } + }, onTogglePlugin = { metadata -> coroutineScope.launch { if (metadata.id in loadedPluginIds) { @@ -322,12 +399,25 @@ fun OverviewStatItem(label: String, value: String, icon: androidx.compose.ui.gra fun PluginCard( plugin: PluginMetadata, isLoaded: Boolean, + isSelected: Boolean = false, + onToggleSelection: (String) -> Unit = {}, onTogglePlugin: (PluginMetadata) -> Unit, onShowDetails: (PluginMetadata) -> Unit, onConfigurePlugin: (PluginMetadata) -> Unit ) { Card( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .then( + if (isSelected) { + Modifier.background( + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f), + RoundedCornerShape(8.dp) + ) + } else { + Modifier + } + ), elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Column( @@ -340,9 +430,16 @@ fun PluginCard( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Column( - modifier = Modifier.weight(1f) + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically ) { + Checkbox( + checked = isSelected, + onCheckedChange = { onToggleSelection(plugin.id) }, + modifier = Modifier.padding(end = 8.dp) + ) + Column { Text( text = plugin.name, style = MaterialTheme.typography.titleMedium,