Files
flutter/lib/widgets/weight/entries_modal.dart
2025-12-30 13:19:09 +01:00

369 lines
12 KiB
Dart

/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (C) 2020, 2021 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* wger Workout Manager is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/body_weight/weight_entry.dart';
import 'package:wger/providers/body_weight.dart';
import 'package:wger/providers/user.dart';
import 'package:wger/theme/theme.dart';
import 'package:wger/widgets/measurements/charts.dart';
import 'edit_modal.dart';
/// Shows a modal bottom sheet with all weight entries
void showWeightEntriesModal(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.7,
minChildSize: 0.5,
maxChildSize: 0.95,
builder: (context, scrollController) => Container(
decoration: BoxDecoration(
color: isDarkMode ? Theme.of(context).scaffoldBackgroundColor : Colors.white,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(
children: [
// Handle bar
Padding(
padding: const EdgeInsets.only(top: 12, bottom: 8),
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey.shade600 : Colors.grey.shade300,
borderRadius: BorderRadius.circular(2),
),
),
),
// Header
Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 12, 16),
child: _WeightEntriesHeader(isDarkMode: isDarkMode),
),
// Entries list
Expanded(
child: Consumer<BodyWeightProvider>(
builder: (context, provider, child) {
final entries = provider.items;
if (entries.isEmpty) {
return _buildEmptyState(context, isDarkMode);
}
return ListView.builder(
controller: scrollController,
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
itemCount: entries.length,
itemBuilder: (context, index) => _WeightEntryTile(
entry: entries[index],
isDarkMode: isDarkMode,
),
);
},
),
),
],
),
),
),
);
}
Widget _buildEmptyState(BuildContext context, bool isDarkMode) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.monitor_weight_outlined,
size: 64,
color: isDarkMode ? Colors.grey.shade600 : Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context).noWeightEntries,
style: TextStyle(
fontSize: 16,
color: isDarkMode ? Colors.grey.shade400 : Colors.grey.shade600,
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () => showEditWeightModal(context, null),
icon: const Icon(Icons.add, size: 18),
label: Text(AppLocalizations.of(context).newEntry),
style: ElevatedButton.styleFrom(
backgroundColor: wgerAccentColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
],
),
);
}
class _WeightEntriesHeader extends StatelessWidget {
final bool isDarkMode;
const _WeightEntriesHeader({required this.isDarkMode});
@override
Widget build(BuildContext context) {
final provider = Provider.of<BodyWeightProvider>(context);
final entryCount = provider.items.length;
return Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context).weight,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'$entryCount ${AppLocalizations.of(context).entries}',
style: TextStyle(
fontSize: 14,
color: isDarkMode ? Colors.grey.shade400 : Colors.grey.shade600,
),
),
],
),
),
],
);
}
}
class _WeightEntryTile extends StatelessWidget {
final WeightEntry entry;
final bool isDarkMode;
const _WeightEntryTile({
required this.entry,
required this.isDarkMode,
});
@override
Widget build(BuildContext context) {
final profile = context.read<UserProvider>().profile;
final numberFormat = NumberFormat.decimalPattern(
Localizations.localeOf(context).toString(),
);
final dateFormat = DateFormat.yMMMd(
Localizations.localeOf(context).languageCode,
);
final timeFormat = DateFormat.Hm(
Localizations.localeOf(context).languageCode,
);
final unit = weightUnit(profile!.isMetric, context);
return Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey.shade800.withValues(alpha: 0.5) : Colors.grey.shade100,
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// Weight value
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
numberFormat.format(entry.weight),
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 4),
Text(
unit,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.grey.shade400 : Colors.grey.shade600,
),
),
],
),
const SizedBox(height: 4),
Text(
'${dateFormat.format(entry.date)} ${timeFormat.format(entry.date)}',
style: TextStyle(
fontSize: 13,
color: isDarkMode ? Colors.grey.shade400 : Colors.grey.shade600,
),
),
],
),
),
// Actions
Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildActionButton(
context,
icon: Icons.edit_outlined,
onTap: () => showEditWeightModal(context, entry),
),
const SizedBox(width: 8),
_buildActionButton(
context,
icon: Icons.delete_outline,
onTap: () => _showDeleteEntryDialog(context, entry),
isDelete: true,
),
],
),
],
),
),
);
}
Widget _buildActionButton(
BuildContext context, {
required IconData icon,
required VoidCallback onTap,
bool isDelete = false,
}) {
return Material(
color: isDelete
? Theme.of(context).colorScheme.error.withValues(alpha: 0.1)
: wgerAccentColor.withValues(alpha: isDarkMode ? 0.2 : 0.12),
borderRadius: BorderRadius.circular(10),
child: InkWell(
borderRadius: BorderRadius.circular(10),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(10),
child: Icon(
icon,
size: 18,
color: isDelete ? Theme.of(context).colorScheme.error : wgerAccentColor,
),
),
),
);
}
void _showDeleteEntryDialog(BuildContext context, WeightEntry entry) {
final numberFormat = NumberFormat.decimalPattern(
Localizations.localeOf(context).toString(),
);
final dateFormat = DateFormat.yMMMd(
Localizations.localeOf(context).languageCode,
);
final profile = context.read<UserProvider>().profile;
final unit = weightUnit(profile!.isMetric, context);
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(AppLocalizations.of(context).delete),
content: Text(
'${numberFormat.format(entry.weight)} $unit - ${dateFormat.format(entry.date)}',
),
actions: [
TextButton(
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: () => Navigator.pop(dialogContext),
child: Text(
MaterialLocalizations.of(context).cancelButtonLabel,
style: TextStyle(color: isDarkMode ? Colors.grey.shade400 : Colors.grey.shade600),
),
),
TextButton(
style: TextButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error.withValues(alpha: 0.1),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: () async {
try {
await Provider.of<BodyWeightProvider>(
context,
listen: false,
).deleteEntry(entry.id!);
if (context.mounted) {
Navigator.pop(dialogContext);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context).successfullyDeleted,
textAlign: TextAlign.center,
),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
}
} catch (error) {
if (context.mounted) {
Navigator.pop(dialogContext);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context).anErrorOccurred,
textAlign: TextAlign.center,
),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
}
},
child: Text(
AppLocalizations.of(context).delete,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
],
),
);
}
}