|
| 1 | +/* |
| 2 | + * Copyright 2026 Google LLC |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +package com.example.common_ui.catalog.compose |
| 18 | + |
| 19 | +import androidx.compose.foundation.layout.Arrangement |
| 20 | +import androidx.compose.foundation.layout.Column |
| 21 | +import androidx.compose.foundation.layout.Row |
| 22 | +import androidx.compose.foundation.layout.Spacer |
| 23 | +import androidx.compose.foundation.layout.fillMaxWidth |
| 24 | +import androidx.compose.foundation.layout.height |
| 25 | +import androidx.compose.foundation.shape.RoundedCornerShape |
| 26 | +import androidx.compose.material3.AlertDialog |
| 27 | +import androidx.compose.material3.Button |
| 28 | +import androidx.compose.material3.ButtonDefaults |
| 29 | +import androidx.compose.material3.Icon |
| 30 | +import androidx.compose.material3.MaterialTheme |
| 31 | +import androidx.compose.material3.OutlinedButton |
| 32 | +import androidx.compose.material3.OutlinedTextField |
| 33 | +import androidx.compose.material3.Text |
| 34 | +import androidx.compose.material3.TextButton |
| 35 | +import androidx.compose.runtime.Composable |
| 36 | +import androidx.compose.runtime.getValue |
| 37 | +import androidx.compose.runtime.mutableStateOf |
| 38 | +import androidx.compose.runtime.saveable.rememberSaveable |
| 39 | +import androidx.compose.runtime.setValue |
| 40 | +import androidx.compose.ui.Modifier |
| 41 | +import androidx.compose.ui.res.painterResource |
| 42 | +import androidx.compose.ui.text.font.FontWeight |
| 43 | +import androidx.compose.ui.unit.dp |
| 44 | +import androidx.compose.ui.unit.sp |
| 45 | +import com.example.common_ui.R |
| 46 | +import com.example.common_ui.catalog.Framework |
| 47 | +import com.example.common_ui.catalog.ReviewStatus |
| 48 | +import com.example.common_ui.catalog.SampleItem |
| 49 | + |
| 50 | +/** |
| 51 | + * Host component managing the display of catalog dialogs (confirmation, detail viewer, quick grading). |
| 52 | + */ |
| 53 | +@Composable |
| 54 | +fun CatalogDialogHost( |
| 55 | + state: CatalogState, |
| 56 | + onSaveEvaluation: ((targetFqcn: String, status: ReviewStatus, notes: String, sample: SampleItem) -> Unit)?, |
| 57 | + onClearEvaluations: (() -> Unit)?, |
| 58 | + onLaunchSample: (SampleItem, Framework) -> Unit |
| 59 | +) { |
| 60 | + if (state.showClearConfirmDialog) { |
| 61 | + ClearEvaluationsDialog( |
| 62 | + onConfirm = { |
| 63 | + onClearEvaluations?.invoke() |
| 64 | + state.showClearConfirmDialog = false |
| 65 | + }, |
| 66 | + onDismiss = { state.showClearConfirmDialog = false } |
| 67 | + ) |
| 68 | + } |
| 69 | + |
| 70 | + state.activeSampleForDetail?.let { sample -> |
| 71 | + val targetFqcn = sample.getTargetFqcn(state.selectedFramework) |
| 72 | + val existingEval = state.evaluations[targetFqcn] ?: state.evaluations[sample.id] |
| 73 | + SampleDetailFullScreenDialog( |
| 74 | + sample = sample, |
| 75 | + targetFqcn = targetFqcn, |
| 76 | + framework = state.selectedFramework, |
| 77 | + isReviewerMode = state.isReviewerMode, |
| 78 | + existingEvaluation = existingEval, |
| 79 | + onDismiss = { state.activeSampleDetailId = null }, |
| 80 | + onSaveEvaluation = { status, notes -> |
| 81 | + onSaveEvaluation?.invoke(targetFqcn, status, notes, sample) |
| 82 | + state.activeSampleDetailId = null |
| 83 | + }, |
| 84 | + onLaunch = { fw -> |
| 85 | + state.activeSampleDetailId = null |
| 86 | + onLaunchSample(sample, fw) |
| 87 | + } |
| 88 | + ) |
| 89 | + } |
| 90 | + |
| 91 | + state.activeQuickGrading?.let { (sample, gradeStatus) -> |
| 92 | + val targetFqcn = sample.getTargetFqcn(state.selectedFramework) |
| 93 | + val existingEval = state.evaluations[targetFqcn] ?: state.evaluations[sample.id] |
| 94 | + |
| 95 | + QuickGradingDialog( |
| 96 | + sample = sample, |
| 97 | + targetFqcn = targetFqcn, |
| 98 | + gradeStatus = gradeStatus, |
| 99 | + initialNotes = existingEval?.notes.orEmpty(), |
| 100 | + onSave = { notes -> |
| 101 | + onSaveEvaluation?.invoke(targetFqcn, gradeStatus, notes, sample) |
| 102 | + state.activeQuickGradingSampleId = null |
| 103 | + state.activeQuickGradingStatus = null |
| 104 | + }, |
| 105 | + onSaveAndNext = { notes -> |
| 106 | + onSaveEvaluation?.invoke(targetFqcn, gradeStatus, notes, sample) |
| 107 | + state.activeQuickGradingSampleId = null |
| 108 | + state.activeQuickGradingStatus = null |
| 109 | + val nextUnchecked = state.findNextUncheckedSample(currentSampleId = sample.id) |
| 110 | + if (nextUnchecked != null) { |
| 111 | + onLaunchSample(nextUnchecked, state.selectedFramework) |
| 112 | + } |
| 113 | + }, |
| 114 | + onDismiss = { |
| 115 | + state.activeQuickGradingSampleId = null |
| 116 | + state.activeQuickGradingStatus = null |
| 117 | + } |
| 118 | + ) |
| 119 | + } |
| 120 | +} |
| 121 | + |
| 122 | +/** |
| 123 | + * Confirmation dialog for clearing / resetting all evaluations back to unchecked. |
| 124 | + */ |
| 125 | +@Composable |
| 126 | +fun ClearEvaluationsDialog( |
| 127 | + onConfirm: () -> Unit, |
| 128 | + onDismiss: () -> Unit |
| 129 | +) { |
| 130 | + AlertDialog( |
| 131 | + onDismissRequest = onDismiss, |
| 132 | + icon = { |
| 133 | + Icon( |
| 134 | + painter = painterResource(R.drawable.ic_undo), |
| 135 | + contentDescription = null, |
| 136 | + tint = MaterialTheme.colorScheme.error |
| 137 | + ) |
| 138 | + }, |
| 139 | + title = { Text("Reset All Review Evaluations?") }, |
| 140 | + text = { |
| 141 | + Text("This will reset all ratings, status marks, and reviewer notes across all Kotlin and Java samples back to Unchecked.") |
| 142 | + }, |
| 143 | + confirmButton = { |
| 144 | + Button( |
| 145 | + onClick = onConfirm, |
| 146 | + colors = ButtonDefaults.buttonColors( |
| 147 | + containerColor = MaterialTheme.colorScheme.error, |
| 148 | + contentColor = MaterialTheme.colorScheme.onError |
| 149 | + ) |
| 150 | + ) { |
| 151 | + Text("Reset All") |
| 152 | + } |
| 153 | + }, |
| 154 | + dismissButton = { |
| 155 | + TextButton(onClick = onDismiss) { |
| 156 | + Text("Cancel") |
| 157 | + } |
| 158 | + } |
| 159 | + ) |
| 160 | +} |
| 161 | + |
| 162 | +/** |
| 163 | + * Quick grading dialog for entering optional notes and saving or saving & advancing to the next sample. |
| 164 | + */ |
| 165 | +@Composable |
| 166 | +fun QuickGradingDialog( |
| 167 | + sample: SampleItem, |
| 168 | + targetFqcn: String, |
| 169 | + gradeStatus: ReviewStatus, |
| 170 | + initialNotes: String, |
| 171 | + onSave: (notes: String) -> Unit, |
| 172 | + onSaveAndNext: (notes: String) -> Unit, |
| 173 | + onDismiss: () -> Unit |
| 174 | +) { |
| 175 | + var notes by rememberSaveable { mutableStateOf(initialNotes) } |
| 176 | + |
| 177 | + AlertDialog( |
| 178 | + onDismissRequest = onDismiss, |
| 179 | + title = { |
| 180 | + Text( |
| 181 | + text = if (gradeStatus == ReviewStatus.PASSING) "👍 Good Job: ${sample.title}" else "⚠️ Something's Wrong: ${sample.title}", |
| 182 | + fontWeight = FontWeight.Bold, |
| 183 | + fontSize = 17.sp |
| 184 | + ) |
| 185 | + }, |
| 186 | + text = { |
| 187 | + Column(modifier = Modifier.fillMaxWidth()) { |
| 188 | + Text( |
| 189 | + text = "Target: ${targetFqcn.substringAfterLast('.')}", |
| 190 | + style = MaterialTheme.typography.labelSmall, |
| 191 | + color = MaterialTheme.colorScheme.outline |
| 192 | + ) |
| 193 | + Spacer(modifier = Modifier.height(10.dp)) |
| 194 | + OutlinedTextField( |
| 195 | + value = notes, |
| 196 | + onValueChange = { notes = it }, |
| 197 | + label = { Text("Notes (optional for pass, describe issues if broken)") }, |
| 198 | + modifier = Modifier.fillMaxWidth(), |
| 199 | + minLines = 3, |
| 200 | + maxLines = 5, |
| 201 | + shape = RoundedCornerShape(10.dp) |
| 202 | + ) |
| 203 | + } |
| 204 | + }, |
| 205 | + confirmButton = { |
| 206 | + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { |
| 207 | + OutlinedButton(onClick = { onSave(notes) }) { |
| 208 | + Text(if (gradeStatus == ReviewStatus.PASSING) "Save Pass 👍" else "Save Issue ⚠️") |
| 209 | + } |
| 210 | + Button( |
| 211 | + onClick = { onSaveAndNext(notes) }, |
| 212 | + colors = ButtonDefaults.buttonColors( |
| 213 | + containerColor = MaterialTheme.colorScheme.primary |
| 214 | + ) |
| 215 | + ) { |
| 216 | + Text("Save & Next ⏭️", fontWeight = FontWeight.Bold) |
| 217 | + } |
| 218 | + } |
| 219 | + }, |
| 220 | + dismissButton = { |
| 221 | + TextButton(onClick = onDismiss) { |
| 222 | + Text("Cancel") |
| 223 | + } |
| 224 | + } |
| 225 | + ) |
| 226 | +} |
0 commit comments