741 lines
25 KiB
Dart
741 lines
25 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
import 'package:intl/intl.dart';
|
|
import '../services/api_service.dart';
|
|
import '../models/sensor_data.dart';
|
|
|
|
class ChartScreen extends StatefulWidget {
|
|
const ChartScreen({super.key});
|
|
|
|
@override
|
|
State<ChartScreen> createState() => _ChartScreenState();
|
|
}
|
|
|
|
class _ChartScreenState extends State<ChartScreen> {
|
|
final ApiService _apiService = ApiService();
|
|
List<SensorData> historyData = [];
|
|
bool isLoading = true;
|
|
String selectedPeriod = '50'; // 50, 100, 200
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadHistoryData();
|
|
}
|
|
|
|
Future<void> _loadHistoryData() async {
|
|
setState(() {
|
|
isLoading = true;
|
|
});
|
|
|
|
try {
|
|
final data = await _apiService.getDataHistory(limit: int.parse(selectedPeriod));
|
|
if (mounted) {
|
|
setState(() {
|
|
historyData = data;
|
|
isLoading = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
print('Error loading history: $e');
|
|
if (mounted) {
|
|
setState(() {
|
|
isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
List<FlSpot> _getSuhuSpots() {
|
|
return historyData.asMap().entries.map((entry) {
|
|
return FlSpot(entry.key.toDouble(), entry.value.suhu);
|
|
}).toList();
|
|
}
|
|
|
|
List<FlSpot> _getBeratSpots() {
|
|
return historyData.asMap().entries.map((entry) {
|
|
return FlSpot(entry.key.toDouble(), entry.value.berat);
|
|
}).toList();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Grafik History'),
|
|
backgroundColor: Colors.blue,
|
|
foregroundColor: Colors.white,
|
|
actions: [
|
|
PopupMenuButton<String>(
|
|
icon: const Icon(Icons.filter_list),
|
|
onSelected: (value) {
|
|
setState(() {
|
|
selectedPeriod = value;
|
|
_loadHistoryData();
|
|
});
|
|
},
|
|
itemBuilder: (context) => [
|
|
const PopupMenuItem(value: '50', child: Text('50 Data Terakhir')),
|
|
const PopupMenuItem(value: '100', child: Text('100 Data Terakhir')),
|
|
const PopupMenuItem(value: '200', child: Text('200 Data Terakhir')),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
body: isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: historyData.isEmpty
|
|
? Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.inbox, size: 64, color: Colors.grey.shade400),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Belum ada data history',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: RefreshIndicator(
|
|
onRefresh: _loadHistoryData,
|
|
child: ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
// Info card
|
|
_buildInfoCard(),
|
|
const SizedBox(height: 16),
|
|
// Temperature chart
|
|
_buildTemperatureChart(),
|
|
const SizedBox(height: 16),
|
|
// Weight chart
|
|
_buildWeightChart(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoCard() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [Colors.blue.shade400, Colors.blue.shade600],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.info_outline, color: Colors.white, size: 32),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'History Data',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'$selectedPeriod data terakhir dari database',
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.white70,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: _loadHistoryData,
|
|
icon: const Icon(Icons.refresh, color: Colors.white),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTemperatureChart() {
|
|
final suhuSpots = _getSuhuSpots();
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.orange.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Icon(Icons.thermostat, color: Colors.orange, size: 24),
|
|
),
|
|
const SizedBox(width: 12),
|
|
const Expanded(
|
|
child: Text(
|
|
'Grafik Suhu',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
SizedBox(
|
|
height: 200,
|
|
child: LineChart(
|
|
LineChartData(
|
|
gridData: FlGridData(
|
|
show: true,
|
|
drawVerticalLine: false,
|
|
horizontalInterval: 10,
|
|
getDrawingHorizontalLine: (value) => FlLine(
|
|
color: Colors.grey.shade200,
|
|
strokeWidth: 1,
|
|
),
|
|
),
|
|
titlesData: FlTitlesData(
|
|
show: true,
|
|
rightTitles: const AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
topTitles: const AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 30,
|
|
interval: suhuSpots.length > 20 ? 20 : 10,
|
|
getTitlesWidget: (value, meta) {
|
|
if (value.toInt() >= 0 && value.toInt() < historyData.length) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 8.0),
|
|
child: Text(
|
|
'#${value.toInt()}',
|
|
style: const TextStyle(fontSize: 10, color: Colors.grey),
|
|
),
|
|
);
|
|
}
|
|
return const Text('');
|
|
},
|
|
),
|
|
),
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
interval: 10,
|
|
reservedSize: 40,
|
|
getTitlesWidget: (value, meta) => Text(
|
|
'${value.toInt()}°C',
|
|
style: const TextStyle(fontSize: 10, color: Colors.grey),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
borderData: FlBorderData(
|
|
show: true,
|
|
border: Border(
|
|
bottom: BorderSide(color: Colors.grey.shade300),
|
|
left: BorderSide(color: Colors.grey.shade300),
|
|
),
|
|
),
|
|
minX: 0,
|
|
maxX: suhuSpots.isNotEmpty ? suhuSpots.length.toDouble() - 1 : 10,
|
|
minY: 20,
|
|
maxY: 60,
|
|
lineBarsData: [
|
|
LineChartBarData(
|
|
spots: suhuSpots,
|
|
isCurved: true,
|
|
color: Colors.orange,
|
|
barWidth: 3,
|
|
dotData: const FlDotData(show: false),
|
|
belowBarData: BarAreaData(
|
|
show: true,
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Colors.orange.withOpacity(0.3),
|
|
Colors.orange.withOpacity(0.1),
|
|
Colors.transparent,
|
|
],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
lineTouchData: LineTouchData(
|
|
touchTooltipData: LineTouchTooltipData(
|
|
getTooltipColor: (touchedSpot) => Colors.black87,
|
|
tooltipRoundedRadius: 8,
|
|
getTooltipItems: (spots) {
|
|
return spots.map((spot) {
|
|
final index = spot.x.toInt();
|
|
if (index >= 0 && index < historyData.length) {
|
|
final data = historyData[index];
|
|
return LineTooltipItem(
|
|
'${spot.y.toStringAsFixed(1)}°C\n',
|
|
const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
children: [
|
|
TextSpan(
|
|
text: data.timestamp,
|
|
style: const TextStyle(
|
|
fontSize: 10,
|
|
color: Colors.white70,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
return null;
|
|
}).toList();
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildWeightChart() {
|
|
final beratSpots = _getBeratSpots();
|
|
final avgTarget = historyData.isNotEmpty
|
|
? historyData.map((e) => e.target).reduce((a, b) => a + b) / historyData.length
|
|
: 0.0;
|
|
|
|
// Filter data yang valid untuk ditampilkan (ganti nilai 0 dengan nilai sebelumnya atau null)
|
|
List<FlSpot> displaySpots = [];
|
|
double? lastValidValue;
|
|
|
|
for (var spot in beratSpots) {
|
|
if (spot.y > 10) {
|
|
// Data valid, gunakan nilai asli
|
|
displaySpots.add(spot);
|
|
lastValidValue = spot.y;
|
|
} else if (lastValidValue != null) {
|
|
// Data invalid (0), gunakan nilai terakhir yang valid
|
|
displaySpots.add(FlSpot(spot.x, lastValidValue));
|
|
}
|
|
// Jika lastValidValue null dan spot.y invalid, skip point ini
|
|
}
|
|
|
|
// Jika masih kosong setelah filter, gunakan data asli
|
|
if (displaySpots.isEmpty) {
|
|
displaySpots = beratSpots;
|
|
}
|
|
|
|
// Hitung minY dan maxY yang aman untuk menghindari overflow
|
|
double minY = 0;
|
|
double maxY = 200;
|
|
|
|
if (displaySpots.isNotEmpty) {
|
|
double dataMin = displaySpots.map((e) => e.y).reduce((a, b) => a < b ? a : b);
|
|
double dataMax = displaySpots.map((e) => e.y).reduce((a, b) => a > b ? a : b);
|
|
|
|
// Sertakan target dalam perhitungan
|
|
if (avgTarget > 0) {
|
|
dataMin = dataMin < avgTarget ? dataMin : avgTarget;
|
|
dataMax = dataMax > avgTarget ? dataMax : avgTarget;
|
|
}
|
|
|
|
// Tambah padding 25%
|
|
double range = dataMax - dataMin;
|
|
double padding = range > 0 ? range * 0.25 : 50;
|
|
|
|
minY = (dataMin - padding).clamp(0, double.infinity);
|
|
maxY = dataMax + padding;
|
|
|
|
// Pastikan range minimal 100
|
|
if (maxY - minY < 100) {
|
|
double center = (maxY + minY) / 2;
|
|
minY = (center - 50).clamp(0, double.infinity);
|
|
maxY = center + 50;
|
|
}
|
|
} else if (avgTarget > 0) {
|
|
minY = 0;
|
|
maxY = (avgTarget * 1.5).clamp(100, double.infinity);
|
|
}
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Icon(Icons.scale, color: Colors.blue, size: 24),
|
|
),
|
|
const SizedBox(width: 12),
|
|
const Expanded(
|
|
child: Text(
|
|
'Grafik Berat',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
SizedBox(
|
|
height: 200,
|
|
child: LineChart(
|
|
LineChartData(
|
|
gridData: FlGridData(
|
|
show: true,
|
|
drawVerticalLine: false,
|
|
horizontalInterval: 50,
|
|
getDrawingHorizontalLine: (value) {
|
|
if ((value - avgTarget).abs() < 10) {
|
|
return FlLine(
|
|
color: Colors.green.shade300,
|
|
strokeWidth: 2,
|
|
dashArray: [5, 5],
|
|
);
|
|
}
|
|
return FlLine(
|
|
color: Colors.grey.shade200,
|
|
strokeWidth: 1,
|
|
);
|
|
},
|
|
),
|
|
titlesData: FlTitlesData(
|
|
show: true,
|
|
rightTitles: const AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
topTitles: const AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 30,
|
|
interval: beratSpots.length > 20 ? 20 : 10,
|
|
getTitlesWidget: (value, meta) {
|
|
if (value.toInt() >= 0 && value.toInt() < historyData.length) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 8.0),
|
|
child: Text(
|
|
'#${value.toInt()}',
|
|
style: const TextStyle(fontSize: 10, color: Colors.grey),
|
|
),
|
|
);
|
|
}
|
|
return const Text('');
|
|
},
|
|
),
|
|
),
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
interval: 50,
|
|
reservedSize: 45,
|
|
getTitlesWidget: (value, meta) {
|
|
final isTarget = (value - avgTarget).abs() < 10;
|
|
return Text(
|
|
'${value.toInt()}g',
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
color: isTarget ? Colors.green : Colors.grey,
|
|
fontWeight: isTarget ? FontWeight.bold : FontWeight.normal,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
borderData: FlBorderData(
|
|
show: true,
|
|
border: Border(
|
|
bottom: BorderSide(color: Colors.grey.shade300),
|
|
left: BorderSide(color: Colors.grey.shade300),
|
|
),
|
|
),
|
|
minX: 0,
|
|
maxX: beratSpots.isNotEmpty ? beratSpots.length.toDouble() - 1 : 10,
|
|
minY: minY,
|
|
maxY: maxY,
|
|
extraLinesData: ExtraLinesData(
|
|
horizontalLines: [
|
|
if (avgTarget > 0)
|
|
HorizontalLine(
|
|
y: avgTarget,
|
|
color: Colors.green.shade400,
|
|
strokeWidth: 2,
|
|
dashArray: [8, 4],
|
|
label: HorizontalLineLabel(
|
|
show: true,
|
|
alignment: Alignment.topRight,
|
|
style: TextStyle(
|
|
color: Colors.green.shade700,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
labelResolver: (line) => 'Target',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
lineBarsData: [
|
|
LineChartBarData(
|
|
spots: displaySpots,
|
|
isCurved: true,
|
|
color: Colors.blue,
|
|
barWidth: 3,
|
|
dotData: const FlDotData(show: false),
|
|
belowBarData: BarAreaData(
|
|
show: true,
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Colors.blue.withOpacity(0.3),
|
|
Colors.blue.withOpacity(0.1),
|
|
Colors.transparent,
|
|
],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
lineTouchData: LineTouchData(
|
|
touchTooltipData: LineTouchTooltipData(
|
|
getTooltipColor: (touchedSpot) => Colors.black87,
|
|
tooltipRoundedRadius: 8,
|
|
getTooltipItems: (spots) {
|
|
return spots.map((spot) {
|
|
final index = spot.x.toInt();
|
|
if (index >= 0 && index < historyData.length) {
|
|
final data = historyData[index];
|
|
return LineTooltipItem(
|
|
'${spot.y.toStringAsFixed(0)}g\n',
|
|
const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
children: [
|
|
TextSpan(
|
|
text: 'Target: ${data.target.toStringAsFixed(0)}g\n',
|
|
style: const TextStyle(
|
|
fontSize: 10,
|
|
color: Colors.green,
|
|
),
|
|
),
|
|
TextSpan(
|
|
text: data.timestamp,
|
|
style: const TextStyle(
|
|
fontSize: 10,
|
|
color: Colors.white70,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
return null;
|
|
}).toList();
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatisticsCard() {
|
|
if (historyData.isEmpty) return const SizedBox.shrink();
|
|
|
|
final avgSuhu = historyData.map((e) => e.suhu).reduce((a, b) => a + b) / historyData.length;
|
|
final maxSuhu = historyData.map((e) => e.suhu).reduce((a, b) => a > b ? a : b);
|
|
final minSuhu = historyData.map((e) => e.suhu).reduce((a, b) => a < b ? a : b);
|
|
|
|
final avgBerat = historyData.map((e) => e.berat).reduce((a, b) => a + b) / historyData.length;
|
|
final maxBerat = historyData.map((e) => e.berat).reduce((a, b) => a > b ? a : b);
|
|
final minBerat = historyData.map((e) => e.berat).reduce((a, b) => a < b ? a : b);
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Statistik',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildStatItem(
|
|
icon: Icons.thermostat,
|
|
iconColor: Colors.orange,
|
|
label: 'Rata-rata Suhu',
|
|
value: '${avgSuhu.toStringAsFixed(1)}°C',
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: _buildStatItem(
|
|
icon: Icons.scale,
|
|
iconColor: Colors.blue,
|
|
label: 'Rata-rata Berat',
|
|
value: '${avgBerat.toStringAsFixed(0)}g',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildStatItem(
|
|
icon: Icons.arrow_upward,
|
|
iconColor: Colors.red,
|
|
label: 'Max Suhu',
|
|
value: '${maxSuhu.toStringAsFixed(1)}°C',
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: _buildStatItem(
|
|
icon: Icons.arrow_downward,
|
|
iconColor: Colors.green,
|
|
label: 'Min Suhu',
|
|
value: '${minSuhu.toStringAsFixed(1)}°C',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildStatItem(
|
|
icon: Icons.arrow_upward,
|
|
iconColor: Colors.red,
|
|
label: 'Max Berat',
|
|
value: '${maxBerat.toStringAsFixed(0)}g',
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: _buildStatItem(
|
|
icon: Icons.arrow_downward,
|
|
iconColor: Colors.green,
|
|
label: 'Min Berat',
|
|
value: '${minBerat.toStringAsFixed(0)}g',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatItem({
|
|
required IconData icon,
|
|
required Color iconColor,
|
|
required String label,
|
|
required String value,
|
|
}) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade50,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Icon(icon, color: iconColor, size: 24),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
value,
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
label,
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|