package com.parkplants.ui.addplant import android.Manifest import android.content.Context import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items 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.* import androidx.compose.material3.* import androidx.compose.runtime.* 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.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.FileProvider import androidx.hilt.navigation.compose.hiltViewModel import coil.compose.AsyncImage import com.google.accompanist.permissions.ExperimentalPermissionsApi import androidx.compose.ui.focus.onFocusChanged import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.parkplants.ui.components.AppTextField import java.io.File import java.util.* private val Green = Color(0xFF22C55E) private val TextPrimary = Color(0xFFF1F5F9) private val TextSecondary = Color(0xFF94A3B8) private val BgPage = Color(0xFF0F172A) private val BgCard = Color(0xFF1E293B) private val BorderColor = Color(0xFF334155) @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class) @Composable fun AddPlantScreen( onSaved: () -> Unit, viewModel: AddPlantViewModel = hiltViewModel(), gpsViewModel: GpsViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsState() val gpsState by gpsViewModel.gpsState.collectAsState() val overlayState by viewModel.overlayState.collectAsState() val context = LocalContext.current // Mutable state - declared early so LaunchedEffects can reference them var selectedTypeId by remember { mutableStateOf(null) } var selectedTypeName by remember { mutableStateOf("") } var selectedYear by remember { mutableStateOf(null) } var nursery by remember { mutableStateOf("") } var photoUri by remember { mutableStateOf(null) } var photoPath by remember { mutableStateOf(null) } var showTypeDialog by remember { mutableStateOf(false) } var showNewTypeDialog by remember { mutableStateOf(false) } var newTypeName by remember { mutableStateOf("") } var typeSearch by remember { mutableStateOf("") } var nurserySearch by remember { mutableStateOf("") } var nurseryDropdownVisible by remember { mutableStateOf(false) } var showYearPicker by remember { mutableStateOf(false) } var showMapPicker by remember { mutableStateOf(false) } LaunchedEffect(uiState.saved) { if (uiState.saved) { viewModel.resetSaved() onSaved() } } LaunchedEffect(uiState.createdType) { uiState.createdType?.let { type -> selectedTypeId = type.id selectedTypeName = type.name viewModel.clearCreatedType() } } val permissionsState = rememberMultiplePermissionsState( listOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.CAMERA) ) val cameraFile = remember { createTempPhotoFile(context) } val cameraUri = remember { FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", cameraFile) } val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { ok -> if (ok) { photoUri = cameraUri; photoPath = cameraFile.absolutePath } } // Map picker dialog if (showMapPicker) { MapPickerDialog( onCoordinatesSelected = { lat, lng -> gpsViewModel.setManualLocation(lat, lng) }, onDismiss = { showMapPicker = false }, overlayState = overlayState, ) } // Year picker dialog if (showYearPicker) { YearPickerDialog( currentYear = selectedYear, onYearSelected = { year -> selectedYear = year showYearPicker = false }, onDismiss = { showYearPicker = false }, ) } // Type selector dialog if (showTypeDialog) { AlertDialog( onDismissRequest = { showTypeDialog = false; viewModel.clearPlantTypeSuggestions() }, containerColor = Color(0xFF1E293B), title = { Text("Вид растения", style = TextStyle(color = TextPrimary, fontSize = 18.sp, fontWeight = FontWeight.SemiBold)) }, text = { Column { AppTextField( value = typeSearch, onValueChange = { typeSearch = it viewModel.searchPlantTypes(it) }, placeholder = "Первые буквы названия...", ) Spacer(Modifier.height(8.dp)) if (typeSearch.isBlank()) { // Не рендерим весь список — он может быть большим Text( "Введите первые буквы названия...", style = TextStyle(color = TextSecondary, fontSize = 14.sp), modifier = Modifier.padding(vertical = 12.dp), ) } else { LazyColumn(modifier = Modifier.heightIn(max = 280.dp)) { items(uiState.plantTypeSuggestions) { type -> Row( modifier = Modifier .fillMaxWidth() .clickable { selectedTypeId = type.id selectedTypeName = type.name showTypeDialog = false typeSearch = "" viewModel.clearPlantTypeSuggestions() } .padding(vertical = 12.dp, horizontal = 4.dp), ) { Text(type.name, style = TextStyle(color = TextPrimary, fontSize = 15.sp)) } HorizontalDivider(color = BorderColor) } if (uiState.plantTypeSuggestions.isEmpty()) { item { Text( "Не найдено", style = TextStyle(color = TextSecondary, fontSize = 14.sp), modifier = Modifier.padding(vertical = 12.dp), ) } } } } TextButton(onClick = { showTypeDialog = false newTypeName = typeSearch showNewTypeDialog = true viewModel.clearPlantTypeSuggestions() }) { Icon(Icons.Default.Add, null, tint = Green, modifier = Modifier.size(18.dp)) Spacer(Modifier.width(4.dp)) Text("Добавить новый вид", style = TextStyle(color = Green, fontSize = 14.sp)) } } }, confirmButton = { TextButton(onClick = { showTypeDialog = false; viewModel.clearPlantTypeSuggestions() }) { Text("Закрыть", style = TextStyle(color = TextSecondary)) } }, ) } // New type dialog if (showNewTypeDialog) { AlertDialog( onDismissRequest = { showNewTypeDialog = false }, containerColor = Color(0xFF1E293B), title = { Text("Новый вид", style = TextStyle(color = TextPrimary, fontSize = 18.sp, fontWeight = FontWeight.SemiBold)) }, text = { AppTextField( value = newTypeName, onValueChange = { newTypeName = it }, label = "Название", placeholder = "Например: Берёза повислая", ) }, confirmButton = { TextButton( onClick = { if (newTypeName.isNotBlank()) { viewModel.createPlantType(newTypeName) showNewTypeDialog = false } }, enabled = newTypeName.isNotBlank(), ) { Text("Добавить", style = TextStyle(color = Green)) } }, dismissButton = { TextButton(onClick = { showNewTypeDialog = false }) { Text("Отмена", style = TextStyle(color = TextSecondary)) } }, ) } Scaffold( topBar = { TopAppBar( title = { Text("Добавить растение", style = TextStyle(color = TextPrimary, fontSize = 18.sp, fontWeight = FontWeight.SemiBold)) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color(0xFF1E293B)), ) }, containerColor = BgPage, ) { padding -> Column( modifier = Modifier .padding(padding) .verticalScroll(rememberScrollState()) .padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { // ── GPS ────────────────────────────────────────────────────────── FormCard { SectionTitle("📍 Координаты GPS") Spacer(Modifier.height(10.dp)) if (gpsState.lat != null) { Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Default.LocationOn, null, tint = Green, modifier = Modifier.size(20.dp)) Spacer(Modifier.width(8.dp)) Column(Modifier.weight(1f)) { Text( "${String.format("%.6f", gpsState.lat)}, ${String.format("%.6f", gpsState.lng)}", style = TextStyle(color = TextPrimary, fontSize = 14.sp, fontWeight = FontWeight.Medium), ) val accuracy = gpsState.accuracy Text( if (accuracy != null) "Точность: ±${accuracy.toInt()} м" else "Выбрано на карте", style = TextStyle( color = if (accuracy == null || accuracy < 10) Green else Color(0xFFEF4444), fontSize = 12.sp, ), ) } IconButton(onClick = { gpsViewModel.clearLocation() }) { Icon(Icons.Default.Close, null, tint = TextSecondary, modifier = Modifier.size(20.dp)) } } } else { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { GreenButton( text = if (gpsState.isLoading) "Получение..." else "Получить GPS координаты", loading = gpsState.isLoading, onClick = { if (permissionsState.allPermissionsGranted) gpsViewModel.requestLocation() else permissionsState.launchMultiplePermissionRequest() }, ) OutlinedButton( onClick = { showMapPicker = true }, modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(10.dp), border = androidx.compose.foundation.BorderStroke(1.5.dp, Green), ) { Icon(Icons.Default.LocationOn, null, tint = Green, modifier = Modifier.size(18.dp)) Spacer(Modifier.width(8.dp)) Text("Указать на карте", style = TextStyle(color = Green, fontSize = 15.sp)) } } gpsState.error?.let { Text(it, style = TextStyle(color = Color(0xFFEF4444), fontSize = 12.sp)) } } } // ── Вид растения ───────────────────────────────────────────────── FormCard { SectionTitle("🌿 Вид растения *") Spacer(Modifier.height(10.dp)) Row( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(10.dp)) .border(1.5.dp, if (selectedTypeId != null) Green else BorderColor, RoundedCornerShape(10.dp)) .clickable { showTypeDialog = true } .padding(horizontal = 14.dp, vertical = 13.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( if (selectedTypeName.isNotBlank()) selectedTypeName else "Выберите вид...", style = TextStyle( color = if (selectedTypeName.isNotBlank()) TextPrimary else Color(0xFF64748B), fontSize = 16.sp, ), modifier = Modifier.weight(1f), ) Icon(Icons.Default.ArrowDropDown, null, tint = TextSecondary) } } // ── Год посадки ────────────────────────────────────────────────── FormCard { SectionTitle("📅 Год посадки") Spacer(Modifier.height(10.dp)) Row( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(10.dp)) .border(1.5.dp, BorderColor, RoundedCornerShape(10.dp)) .clickable { showYearPicker = true } .padding(horizontal = 14.dp, vertical = 13.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( if (selectedYear != null) "$selectedYear" else "Выберите год...", style = TextStyle( color = if (selectedYear != null) TextPrimary else Color(0xFF64748B), fontSize = 16.sp, ), modifier = Modifier.weight(1f), ) if (selectedYear != null) { IconButton( onClick = { selectedYear = null }, modifier = Modifier.size(24.dp), ) { Icon(Icons.Default.Close, null, tint = TextSecondary, modifier = Modifier.size(18.dp)) } } else { Icon(Icons.Default.CalendarToday, null, tint = TextSecondary, modifier = Modifier.size(18.dp)) } } } // ── Питомник ────────────────────────────────────────────────────── FormCard { SectionTitle("🏡 Питомник") Spacer(Modifier.height(8.dp)) Column { OutlinedTextField( value = nurserySearch, onValueChange = { v -> nurserySearch = v nursery = v viewModel.searchNurseries(v) nurseryDropdownVisible = v.isNotBlank() }, modifier = Modifier .fillMaxWidth() .onFocusChanged { if (!it.isFocused) nurseryDropdownVisible = false }, placeholder = { Text("Первые буквы названия...", color = Color(0xFF64748B)) }, textStyle = TextStyle(color = TextPrimary, fontSize = 16.sp), colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = Green, unfocusedBorderColor = BorderColor, cursorColor = Green, ), singleLine = true, shape = RoundedCornerShape(10.dp), trailingIcon = if (nurserySearch.isNotEmpty()) { { IconButton(onClick = { nurserySearch = "" nursery = "" nurseryDropdownVisible = false viewModel.clearNurserySuggestions() }) { Icon(Icons.Default.Close, null, tint = TextSecondary, modifier = Modifier.size(18.dp)) } } } else null, ) if (nurseryDropdownVisible && uiState.nurserySuggestions.isNotEmpty()) { Column( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(0.dp, 0.dp, 10.dp, 10.dp)) .background(Color(0xFF0F172A)) .border(1.dp, BorderColor, RoundedCornerShape(0.dp, 0.dp, 10.dp, 10.dp)), ) { uiState.nurserySuggestions.forEach { nr -> Text( nr.name, style = TextStyle(color = TextPrimary, fontSize = 15.sp), modifier = Modifier .fillMaxWidth() .clickable { nurserySearch = nr.name nursery = nr.name nurseryDropdownVisible = false viewModel.clearNurserySuggestions() } .padding(horizontal = 16.dp, vertical = 12.dp), ) HorizontalDivider(color = BorderColor) } } } } } // ── Фото ────────────────────────────────────────────────────────── FormCard { SectionTitle("📷 Фотография") Spacer(Modifier.height(10.dp)) if (photoUri != null) { AsyncImage( model = photoUri, contentDescription = null, modifier = Modifier .fillMaxWidth() .height(180.dp) .clip(RoundedCornerShape(10.dp)), contentScale = ContentScale.Crop, ) Spacer(Modifier.height(8.dp)) TextButton(onClick = { photoUri = null; photoPath = null }) { Text("Удалить фото", style = TextStyle(color = Color(0xFFEF4444), fontSize = 14.sp)) } } else { OutlinedButton( onClick = { if (permissionsState.allPermissionsGranted) cameraLauncher.launch(cameraUri) else permissionsState.launchMultiplePermissionRequest() }, modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(10.dp), border = androidx.compose.foundation.BorderStroke(1.5.dp, BorderColor), ) { Icon(Icons.Default.CameraAlt, null, tint = TextSecondary) Spacer(Modifier.width(8.dp)) Text("Сфотографировать", style = TextStyle(color = TextSecondary, fontSize = 15.sp)) } } } // ── Ошибка ──────────────────────────────────────────────────────── uiState.error?.let { Box( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(8.dp)) .background(Color(0xFF3F1515)) .padding(12.dp), ) { Text(it, style = TextStyle(color = Color(0xFFEF4444), fontSize = 13.sp)) } } // ── Сохранить ───────────────────────────────────────────────────── val canSave = gpsState.lat != null && selectedTypeId != null GreenButton( text = "Сохранить", loading = uiState.isSaving, enabled = canSave && !uiState.isSaving, onClick = { viewModel.savePlant( plantTypeId = selectedTypeId!!, plantTypeName = selectedTypeName, lat = gpsState.lat!!, lng = gpsState.lng!!, plantedAt = selectedYear?.let { "$it-01-01" }, nursery = nursery.ifBlank { null }, photoPath = photoPath, ) }, ) Spacer(Modifier.height(16.dp)) } } } @Composable private fun YearPickerDialog( currentYear: Int?, onYearSelected: (Int) -> Unit, onDismiss: () -> Unit, ) { val years = remember { val cur = Calendar.getInstance().get(Calendar.YEAR) (cur downTo 1900).toList() } AlertDialog( onDismissRequest = onDismiss, containerColor = Color(0xFF1E293B), title = { Text("Год посадки", style = TextStyle(color = TextPrimary, fontSize = 18.sp, fontWeight = FontWeight.SemiBold)) }, text = { LazyColumn(modifier = Modifier.height(300.dp)) { items(years) { year -> val isSelected = year == currentYear Row( modifier = Modifier .fillMaxWidth() .clickable { onYearSelected(year) } .background(if (isSelected) Color(0xFF14532D) else Color.Transparent) .padding(vertical = 12.dp, horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { if (isSelected) { Icon(Icons.Default.Check, null, tint = Green, modifier = Modifier.size(16.dp)) Spacer(Modifier.width(8.dp)) } else { Spacer(Modifier.width(24.dp)) } Text( "$year", style = TextStyle( color = if (isSelected) Green else TextPrimary, fontSize = 15.sp, fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, ), ) } HorizontalDivider(color = BorderColor) } } }, confirmButton = { TextButton(onClick = onDismiss) { Text("Отмена", style = TextStyle(color = TextSecondary)) } }, ) } @Composable private fun FormCard(content: @Composable ColumnScope.() -> Unit) { Column( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(14.dp)) .background(BgCard) .padding(16.dp), content = content, ) } @Composable private fun SectionTitle(text: String) { Text(text, style = TextStyle(color = TextPrimary, fontSize = 14.sp, fontWeight = FontWeight.SemiBold)) } @Composable private fun GreenButton( text: String, onClick: () -> Unit, loading: Boolean = false, enabled: Boolean = true, ) { Button( onClick = onClick, modifier = Modifier.fillMaxWidth().height(50.dp), enabled = enabled, colors = ButtonDefaults.buttonColors( containerColor = Green, disabledContainerColor = Green.copy(alpha = 0.4f), ), shape = RoundedCornerShape(10.dp), ) { if (loading) { CircularProgressIndicator(Modifier.size(18.dp), color = Color.White, strokeWidth = 2.dp) Spacer(Modifier.width(8.dp)) } Text(text, style = TextStyle(color = Color.White, fontSize = 16.sp, fontWeight = FontWeight.SemiBold)) } } private fun createTempPhotoFile(context: Context): File { val dir = File(context.cacheDir, "photos").also { it.mkdirs() } return File(dir, "photo_${System.currentTimeMillis()}.jpg") }