Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

175 feat allow selecting exercise in summary tab to see past sessions #177

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions lib/core/database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,79 @@ class DatabaseHelper {
return summary;
}

Future<Map<DateTime, List<Map<String, dynamic>>>> getDailyRecordsForExercise(
String exerciseName) async {
final db = await database;
final List<Map<String, dynamic>> records = await db.query(
'exercises',
where: 'exercise = ?',
whereArgs: [exerciseName],
orderBy: 'timestamp DESC', // Optional: order by timestamp
);

Map<DateTime, List<Map<String, dynamic>>> dailyRecords = {};

for (var record in records) {
DateTime date = DateTime.parse(record['timestamp']).toLocal();
DateTime day = DateTime(date.year, date.month, date.day);

if (!dailyRecords.containsKey(day)) {
dailyRecords[day] = [];
}
dailyRecords[day]!.add(record);
}

return dailyRecords;
}

Future<Map<String, dynamic>> getSummaryForExercise(
String exerciseName) async {
final db = await database;
final List<Map<String, dynamic>> exercises = await db.query(
'exercises',
where: 'exercise = ?',
whereArgs: [exerciseName],
);

Map<String, dynamic> summary = {};
Map<String, List<Map<String, dynamic>>> recordsByDate = {};

if (exercises.isNotEmpty) {
double totalWeight = 0;
int totalSets = 0;
int totalReps = 0;

for (var record in exercises) {
double weight = double.parse(record['weight']);
int reps = record['reps'];
int sets = record['sets'];
DateTime timestamp = DateTime.parse(record['timestamp']);
String dateKey =
'${timestamp.year}-${timestamp.month}-${timestamp.day}';

double weightPerSession = weight * reps * sets;
totalWeight += weightPerSession;
totalSets += sets;
totalReps += reps;

if (recordsByDate[dateKey] == null) {
recordsByDate[dateKey] = [];
}
recordsByDate[dateKey]!.add(record);
}

summary[exerciseName] = {
'totalWeight': totalWeight,
'totalSets': totalSets,
'totalReps': totalReps,
'avgWeight': totalWeight / totalSets,
'recordsByDate': recordsByDate,
};
}

return summary;
}

Future<List<String>> getPredefinedExercises() async {
final db = await database;
final List<Map<String, dynamic>> result =
Expand Down
223 changes: 175 additions & 48 deletions lib/tabs/summary.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class SummaryTab extends StatefulWidget {
class _SummaryTabState extends State<SummaryTab> {
final DatabaseHelper _dbHelper = DatabaseHelper();
List<DateTime> _trainedDates = [];
bool _isExerciseView = false;
String? _selectedExercise;
Map<DateTime, List<Map<String, dynamic>>> _dailyRecords = {};

@override
void initState() {
Expand Down Expand Up @@ -42,25 +45,29 @@ class _SummaryTabState extends State<SummaryTab> {
onDaySelected: (selectedDay, focusedDay) {
setState(() {
widget.onDateSelected(selectedDay);
_isExerciseView = false;
});
Navigator.of(context).pop(); // Close the modal after selection
Navigator.of(context).pop();
},
eventLoader: (day) {
return _trainedDates.where((d) => isSameDay(d, day)).toList();
},
headerStyle: HeaderStyle(
formatButtonVisible: false,
titleTextStyle:
TextStyle(color: Theme.of(context).colorScheme.primary),
),
calendarStyle: CalendarStyle(
todayDecoration: BoxDecoration(
color: Colors.blueAccent,
color: Theme.of(context).colorScheme.secondary,
shape: BoxShape.circle,
),
selectedDecoration: BoxDecoration(
color: Colors.orange,
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
todayTextStyle: TextStyle(color: Colors.white),
todayTextStyle:
TextStyle(color: Theme.of(context).colorScheme.onBackground),
markersMaxCount: 1,
),
calendarBuilders: CalendarBuilders(
Expand All @@ -72,7 +79,7 @@ class _SummaryTabState extends State<SummaryTab> {
height: 40,
decoration: BoxDecoration(
border: Border.all(
color: Colors.red,
color: Theme.of(context).colorScheme.secondary,
width: 2.0,
),
shape: BoxShape.circle,
Expand All @@ -88,8 +95,42 @@ class _SummaryTabState extends State<SummaryTab> {
);
}

Future<void> _showExerciseSelectionModal(BuildContext context) async {
List<String> exercises = await _dbHelper.getPredefinedExercises();
await showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return ListView.builder(
itemCount: exercises.length,
itemBuilder: (context, index) {
final exercise = exercises[index];
return ListTile(
title: Text(exercise),
onTap: () async {
final dailyRecords =
await _dbHelper.getDailyRecordsForExercise(exercise);
setState(() {
_selectedExercise = exercise;
_dailyRecords = dailyRecords;
_isExerciseView = true;
});
Navigator.of(context).pop();
},
);
},
);
},
);
}

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final cardColor = theme.cardColor;
final secondaryColor = theme.colorScheme.secondary;
final primaryColor = theme.colorScheme.primary;
final textColor = theme.textTheme.bodyLarge?.color ?? Colors.black;

return Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
Expand All @@ -98,57 +139,143 @@ class _SummaryTabState extends State<SummaryTab> {
children: [
Row(
children: [
const Text('Select Day: '),
TextButton(
onPressed: () => _showCalendarModal(context),
child: Text(
'${widget.selectedDay.year}-${widget.selectedDay.month}-${widget.selectedDay.day}'),
_isExerciseView
? const Text('Select Exercise: ')
: const Text('Select Day: '),
_isExerciseView
? TextButton(
onPressed: () => _showExerciseSelectionModal(context),
child: Text(_selectedExercise ?? 'None',
style: TextStyle(color: textColor)),
)
: TextButton(
onPressed: () => _showCalendarModal(context),
child: Text(
'${widget.selectedDay.year}-${widget.selectedDay.month}-${widget.selectedDay.day}',
style: TextStyle(color: textColor)),
),
SizedBox(width: 8), // Add space between button and icon
IconButton(
icon: Icon(
_isExerciseView
? Icons.calendar_today
: Icons.fitness_center,
color: primaryColor,
),
onPressed: () {
setState(() {
_isExerciseView = !_isExerciseView;
if (!_isExerciseView) {
// Clear exercise data when switching to Day View
_selectedExercise = null;
_dailyRecords = {};
}
});
},
),
],
),
FutureBuilder<Map<String, dynamic>>(
future: _dbHelper.getSummaryForDay(widget.selectedDay),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Text('No data available for selected day');
}
const SizedBox(height: 20),
_isExerciseView && _selectedExercise != null
? _dailyRecords.isEmpty
? Text('No data available for selected exercise',
style: TextStyle(color: textColor))
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _dailyRecords.entries.map((entry) {
final day = entry.key;
final records = entry.value;

return Card(
margin: const EdgeInsets.symmetric(vertical: 8.0),
color: cardColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.2),
borderRadius: BorderRadius.vertical(
top: Radius.circular(4)),
),
child: Text(
'${day.year}-${day.month}-${day.day}',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: primaryColor,
),
),
),
...records.map((record) {
return ListTile(
title: Text(
'Sets: ${record['sets']}, Reps: ${record['reps']}, Weight: ${record['weight']} kg'),
subtitle: Text(
'Timestamp: ${record['timestamp']}'),
);
}).toList(),
],
),
);
}).toList(),
)
: _isExerciseView
? Text('Please select an exercise to view data.',
style: TextStyle(color: textColor))
: FutureBuilder<Map<String, dynamic>>(
future: _dbHelper.getSummaryForDay(widget.selectedDay),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}',
style: TextStyle(color: textColor));
} else if (!snapshot.hasData ||
snapshot.data!.isEmpty) {
return Text('No data available for selected day',
style: TextStyle(color: textColor));
}

final summaryData = snapshot.data!;
final summaryData = snapshot.data!;

return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: summaryData.entries.map((entry) {
final exercise = entry.key;
final details = entry.value as Map<String, dynamic>;
final totalWeight = details['totalWeight'];
final totalSets = details['totalSets'];
final totalReps = details['totalReps'];
final avgWeight = details['avgWeight'];
final records =
details['records'] as List<Map<String, dynamic>>;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: summaryData.entries.map((entry) {
final exercise = entry.key;
final details =
entry.value as Map<String, dynamic>;
final totalWeight = details['totalWeight'];
final totalSets = details['totalSets'];
final totalReps = details['totalReps'];
final avgWeight = details['avgWeight'];
final records = details['records']
as List<Map<String, dynamic>>;

return Card(
child: ExpansionTile(
title: Text(exercise),
subtitle: Text(
'Total Weight: ${totalWeight.toStringAsFixed(1)} kg, Sets: $totalSets, Reps: $totalReps, Avg Weight per Set: ${avgWeight.toStringAsFixed(1)} kg'),
children: records.map((record) {
return ListTile(
title: Text(
'Sets: ${record['sets']}, Reps: ${record['reps']}, Weight: ${record['weight']} kg'),
subtitle: Text('Timestamp: ${record['timestamp']}'),
return Card(
color: cardColor,
child: ExpansionTile(
title: Text(exercise,
style: TextStyle(color: textColor)),
subtitle: Text(
'Total Weight: ${totalWeight.toStringAsFixed(1)} kg, Total Sets: $totalSets, Total Reps: $totalReps, Avg Weight: ${avgWeight.toStringAsFixed(1)} kg',
style: TextStyle(color: textColor)),
children: records.map((record) {
return ListTile(
title: Text(
'Sets: ${record['sets']}, Reps: ${record['reps']}, Weight: ${record['weight']} kg'),
subtitle: Text(
'Timestamp: ${record['timestamp']}'),
);
}).toList(),
),
);
}).toList(),
);
}).toList(),
},
),
);
}).toList(),
);
},
),
],
),
),
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.4.4+18
version: 0.4.5+19

environment:
sdk: '>=3.4.3 <4.0.0'
Expand Down
Loading