519 lines
25 KiB
Dart
519 lines
25 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:seedina/provider/rtdb_handler.dart';
|
|
import 'package:seedina/utils/style/gcolor.dart'; //
|
|
|
|
class EditInfo extends StatefulWidget {
|
|
const EditInfo({super.key});
|
|
|
|
@override
|
|
State<EditInfo> createState() => _EditInfoState();
|
|
}
|
|
|
|
class _EditInfoState extends State<EditInfo> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
|
|
late TextEditingController _titleController;
|
|
late TextEditingController _latinController;
|
|
late TextEditingController _waktuSiramController;
|
|
late TextEditingController _jedaSiramController;
|
|
late TextEditingController _suhuAirMinController;
|
|
late TextEditingController _suhuAirMaxController;
|
|
late TextEditingController _nutrisiMinController;
|
|
late TextEditingController _nutrisiMaxController;
|
|
late TextEditingController _suhuLingMinController;
|
|
late TextEditingController _suhuLingMaxController;
|
|
late TextEditingController _humiLingMinController;
|
|
late TextEditingController _humiLingMaxController;
|
|
|
|
bool _isSaving = false;
|
|
// bool _isLoadingForm = true; // Tidak lagi diperlukan jika Consumer menangani update
|
|
String _currentPlantSelectionInProvider = "";
|
|
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_titleController = TextEditingController();
|
|
_latinController = TextEditingController();
|
|
_waktuSiramController = TextEditingController();
|
|
_jedaSiramController = TextEditingController();
|
|
_suhuAirMinController = TextEditingController();
|
|
_suhuAirMaxController = TextEditingController();
|
|
_nutrisiMinController = TextEditingController();
|
|
_nutrisiMaxController = TextEditingController();
|
|
_suhuLingMinController = TextEditingController();
|
|
_suhuLingMaxController = TextEditingController();
|
|
_humiLingMinController = TextEditingController();
|
|
_humiLingMaxController = TextEditingController();
|
|
|
|
// Ambil data awal saat initState
|
|
// dan simpan tipe tanaman dari provider untuk deteksi perubahan nanti
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
final provider = Provider.of<HandlingProvider>(context, listen: false);
|
|
_currentPlantSelectionInProvider = provider.draftSelectedPlantForEditing;
|
|
_loadParametersFromProviderDraft(provider.draftParametersForEditingReadonly);
|
|
});
|
|
}
|
|
|
|
// Tidak lagi menggunakan didChangeDependencies untuk reload form secara otomatis
|
|
// karena bisa me-reset input pengguna. Kita akan mengandalkan Consumer.
|
|
|
|
void _loadParametersFromProviderDraft(Map<String, dynamic> params) {
|
|
if (!mounted) return;
|
|
// setState(() { _isLoadingForm = true; }); // Tidak lagi diperlukan
|
|
|
|
// Logika untuk memastikan title dan latin sesuai untuk Kustom saat load
|
|
// Provider sekarang sudah memastikan _draftParametersForEditing['title'] dan ['latin']
|
|
// sudah benar untuk mode Kustom ("Tanaman Lain", "Parameter Kustom").
|
|
_titleController.text = params['title']?.toString() ?? '';
|
|
_latinController.text = params['latin']?.toString() ?? '';
|
|
|
|
_waktuSiramController.text = params['waktu_siram']?.toString() ?? '';
|
|
_jedaSiramController.text = params['jeda_siram']?.toString() ?? '';
|
|
_suhuAirMinController.text = params['min_suhuair']?.toString() ?? '';
|
|
_suhuAirMaxController.text = params['max_suhuair']?.toString() ?? '';
|
|
_nutrisiMinController.text = params['min_tdsair']?.toString() ?? '';
|
|
_nutrisiMaxController.text = params['max_tdsair']?.toString() ?? '';
|
|
_suhuLingMinController.text = params['min_suhuling']?.toString() ?? '';
|
|
_suhuLingMaxController.text = params['max_suhuling']?.toString() ?? '';
|
|
_humiLingMinController.text = params['min_humiling']?.toString() ?? '';
|
|
_humiLingMaxController.text = params['max_humiling']?.toString() ?? '';
|
|
|
|
// if (mounted) {
|
|
// setState(() { _isLoadingForm = false; });
|
|
// }
|
|
// Cukup panggil setState di build jika menggunakan Consumer untuk trigger rebuild
|
|
}
|
|
|
|
Future<void> _submitCustomParameters() async {
|
|
if (!_formKey.currentState!.validate()) {
|
|
if(mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Harap perbaiki semua error pada form.'), backgroundColor: Colors.orangeAccent),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
if(!mounted) return;
|
|
setState(() => _isSaving = true);
|
|
|
|
final provider = Provider.of<HandlingProvider>(context, listen: false);
|
|
// Ambil default title Kustom jika input title kosong
|
|
String customTitle = _titleController.text.trim();
|
|
if (customTitle.isEmpty) {
|
|
customTitle = provider.parameters['Kustom']!['title'].toString();
|
|
}
|
|
|
|
|
|
Map<String, dynamic> parametersToSaveFromForm = {
|
|
'title': customTitle,
|
|
'latin': _latinController.text.trim(), // Latin bisa kosong
|
|
'waktu_siram': int.tryParse(_waktuSiramController.text) ?? provider.parameters['Kustom']!['waktu_siram'],
|
|
'jeda_siram': int.tryParse(_jedaSiramController.text) ?? provider.parameters['Kustom']!['jeda_siram'],
|
|
'min_suhuair': double.tryParse(_suhuAirMinController.text) ?? provider.parameters['Kustom']!['min_suhuair'],
|
|
'max_suhuair': double.tryParse(_suhuAirMaxController.text) ?? provider.parameters['Kustom']!['max_suhuair'],
|
|
'min_tdsair': int.tryParse(_nutrisiMinController.text) ?? provider.parameters['Kustom']!['min_tdsair'],
|
|
'max_tdsair': int.tryParse(_nutrisiMaxController.text) ?? provider.parameters['Kustom']!['max_tdsair'],
|
|
'min_suhuling': double.tryParse(_suhuLingMinController.text) ?? provider.parameters['Kustom']!['min_suhuling'],
|
|
'max_suhuling': double.tryParse(_suhuLingMaxController.text) ?? provider.parameters['Kustom']!['max_suhuling'],
|
|
'min_humiling': int.tryParse(_humiLingMinController.text) ?? provider.parameters['Kustom']!['min_humiling'],
|
|
'max_humiling': int.tryParse(_humiLingMaxController.text) ?? provider.parameters['Kustom']!['max_humiling'],
|
|
// Thumbnail akan diurus oleh provider.applyCustomDraftToActive
|
|
};
|
|
|
|
// ignore: unused_local_variable
|
|
bool success = await provider.applyCustomDraftToActive(context, parametersToSaveFromForm);
|
|
// Pesan sukses/gagal sudah dihandle provider
|
|
|
|
if (mounted) {
|
|
setState(() => _isSaving = false);
|
|
// Jika sukses, dan kita ingin form merefleksikan data yang baru disimpan (terutama jika title berubah)
|
|
// Kita bisa panggil _loadParametersFromProviderDraft lagi dengan data terbaru dari provider.
|
|
// Provider akan memanggil notifyListeners setelah applyCustomDraftToActive,
|
|
// jadi Consumer di bawah akan otomatis memanggil _load...
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_titleController.dispose();
|
|
_latinController.dispose();
|
|
_waktuSiramController.dispose();
|
|
_jedaSiramController.dispose();
|
|
_suhuAirMinController.dispose();
|
|
_suhuAirMaxController.dispose();
|
|
_nutrisiMinController.dispose();
|
|
_nutrisiMaxController.dispose();
|
|
_suhuLingMinController.dispose();
|
|
_suhuLingMaxController.dispose();
|
|
_humiLingMinController.dispose();
|
|
_humiLingMaxController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
String? _validateRequired(String? value, String fieldName) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
// Khusus untuk nama tanaman kustom, tidak wajib diisi (akan default ke "Tanaman Lain")
|
|
// Validasi nama preset ada di TextFormField langsung.
|
|
// if (fieldName.toLowerCase().contains("nama tanaman")) return null;
|
|
// Untuk field lain tetap wajib:
|
|
// return '$fieldName wajib diisi';
|
|
}
|
|
// Untuk nama tanaman, cek apakah sama dengan nama preset (kecuali "Kustom")
|
|
if (fieldName.toLowerCase().contains("nama tanaman") && value != null && value.trim().isNotEmpty) {
|
|
final p = Provider.of<HandlingProvider>(context, listen: false);
|
|
// Cek apakah nama yang dimasukkan adalah salah satu key preset (case insensitive)
|
|
// dan BUKAN merupakan nama default untuk Kustom itu sendiri ("Tanaman Lain")
|
|
// atau nama latin default Kustom.
|
|
String lowerValue = value.trim().toLowerCase();
|
|
bool isPresetName = p.parameters.keys.any((key) {
|
|
return key != "Kustom" && key.toLowerCase() == lowerValue;
|
|
});
|
|
if (isPresetName) {
|
|
return "'$value' adalah nama preset, tidak bisa digunakan untuk Kustom.";
|
|
}
|
|
}
|
|
// Untuk field selain nama tanaman, jika wajib:
|
|
if (!fieldName.toLowerCase().contains("nama tanaman") && (value == null || value.trim().isEmpty)) {
|
|
return '$fieldName wajib diisi';
|
|
}
|
|
|
|
|
|
return null;
|
|
}
|
|
|
|
String? _validateInt(String? value, String fieldName, {int? minValAbs, int? maxValAbs, bool allowZero = true}) {
|
|
String? requiredError = _validateRequired(value, fieldName);
|
|
if (requiredError != null) return requiredError;
|
|
final val = int.tryParse(value!);
|
|
if (val == null) return '$fieldName harus berupa angka bulat';
|
|
if (!allowZero && val == 0) return '$fieldName tidak boleh nol';
|
|
if (val < 0 && !allowZero && (minValAbs == null || minValAbs >=0) ) return '$fieldName tidak boleh negatif';
|
|
if (minValAbs != null && val < minValAbs) return '$fieldName minimal $minValAbs';
|
|
if (maxValAbs != null && val > maxValAbs) return '$fieldName maksimal $maxValAbs';
|
|
return null;
|
|
}
|
|
|
|
String? _validateDouble(String? value, String fieldName, {double? minValAbs, double? maxValAbs}) {
|
|
String? requiredError = _validateRequired(value, fieldName);
|
|
if (requiredError != null) return requiredError;
|
|
final val = double.tryParse(value!);
|
|
if (val == null) return '$fieldName harus berupa angka desimal';
|
|
if (fieldName.toLowerCase().contains("kelembapan") && (val < 0.0 || val > 100.0)) return 'Kelembapan harus antara 0.0 - 100.0';
|
|
if (minValAbs != null && val < minValAbs) return '$fieldName minimal $minValAbs';
|
|
if (maxValAbs != null && val > maxValAbs) return '$fieldName maksimal $maxValAbs';
|
|
return null;
|
|
}
|
|
|
|
Widget _buildSingleTextFormField({
|
|
required String label,
|
|
required TextEditingController controller,
|
|
required String unit,
|
|
bool isDecimal = false,
|
|
num? absoluteMin,
|
|
num? absoluteMax,
|
|
bool allowZeroForInt = true,
|
|
}) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
|
child: TextFormField(
|
|
controller: controller,
|
|
keyboardType: isDecimal
|
|
? const TextInputType.numberWithOptions(decimal: true, signed: true)
|
|
: TextInputType.number,
|
|
inputFormatters: isDecimal
|
|
? [FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*'))]
|
|
: [FilteringTextInputFormatter.digitsOnly],
|
|
decoration: InputDecoration(
|
|
labelText: label,
|
|
suffixText: unit,
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10.0)),
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
|
isDense: true,
|
|
helperText: isDecimal ? "Gunakan '.' untuk desimal" : "Angka bulat",
|
|
helperStyle: TextStyle(fontSize: 11, color: Colors.grey.shade600)
|
|
),
|
|
validator: (value) {
|
|
return isDecimal
|
|
? _validateDouble(value, label, minValAbs: absoluteMin?.toDouble(), maxValAbs: absoluteMax?.toDouble())
|
|
: _validateInt(value, label, minValAbs: absoluteMin?.toInt(), maxValAbs: absoluteMax?.toInt(), allowZero: allowZeroForInt);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMinMaxTextFormFieldRow({
|
|
required String label,
|
|
required TextEditingController minController,
|
|
required TextEditingController maxController,
|
|
required String unit,
|
|
bool isDecimal = false,
|
|
num? absoluteMin,
|
|
num? absoluteMax,
|
|
}) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(label, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextFormField(
|
|
controller: minController,
|
|
keyboardType: isDecimal
|
|
? const TextInputType.numberWithOptions(decimal: true, signed: true)
|
|
: TextInputType.number,
|
|
inputFormatters: isDecimal
|
|
? [FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*'))]
|
|
: [FilteringTextInputFormatter.digitsOnly],
|
|
decoration: InputDecoration(
|
|
labelText: "Min",
|
|
hintText: "Nilai Min",
|
|
suffixText: unit,
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
|
),
|
|
validator: (value) {
|
|
String? commonError = isDecimal
|
|
? _validateDouble(value, "Min $label", minValAbs: absoluteMin?.toDouble(), maxValAbs: absoluteMax?.toDouble())
|
|
: _validateInt(value, "Min $label", minValAbs: absoluteMin?.toInt(), maxValAbs: absoluteMax?.toInt());
|
|
if (commonError != null) return commonError;
|
|
final maxStr = maxController.text;
|
|
if (maxStr.isNotEmpty && value != null && value.isNotEmpty) {
|
|
final currentMin = isDecimal ? double.tryParse(value) : int.tryParse(value);
|
|
final currentMax = isDecimal ? double.tryParse(maxStr) : int.tryParse(maxStr);
|
|
if (currentMin != null && currentMax != null && currentMin > currentMax) {
|
|
return 'Min harus <= Maks';
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: TextFormField(
|
|
controller: maxController,
|
|
keyboardType: isDecimal
|
|
? const TextInputType.numberWithOptions(decimal: true, signed: true)
|
|
: TextInputType.number,
|
|
inputFormatters: isDecimal
|
|
? [FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*'))]
|
|
: [FilteringTextInputFormatter.digitsOnly],
|
|
decoration: InputDecoration(
|
|
labelText: "Maks",
|
|
hintText: "Nilai Maks",
|
|
suffixText: unit,
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
|
),
|
|
validator: (value) {
|
|
String? commonError = isDecimal
|
|
? _validateDouble(value, "Maks $label", minValAbs: absoluteMin?.toDouble(), maxValAbs: absoluteMax?.toDouble())
|
|
: _validateInt(value, "Maks $label", minValAbs: absoluteMin?.toInt(), maxValAbs: absoluteMax?.toInt());
|
|
if (commonError != null) return commonError;
|
|
final minStr = minController.text;
|
|
if (minStr.isNotEmpty && value != null && value.isNotEmpty) {
|
|
final currentMin = isDecimal ? double.tryParse(minStr) : int.tryParse(minStr);
|
|
final currentMax = isDecimal ? double.tryParse(value) : int.tryParse(value);
|
|
if (currentMin != null && currentMax != null && currentMin > currentMax) {
|
|
return 'Maks harus >= Min';
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (isDecimal) Padding(
|
|
padding: const EdgeInsets.only(top: 4.0),
|
|
child: Text("Gunakan '.' untuk desimal", style: TextStyle(fontSize: 11, color: Colors.grey.shade600)),
|
|
) else Padding(
|
|
padding: const EdgeInsets.only(top: 4.0),
|
|
child: Text("Angka bulat", style: TextStyle(fontSize: 11, color: Colors.grey.shade600)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionTitle(String title) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 16.0, bottom: 4.0),
|
|
child: Text(title, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: GColors.myBiru)), //
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Gunakan Consumer untuk mendapatkan data draft terbaru dan re-load form jika perlu
|
|
return Consumer<HandlingProvider>(
|
|
builder: (context, provider, child) {
|
|
// Cek apakah tipe tanaman di provider berubah sejak widget ini dibangun
|
|
// Jika berubah (misal dari preset ke Kustom atau sebaliknya karena interaksi di luar widget ini),
|
|
// maka kita perlu me-reload controller dengan data draft baru.
|
|
if (_currentPlantSelectionInProvider != provider.draftSelectedPlantForEditing) {
|
|
_currentPlantSelectionInProvider = provider.draftSelectedPlantForEditing;
|
|
// Panggil _loadParametersFromProviderDraft di post frame callback agar tidak error
|
|
// saat build sedang berlangsung.
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_loadParametersFromProviderDraft(provider.draftParametersForEditingReadonly);
|
|
});
|
|
}
|
|
|
|
final plantInfoForImage = provider.draftPlantInfoForEditingPage;
|
|
|
|
return Container(
|
|
width: MediaQuery.of(context).size.width,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(16),
|
|
color: Colors.white,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: GColors.shadowColor.withOpacity(0.15), //
|
|
blurRadius: 6,
|
|
spreadRadius: 2,
|
|
offset: const Offset(0, 3))
|
|
]),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: provider.isLoading && provider.draftParametersForEditingReadonly.isEmpty
|
|
? Center(child: CircularProgressIndicator())
|
|
: SingleChildScrollView(
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 80, height: 80,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
image: DecorationImage(
|
|
image: AssetImage(plantInfoForImage['thumbnail'] ?? 'assets/myicon/unknown.png'), //
|
|
fit: BoxFit.cover),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
TextFormField(
|
|
controller: _titleController,
|
|
decoration: const InputDecoration(
|
|
labelText: "Nama Tanaman Kustom",
|
|
border: UnderlineInputBorder(),
|
|
floatingLabelBehavior: FloatingLabelBehavior.auto, // Biar label naik
|
|
hintText: "Contoh: Cabe Rawit Saya"
|
|
),
|
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
|
|
validator: (value) {
|
|
return _validateRequired(value, "Nama Tanaman Kustom");
|
|
}
|
|
),
|
|
TextFormField(
|
|
controller: _latinController,
|
|
decoration: const InputDecoration(
|
|
labelText: "Nama Latin/Deskripsi (Opsional)",
|
|
border: UnderlineInputBorder(),
|
|
floatingLabelBehavior: FloatingLabelBehavior.auto,
|
|
hintText: "Contoh: Capsicum frutescens"
|
|
),
|
|
style: const TextStyle(fontSize: 12, fontStyle: FontStyle.italic),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
],
|
|
),
|
|
const Divider(height: 32, thickness: 1),
|
|
|
|
_buildSectionTitle("Pengaturan Penyiraman"),
|
|
_buildSingleTextFormField(
|
|
label: "Waktu Siram",
|
|
controller: _waktuSiramController,
|
|
unit: "Menit",
|
|
isDecimal: false,
|
|
absoluteMin: 1,
|
|
allowZeroForInt: false,
|
|
),
|
|
_buildSingleTextFormField(
|
|
label: "Jeda Siram",
|
|
controller: _jedaSiramController,
|
|
unit: "Menit",
|
|
isDecimal: false,
|
|
absoluteMin: 1,
|
|
allowZeroForInt: false,
|
|
),
|
|
|
|
_buildSectionTitle("Parameter Air"),
|
|
_buildMinMaxTextFormFieldRow(
|
|
label: "Nutrisi (TDS)",
|
|
minController: _nutrisiMinController,
|
|
maxController: _nutrisiMaxController,
|
|
unit: "ppm",
|
|
isDecimal: false,
|
|
absoluteMin: 0),
|
|
_buildMinMaxTextFormFieldRow(
|
|
label: "Suhu Air",
|
|
minController: _suhuAirMinController,
|
|
maxController: _suhuAirMaxController,
|
|
unit: "°C",
|
|
isDecimal: true),
|
|
|
|
_buildSectionTitle("Parameter Lingkungan"),
|
|
_buildMinMaxTextFormFieldRow(
|
|
label: "Suhu Lingkungan",
|
|
minController: _suhuLingMinController,
|
|
maxController: _suhuLingMaxController,
|
|
unit: "°C",
|
|
isDecimal: true),
|
|
_buildMinMaxTextFormFieldRow(
|
|
label: "Kelembapan Udara",
|
|
minController: _humiLingMinController,
|
|
maxController: _humiLingMaxController,
|
|
unit: "%",
|
|
isDecimal: false,
|
|
absoluteMin: 0,
|
|
absoluteMax: 100
|
|
),
|
|
const SizedBox(height: 24),
|
|
ElevatedButton(
|
|
onPressed: _isSaving ? null : _submitCustomParameters,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: GColors.myBiru, //
|
|
foregroundColor: Colors.white,
|
|
minimumSize: const Size(double.infinity, 48),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12))),
|
|
child: _isSaving
|
|
? const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5))
|
|
: const Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.save_alt_outlined),
|
|
SizedBox(width: 8),
|
|
Text('Simpan Parameter Kustom', style: TextStyle(fontSize: 16))
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
);
|
|
}
|
|
} |