481 lines
18 KiB
Dart
481 lines
18 KiB
Dart
|
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:flutter/services.dart';
|
||
|
|
import 'package:intl/intl.dart';
|
||
|
|
import 'package:geolocator/geolocator.dart';
|
||
|
|
import 'package:geocoding/geocoding.dart';
|
||
|
|
import 'comp_6-7.dart';
|
||
|
|
import 'global_data.dart';
|
||
|
|
|
||
|
|
// Formattatore per inserire automaticamente gli slash nella data
|
||
|
|
class DateInputFormatter extends TextInputFormatter {
|
||
|
|
@override
|
||
|
|
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
|
||
|
|
var text = newValue.text.replaceAll('/', '');
|
||
|
|
if (text.length > 8) text = text.substring(0, 8);
|
||
|
|
var result = "";
|
||
|
|
for (int i = 0; i < text.length; i++) {
|
||
|
|
if (i == 2 || i == 4) result += "/";
|
||
|
|
result += text[i];
|
||
|
|
}
|
||
|
|
return newValue.copyWith(text: result, selection: TextSelection.collapsed(offset: result.length));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class Comp1_5Screen extends StatefulWidget {
|
||
|
|
const Comp1_5Screen({super.key});
|
||
|
|
|
||
|
|
@override
|
||
|
|
_Comp1_5ScreenState createState() => _Comp1_5ScreenState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _Comp1_5ScreenState extends State<Comp1_5Screen> {
|
||
|
|
late TextEditingController _data, _ora, _luogo, _testimoni;
|
||
|
|
late bool _feriti, _danniVeicoli, _danniCose;
|
||
|
|
|
||
|
|
final bool isB = GlobalData.latoCorrente == 'B';
|
||
|
|
|
||
|
|
bool _isReady = false; // Per l'orientamento
|
||
|
|
bool _gpsLoading = false; // Per mostrare lo spinner
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
_forzaVerticaleEInizializza();
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _forzaVerticaleEInizializza() async {
|
||
|
|
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||
|
|
await Future.delayed(const Duration(milliseconds: 150));
|
||
|
|
|
||
|
|
_initControllers();
|
||
|
|
|
||
|
|
if (mounted) {
|
||
|
|
setState(() => _isReady = true);
|
||
|
|
|
||
|
|
// MOSTRA IL POPUP INFORMATIVO APPENA LA SCHERMATA È CARICATA
|
||
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
|
|
_mostraInfoPopup(context);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Se il luogo è vuoto, provo il GPS automatico in background
|
||
|
|
if (_luogo.text.isEmpty) {
|
||
|
|
_trovaPosizione(silenzioso: true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void _initControllers() {
|
||
|
|
_data = TextEditingController(text: GlobalData.data_incidente);
|
||
|
|
if (_data.text.isEmpty) {
|
||
|
|
_data.text = DateFormat('dd/MM/yyyy').format(DateTime.now());
|
||
|
|
}
|
||
|
|
|
||
|
|
_ora = TextEditingController(text: GlobalData.ora);
|
||
|
|
if (_ora.text.isEmpty) {
|
||
|
|
_ora.text = DateFormat('HH:mm').format(DateTime.now());
|
||
|
|
}
|
||
|
|
|
||
|
|
_luogo = TextEditingController(text: GlobalData.luogo);
|
||
|
|
_testimoni = TextEditingController(text: GlobalData.testimoni);
|
||
|
|
|
||
|
|
_feriti = GlobalData.feriti;
|
||
|
|
_danniVeicoli = GlobalData.Veicoli_danni_materiali_oltre;
|
||
|
|
_danniCose = GlobalData.Oggetti_diversi_danni_materiali;
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- POPUP INFORMATIVO ---
|
||
|
|
void _mostraInfoPopup(BuildContext context) {
|
||
|
|
Color activeColor = isB ? Colors.amber.shade700 : Colors.blue.shade900;
|
||
|
|
|
||
|
|
showGeneralDialog(
|
||
|
|
context: context,
|
||
|
|
barrierDismissible: false, // Obbliga l'utente a premere "Ho capito"
|
||
|
|
barrierLabel: "Popup",
|
||
|
|
barrierColor: Colors.black.withOpacity(0.5), // Sfondo scuro
|
||
|
|
transitionDuration: const Duration(milliseconds: 400), // Durata della dissolvenza (300 millisecondi)
|
||
|
|
pageBuilder: (context, animation, secondaryAnimation) {
|
||
|
|
return AlertDialog(
|
||
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||
|
|
title: Row(
|
||
|
|
children: [
|
||
|
|
Icon(Icons.info_outline, color: activeColor, size: 28),
|
||
|
|
const SizedBox(width: 10),
|
||
|
|
const Expanded(child: Text("Dati Generali", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20))),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
content: SingleChildScrollView(
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
mainAxisSize: MainAxisSize.min,
|
||
|
|
children: [
|
||
|
|
const Text("In questa prima pagina dovrai inserire i dati condivisi dell'incidente (Sezioni 1-5 del modulo).", style: TextStyle(fontSize: 15)),
|
||
|
|
const SizedBox(height: 16),
|
||
|
|
_buildPopupRow(Icons.place, "Luogo", "Usa il mirino GPS per trovare automaticamente l'indirizzo esatto."),
|
||
|
|
const SizedBox(height: 12),
|
||
|
|
_buildPopupRow(Icons.local_hospital, "Feriti", "Indica se ci sono persone che hanno subito lesioni."),
|
||
|
|
const SizedBox(height: 12),
|
||
|
|
_buildPopupRow(Icons.people, "Testimoni", "Se qualcuno ha visto l'incidente, segna i suoi contatti (nome, cognome, telefono)."),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
actions: [
|
||
|
|
SizedBox(
|
||
|
|
width: double.infinity,
|
||
|
|
child: ElevatedButton(
|
||
|
|
style: ElevatedButton.styleFrom(
|
||
|
|
backgroundColor: activeColor,
|
||
|
|
foregroundColor: isB ? Colors.black87 : Colors.white,
|
||
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||
|
|
),
|
||
|
|
onPressed: () => Navigator.pop(context),
|
||
|
|
child: const Text("HO CAPITO", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
},
|
||
|
|
// QUI GESTIAMO L'ANIMAZIONE DI DISSOLVENZA
|
||
|
|
// NUOVA ANIMAZIONE COMBINATA (FADE + ZOOM)
|
||
|
|
transitionBuilder: (context, animation, secondaryAnimation, child) {
|
||
|
|
var curvePosizione = CurvedAnimation(
|
||
|
|
parent: animation,
|
||
|
|
curve: Curves.easeOutBack, // Quando entra, fa un piccolo "rimbalzo" frenato
|
||
|
|
reverseCurve: Curves.easeInBack, // Quando esce, "prende la rincorsa" e cade giù
|
||
|
|
);
|
||
|
|
|
||
|
|
var curveOpacita = CurvedAnimation(
|
||
|
|
parent: animation,
|
||
|
|
curve: Curves.easeOut,
|
||
|
|
reverseCurve: Curves.easeIn,
|
||
|
|
);
|
||
|
|
|
||
|
|
return SlideTransition(
|
||
|
|
// Muove il popup sull'asse Y (verticale)
|
||
|
|
position: Tween<Offset>(
|
||
|
|
begin: const Offset(0.0, 0.4), // Parte dal basso (o cade verso il basso)
|
||
|
|
end: Offset.zero, // Centro esatto
|
||
|
|
).animate(curvePosizione),
|
||
|
|
child: FadeTransition(
|
||
|
|
opacity: curveOpacita,
|
||
|
|
child: child,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildPopupRow(IconData icon, String title, String desc) {
|
||
|
|
return Row(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Icon(icon, size: 24, color: Colors.blueGrey),
|
||
|
|
const SizedBox(width: 12),
|
||
|
|
Expanded(
|
||
|
|
child: RichText(
|
||
|
|
text: TextSpan(
|
||
|
|
style: const TextStyle(fontSize: 14, color: Colors.black87, height: 1.4),
|
||
|
|
children: [
|
||
|
|
TextSpan(text: "$title: ", style: const TextStyle(fontWeight: FontWeight.bold)),
|
||
|
|
TextSpan(text: desc),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- GEOLOCALIZZAZIONE ---
|
||
|
|
Future<void> _trovaPosizione({bool silenzioso = false}) async {
|
||
|
|
if (!silenzioso) setState(() => _gpsLoading = true);
|
||
|
|
|
||
|
|
try {
|
||
|
|
LocationPermission permission = await Geolocator.checkPermission();
|
||
|
|
if (permission == LocationPermission.denied) {
|
||
|
|
permission = await Geolocator.requestPermission();
|
||
|
|
if (permission == LocationPermission.denied) {
|
||
|
|
if (!silenzioso) throw Exception("Permesso negato");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (permission == LocationPermission.deniedForever) {
|
||
|
|
if (!silenzioso) throw Exception("Permessi GPS bloccati.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
Position position = await Geolocator.getCurrentPosition(
|
||
|
|
desiredAccuracy: LocationAccuracy.high
|
||
|
|
).timeout(const Duration(seconds: 5));
|
||
|
|
|
||
|
|
List<Placemark> placemarks = await placemarkFromCoordinates(position.latitude, position.longitude);
|
||
|
|
|
||
|
|
if (placemarks.isNotEmpty) {
|
||
|
|
Placemark place = placemarks[0];
|
||
|
|
|
||
|
|
String via = place.thoroughfare ?? place.street ?? "";
|
||
|
|
String numero = place.subThoroughfare ?? "";
|
||
|
|
String citta = place.locality ?? place.subLocality ?? "";
|
||
|
|
String prov = place.administrativeArea ?? "";
|
||
|
|
|
||
|
|
String indirizzoCompleto = "$via $numero, $citta ($prov)".trim();
|
||
|
|
if (indirizzoCompleto.startsWith(",")) indirizzoCompleto = indirizzoCompleto.substring(1).trim();
|
||
|
|
|
||
|
|
if (indirizzoCompleto.isEmpty) {
|
||
|
|
indirizzoCompleto = "${position.latitude}, ${position.longitude}";
|
||
|
|
}
|
||
|
|
|
||
|
|
if (mounted) {
|
||
|
|
setState(() {
|
||
|
|
_luogo.text = indirizzoCompleto.toUpperCase();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
if (!silenzioso && mounted) {
|
||
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||
|
|
content: Text("Errore GPS: ${e.toString().replaceAll('Exception:', '')}"),
|
||
|
|
backgroundColor: Colors.orange,
|
||
|
|
));
|
||
|
|
}
|
||
|
|
} finally {
|
||
|
|
if (mounted) setState(() => _gpsLoading = false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _selezionaData(BuildContext context) async {
|
||
|
|
DateTime initialDate = DateTime.now();
|
||
|
|
|
||
|
|
if (_data.text.length == 10) {
|
||
|
|
try {
|
||
|
|
List<String> parti = _data.text.split('/');
|
||
|
|
initialDate = DateTime(int.parse(parti[2]), int.parse(parti[1]), int.parse(parti[0]));
|
||
|
|
} catch (_) {}
|
||
|
|
}
|
||
|
|
|
||
|
|
final DateTime? picked = await showDatePicker(
|
||
|
|
context: context,
|
||
|
|
initialDate: initialDate,
|
||
|
|
firstDate: DateTime(1990),
|
||
|
|
lastDate: DateTime.now(), // Non si può fare un incidente nel futuro!
|
||
|
|
locale: const Locale('it', 'IT'),
|
||
|
|
);
|
||
|
|
|
||
|
|
if (picked != null) {
|
||
|
|
String dataFormattata = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}";
|
||
|
|
setState(() {
|
||
|
|
_data.text = dataFormattata;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void _salvaEProsegui() {
|
||
|
|
if (_data.text.isEmpty || _ora.text.isEmpty || _luogo.text.isEmpty) {
|
||
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Compila data, ora e luogo!"), backgroundColor: Colors.red));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
GlobalData.data_incidente = _data.text;
|
||
|
|
GlobalData.ora = _ora.text;
|
||
|
|
GlobalData.luogo = _luogo.text.toUpperCase();
|
||
|
|
GlobalData.feriti = _feriti;
|
||
|
|
GlobalData.Veicoli_danni_materiali_oltre = _danniVeicoli;
|
||
|
|
GlobalData.Oggetti_diversi_danni_materiali = _danniCose;
|
||
|
|
GlobalData.testimoni = _testimoni.text.toUpperCase();
|
||
|
|
|
||
|
|
Navigator.push(context, MaterialPageRoute(builder: (context) => const Comp6_7Screen()));
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
Color activeColor = isB ? Colors.amber.shade700 : Colors.blue.shade900;
|
||
|
|
Color bgColor = isB ? const Color(0xFFFFF9C4) : const Color(0xFFE3F2FD);
|
||
|
|
|
||
|
|
if (!_isReady) {
|
||
|
|
return Scaffold(backgroundColor: bgColor, body: Container());
|
||
|
|
}
|
||
|
|
|
||
|
|
return Scaffold(
|
||
|
|
backgroundColor: bgColor,
|
||
|
|
appBar: AppBar(
|
||
|
|
title: const Text("Sez. 1-5: Dati Generali"),
|
||
|
|
backgroundColor: activeColor,
|
||
|
|
foregroundColor: Colors.white,
|
||
|
|
),
|
||
|
|
body: SingleChildScrollView(
|
||
|
|
padding: const EdgeInsets.all(16),
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
|
|
children: [
|
||
|
|
// CARD 1: DATA E ORA
|
||
|
|
_buildCard(
|
||
|
|
titolo: "1. DATA E ORA",
|
||
|
|
accentColor: activeColor,
|
||
|
|
child: Row(children: [
|
||
|
|
Expanded(
|
||
|
|
child: _buildField(_data, "Data (GG/MM/AAAA)", Icons.calendar_today, activeColor,
|
||
|
|
isDate: true,
|
||
|
|
customPrefix: InkWell(
|
||
|
|
onTap: () => _selezionaData(context),
|
||
|
|
borderRadius: BorderRadius.circular(20),
|
||
|
|
child: Container(
|
||
|
|
width: 38,
|
||
|
|
alignment: Alignment.center,
|
||
|
|
child: Icon(Icons.calendar_today, size: 20, color: activeColor),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(width: 15),
|
||
|
|
Expanded(child: _buildField(_ora, "Ora (HH:MM)", Icons.access_time, activeColor)),
|
||
|
|
]),
|
||
|
|
),
|
||
|
|
|
||
|
|
// CARD 2: LUOGO
|
||
|
|
_buildCard(
|
||
|
|
titolo: "2. LUOGO",
|
||
|
|
accentColor: activeColor,
|
||
|
|
child: Row(children: [
|
||
|
|
Expanded(child: _buildField(_luogo, "Indirizzo / Luogo", Icons.map, activeColor, maxLines: 2)),
|
||
|
|
_gpsLoading
|
||
|
|
? const Padding(padding: EdgeInsets.all(12), child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2)))
|
||
|
|
: IconButton(
|
||
|
|
icon: const Icon(Icons.my_location, color: Colors.red),
|
||
|
|
onPressed: () => _trovaPosizione(silenzioso: false),
|
||
|
|
tooltip: "Usa GPS",
|
||
|
|
)
|
||
|
|
]),
|
||
|
|
),
|
||
|
|
|
||
|
|
// CARD 3: FERITI
|
||
|
|
_buildCard(
|
||
|
|
titolo: "3. FERITI",
|
||
|
|
accentColor: activeColor,
|
||
|
|
child: _buildSwitch("Ci sono feriti?", _feriti, (v) => setState(() => _feriti = v), activeColor),
|
||
|
|
),
|
||
|
|
|
||
|
|
// CARD 4: DANNI MATERIALI
|
||
|
|
_buildCard(
|
||
|
|
titolo: "4. ALTRI DANNI",
|
||
|
|
accentColor: activeColor,
|
||
|
|
child: Column(children: [
|
||
|
|
_buildSwitch("A veicoli oltre A e B?", _danniVeicoli, (v) => setState(() => _danniVeicoli = v), activeColor),
|
||
|
|
const Divider(),
|
||
|
|
_buildSwitch("A oggetti diversi dai veicoli?", _danniCose, (v) => setState(() => _danniCose = v), activeColor),
|
||
|
|
]),
|
||
|
|
),
|
||
|
|
|
||
|
|
// CARD 5: TESTIMONI
|
||
|
|
_buildCard(
|
||
|
|
titolo: "5. TESTIMONI",
|
||
|
|
accentColor: activeColor,
|
||
|
|
child: _buildField(_testimoni, "Nomi, Indirizzi, Telefoni...", Icons.people, activeColor, maxLines: 3),
|
||
|
|
),
|
||
|
|
|
||
|
|
const SizedBox(height: 20),
|
||
|
|
ElevatedButton(
|
||
|
|
onPressed: _salvaEProsegui,
|
||
|
|
style: ElevatedButton.styleFrom(
|
||
|
|
backgroundColor: activeColor,
|
||
|
|
foregroundColor: Colors.white,
|
||
|
|
padding: const EdgeInsets.symmetric(vertical: 18),
|
||
|
|
textStyle: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||
|
|
),
|
||
|
|
child: const Text("SALVA E PROSEGUI"),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 30),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- WIDGET PERSONALIZZATO NO / SÌ ---
|
||
|
|
Widget _buildSwitch(String label, bool value, Function(bool) onChanged, Color activeColor) {
|
||
|
|
return InkWell(
|
||
|
|
onTap: () => onChanged(!value), // Permette di cliccare su tutta la riga
|
||
|
|
child: Padding(
|
||
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||
|
|
child: Row(
|
||
|
|
children: [
|
||
|
|
// Etichetta Domanda
|
||
|
|
Expanded(
|
||
|
|
child: Text(
|
||
|
|
label,
|
||
|
|
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(width: 8),
|
||
|
|
|
||
|
|
// Testo NO
|
||
|
|
Text(
|
||
|
|
"NO",
|
||
|
|
style: TextStyle(
|
||
|
|
fontWeight: !value ? FontWeight.bold : FontWeight.normal,
|
||
|
|
color: !value ? Colors.red.shade700 : Colors.grey.shade400,
|
||
|
|
fontSize: 14,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
|
||
|
|
// Switch Fisico
|
||
|
|
Switch(
|
||
|
|
value: value,
|
||
|
|
onChanged: onChanged,
|
||
|
|
activeColor: activeColor,
|
||
|
|
activeTrackColor: activeColor.withOpacity(0.4),
|
||
|
|
inactiveThumbColor: Colors.grey,
|
||
|
|
inactiveTrackColor: Colors.grey.shade300,
|
||
|
|
),
|
||
|
|
|
||
|
|
// Testo SÌ
|
||
|
|
Text(
|
||
|
|
"SÌ",
|
||
|
|
style: TextStyle(
|
||
|
|
fontWeight: value ? FontWeight.bold : FontWeight.normal,
|
||
|
|
color: value ? activeColor : Colors.grey.shade400,
|
||
|
|
fontSize: 14,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildField(TextEditingController c, String label, IconData i, Color iconColor, {String? hint, int maxLines = 1, bool isDate = false, Widget? customPrefix}) {
|
||
|
|
return TextField(
|
||
|
|
controller: c, maxLines: maxLines,
|
||
|
|
textCapitalization: TextCapitalization.sentences,
|
||
|
|
keyboardType: isDate ? TextInputType.number : TextInputType.text,
|
||
|
|
inputFormatters: isDate ? [DateInputFormatter(), LengthLimitingTextInputFormatter(10)] : [],
|
||
|
|
style: TextStyle(fontSize: isDate ? 14 : 16), // Rimpicciolisce un po' il font se è una data
|
||
|
|
decoration: InputDecoration(
|
||
|
|
labelText: label, hintText: hint,
|
||
|
|
prefixIcon: customPrefix ?? Icon(i, size: 20, color: iconColor),
|
||
|
|
prefixIconConstraints: isDate ? const BoxConstraints(minWidth: 38, minHeight: 38) : null, // Stringe l'icona
|
||
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
||
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 15),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildCard({required String titolo, required Widget child, required Color accentColor}) {
|
||
|
|
return Container(
|
||
|
|
width: double.infinity, margin: const EdgeInsets.only(bottom: 20), padding: const EdgeInsets.all(16),
|
||
|
|
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 10, offset: const Offset(0, 4))]),
|
||
|
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||
|
|
Text(titolo, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: accentColor)),
|
||
|
|
const Divider(height: 25, thickness: 1.2),
|
||
|
|
child
|
||
|
|
]),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() {
|
||
|
|
_data.dispose(); _ora.dispose(); _luogo.dispose(); _testimoni.dispose();
|
||
|
|
super.dispose();
|
||
|
|
}
|
||
|
|
}
|