feat: adds auto sync feature and fixes UI a bit up

This commit is contained in:
2025-08-27 21:47:24 +02:00
parent 33dfd6e3e5
commit e95dcf3322
8 changed files with 1268 additions and 223 deletions

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import '../providers/settings_provider.dart';
import '../providers/supplement_provider.dart';
class HistoryScreen extends StatefulWidget {
@@ -125,27 +126,33 @@ class _HistoryScreenState extends State<HistoryScreen> {
Expanded(
flex: 3,
child: Container(
margin: const EdgeInsets.fromLTRB(0, 16, 16, 16),
child: _buildSelectedDayDetails(groupedIntakes),
// add a bit more horizontal spacing between calendar and card
margin: const EdgeInsets.fromLTRB(8, 16, 16, 16),
child: SingleChildScrollView(
child: _buildSelectedDayDetails(groupedIntakes),
),
),
),
],
);
} else {
// Mobile layout: vertical stack
return Column(
children: [
// Calendar
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
child: _buildCalendar(groupedIntakes),
),
const SizedBox(height: 16),
// Selected day details
Expanded(
child: _buildSelectedDayDetails(groupedIntakes),
),
],
return SingleChildScrollView(
child: Column(
children: [
// Calendar
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
child: _buildCalendar(groupedIntakes),
),
const SizedBox(height: 16),
// Selected day details
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
child: _buildSelectedDayDetails(groupedIntakes),
),
],
),
);
}
},
@@ -479,78 +486,208 @@ class _HistoryScreenState extends State<HistoryScreen> {
],
),
),
Expanded(
child: ListView.builder(
padding: EdgeInsets.all(isWideScreen ? 20 : 16),
itemCount: dayIntakes.length,
itemBuilder: (context, index) {
final intake = dayIntakes[index];
final takenAt = DateTime.parse(intake['takenAt']);
final units = (intake['unitsTaken'] as num?)?.toDouble() ?? 1.0;
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
child: Padding(
padding: EdgeInsets.all(isWideScreen ? 16 : 12),
Padding(
padding: EdgeInsets.all(isWideScreen ? 20 : 16),
child: Builder(
builder: (context) {
final settingsProvider = Provider.of<SettingsProvider>(context, listen: false);
// Sort once per render
final sortedDayIntakes = List<Map<String, dynamic>>.from(dayIntakes)
..sort((a, b) => DateTime.parse(a['takenAt']).compareTo(DateTime.parse(b['takenAt'])));
// Helpers
String timeCategory(DateTime dt) {
final h = dt.hour;
if (h >= settingsProvider.morningStart && h <= settingsProvider.morningEnd) return 'morning';
if (h >= settingsProvider.afternoonStart && h <= settingsProvider.afternoonEnd) return 'afternoon';
if (h >= settingsProvider.eveningStart && h <= settingsProvider.eveningEnd) return 'evening';
final ns = settingsProvider.nightStart;
final ne = settingsProvider.nightEnd;
final inNight = ns <= ne ? (h >= ns && h <= ne) : (h >= ns || h <= ne);
return inNight ? 'night' : 'anytime';
}
String? sectionRange(String cat) {
switch (cat) {
case 'morning':
return settingsProvider.morningRange;
case 'afternoon':
return settingsProvider.afternoonRange;
case 'evening':
return settingsProvider.eveningRange;
case 'night':
return settingsProvider.nightRange;
default:
return null;
}
}
Widget headerFor(String cat) {
late final IconData icon;
late final Color color;
late final String title;
switch (cat) {
case 'morning':
icon = Icons.wb_sunny;
color = Colors.orange;
title = 'Morning';
break;
case 'afternoon':
icon = Icons.light_mode;
color = Colors.blue;
title = 'Afternoon';
break;
case 'evening':
icon = Icons.nightlight_round;
color = Colors.indigo;
title = 'Evening';
break;
case 'night':
icon = Icons.bedtime;
color = Colors.purple;
title = 'Night';
break;
default:
icon = Icons.schedule;
color = Colors.grey;
title = 'Anytime';
}
final range = sectionRange(cat);
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: color.withOpacity(0.3),
width: 1,
),
),
child: Row(
children: [
CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primary,
radius: isWideScreen ? 24 : 20,
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
Icons.medication,
color: Theme.of(context).colorScheme.onPrimary,
size: isWideScreen ? 24 : 20,
icon,
size: 20,
color: color,
),
),
SizedBox(width: isWideScreen ? 16 : 12),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
intake['supplementName'],
title,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: isWideScreen ? 16 : 14,
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 4),
Text(
'${units.toStringAsFixed(units % 1 == 0 ? 0 : 1)} ${intake['supplementUnitType'] ?? 'units'} at ${DateFormat('HH:mm').format(takenAt)}',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500,
fontSize: isWideScreen ? 14 : 12,
),
),
if (intake['notes'] != null && intake['notes'].toString().isNotEmpty) ...[
const SizedBox(height: 4),
if (range != null) ...[
Text(
intake['notes'],
'($range)',
style: TextStyle(
fontSize: isWideScreen ? 13 : 12,
fontStyle: FontStyle.italic,
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 12,
fontWeight: FontWeight.w500,
color: color.withOpacity(0.8),
),
),
],
],
),
),
IconButton(
icon: Icon(
Icons.delete_outline,
color: Colors.red.shade400,
size: isWideScreen ? 24 : 20,
),
onPressed: () => _deleteIntake(context, intake['id'], intake['supplementName']),
tooltip: 'Delete intake',
),
],
),
),
);
}
// Build a non-scrollable list so the card auto-expands to fit content
final List<Widget> children = [];
for (int index = 0; index < sortedDayIntakes.length; index++) {
final intake = sortedDayIntakes[index];
final takenAt = DateTime.parse(intake['takenAt']);
final units = (intake['unitsTaken'] as num?)?.toDouble() ?? 1.0;
final currentCategory = timeCategory(takenAt);
final needsHeader = index == 0
? true
: currentCategory != timeCategory(DateTime.parse(sortedDayIntakes[index - 1]['takenAt']));
if (needsHeader) {
children.add(headerFor(currentCategory));
}
children.add(
Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
child: Padding(
padding: EdgeInsets.all(isWideScreen ? 16 : 12),
child: Row(
children: [
CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primary,
radius: isWideScreen ? 24 : 20,
child: Icon(
Icons.medication,
color: Theme.of(context).colorScheme.onPrimary,
size: isWideScreen ? 24 : 20,
),
),
SizedBox(width: isWideScreen ? 16 : 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
intake['supplementName'],
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: isWideScreen ? 16 : 14,
),
),
const SizedBox(height: 4),
Text(
'${units.toStringAsFixed(units % 1 == 0 ? 0 : 1)} ${intake['supplementUnitType'] ?? 'units'} at ${DateFormat('HH:mm').format(takenAt)}',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500,
fontSize: isWideScreen ? 14 : 12,
),
),
if (intake['notes'] != null && intake['notes'].toString().isNotEmpty) ...[
const SizedBox(height: 4),
Text(
intake['notes'],
style: TextStyle(
fontSize: isWideScreen ? 13 : 12,
fontStyle: FontStyle.italic,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
],
),
),
IconButton(
icon: Icon(
Icons.delete_outline,
color: Colors.red.shade400,
size: isWideScreen ? 24 : 20,
),
onPressed: () => _deleteIntake(context, intake['id'], intake['supplementName']),
tooltip: 'Delete intake',
),
],
),
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
);
},
),