Skip to content
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
5 changes: 4 additions & 1 deletion workout-logger/lib/screens/profile_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,10 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.lg,
vertical: AppSpacing.md,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
Expand Down
92 changes: 32 additions & 60 deletions workout-logger/lib/screens/settings_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,28 @@ class _SettingsScreenState extends State<SettingsScreen> {
bool _isExporting = false;
bool _isImporting = false;

// ==================== Feedback helpers ====================

void _showErrorSnackBar(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: AppTheme.error,
),
);
}

void _showSuccessSnackBar(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: AppTheme.success,
),
);
}

// ==================== Remote Backup ====================

Future<void> _performBackup() async {
Expand All @@ -37,32 +59,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
await api.trackEvent('backup_triggered').catchError((_) => null);
final success = await api.backupData(data);

if (!mounted) return;

if (success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Backup successful!'),
backgroundColor: AppTheme.success,
),
);
_showSuccessSnackBar('Backup successful!');
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Backup failed. Please try again.'),
backgroundColor: AppTheme.error,
),
);
_showErrorSnackBar('Backup failed. Please try again.');
}
} catch (e, stackTrace) {
debugPrint('Backup error: $e\n$stackTrace');
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Something went wrong. Please try again.'),
backgroundColor: AppTheme.error,
),
);
_showErrorSnackBar('Something went wrong. Please try again.');
} finally {
if (mounted) {
setState(() => _isBackingUp = false);
Expand All @@ -89,26 +93,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
XFile(file.path),
], subject: 'RepForge Backup');

if (!mounted) return;

if (result.status == ShareResultStatus.success ||
result.status == ShareResultStatus.dismissed) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Backup file exported successfully!'),
backgroundColor: AppTheme.success,
),
);
_showSuccessSnackBar('Backup file exported successfully!');
}
} catch (e, stackTrace) {
debugPrint('Export error: $e\n$stackTrace');
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Export failed. Please try again.'),
backgroundColor: AppTheme.error,
),
);
_showErrorSnackBar('Export failed. Please try again.');
} finally {
if (mounted) {
setState(() => _isExporting = false);
Expand Down Expand Up @@ -179,42 +170,23 @@ class _SettingsScreenState extends State<SettingsScreen> {
// Basic validation: ensure it's valid JSON with expected keys
final data = jsonDecode(jsonString) as Map<String, dynamic>;
if (!data.containsKey('sessions') && !data.containsKey('routines')) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Invalid backup file. Expected a RepForge backup.'),
backgroundColor: AppTheme.error,
),
);
_showErrorSnackBar('Invalid backup file. Expected a RepForge backup.');
return;
}

final provider = context.read<WorkoutProvider>();
await provider.importData(jsonString);

if (!mounted) return;

final itemCount = (data['sessions'] as List?)?.length ?? 0;
final routineCount = (data['routines'] as List?)?.length ?? 0;

ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Import complete! Processed $itemCount sessions, $routineCount routines.',
),
backgroundColor: AppTheme.success,
),
_showSuccessSnackBar(
'Import complete! Processed $itemCount sessions, $routineCount routines.',
);
} catch (e, stackTrace) {
debugPrint('Import error: $e\n$stackTrace');
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Import failed. Make sure you selected a valid backup file.',
),
backgroundColor: AppTheme.error,
),
_showErrorSnackBar(
'Import failed. Make sure you selected a valid backup file.',
);
} finally {
if (mounted) {
Expand Down
21 changes: 4 additions & 17 deletions workout-logger/lib/services/managers/analytics_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:flutter/foundation.dart';
import '../../models/models.dart';
import '../interfaces/storage_service_interface.dart';
import '../interfaces/ml_service_interface.dart';
import '../utils/exercise_history.dart';

/// Manages analytics and statistics for workouts.
///
Expand Down Expand Up @@ -93,28 +94,14 @@ class AnalyticsManager extends ChangeNotifier {
notifyListeners();
}

/// Get set recommendations for an exercise
/// Get set recommendations for an exercise.
///
/// Sessions are sorted newest-first to find the most recent exercise log.
/// Uses the most-recently-dated session containing this exercise.
List<SetRecommendation> getRecommendations(
String exerciseId,
List<WorkoutSession> sessions,
) {
// Sort sessions newest-first to find the most recent exercise log
final sortedSessions = List<WorkoutSession>.from(sessions)
..sort((a, b) => b.date.compareTo(a.date));

// Find last session with this exercise
ExerciseLog? lastLog;
for (var session in sortedSessions) {
for (var log in session.exercises) {
if (log.exerciseId == exerciseId) {
lastLog = log;
break;
}
}
if (lastLog != null) break;
}
final lastLog = findMostRecentExerciseLog(exerciseId, sessions);

if (lastLog == null || lastLog.sets.isEmpty) {
return _mlService.getDefaultRecommendations(3);
Expand Down
10 changes: 8 additions & 2 deletions workout-logger/lib/services/managers/history_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,18 @@ class HistoryManager extends ChangeNotifier {
.toList();
}

/// Get sessions within a date range (inclusive of start and end)
/// Get sessions within a date range (inclusive of start and end).
///
/// Tolerates inverted ranges (mirrors [StorageService.getSessionsInDateRange])
/// so callers don't silently get an empty list when start/end are passed in
/// the wrong order.
List<WorkoutSession> getSessionsInDateRange(DateTime start, DateTime end) {
final lo = start.isAfter(end) ? end : start;
final hi = start.isAfter(end) ? start : end;
return _sessions
.where(
(session) =>
!session.date.isBefore(start) && !session.date.isAfter(end),
!session.date.isBefore(lo) && !session.date.isAfter(hi),
)
.toList();
}
Expand Down
6 changes: 5 additions & 1 deletion workout-logger/lib/services/storage_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,14 @@ class StorageService implements IStorageService {
DateTime end,
) async {
final allSessions = await getAllWorkoutSessions();
// Tolerate inverted ranges so analytics callers don't silently get an
// empty list when start/end are passed in the wrong order.
final lo = start.isAfter(end) ? end : start;
final hi = start.isAfter(end) ? start : end;
return allSessions
.where(
(session) =>
!session.date.isBefore(start) && !session.date.isAfter(end),
!session.date.isBefore(lo) && !session.date.isAfter(hi),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)
.toList();
}
Expand Down
29 changes: 29 additions & 0 deletions workout-logger/lib/services/utils/exercise_history.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import '../../models/models.dart';

/// Returns the [ExerciseLog] for [exerciseId] from the most-recently-dated
/// [WorkoutSession] in [sessions], or `null` if the exercise has never been
/// logged.
///
/// Sessions are sorted by date descending before scanning so the result does
/// not depend on the storage's insertion order. Used by recommendation and
/// "last session" lookups so the two paths never disagree.
///
/// Tie-break: when two sessions share the exact same [WorkoutSession.date],
/// the choice between them is unspecified. `List.sort` is not stable in Dart,
/// so callers must not rely on a deterministic winner for equal timestamps.
ExerciseLog? findMostRecentExerciseLog(
String exerciseId,
List<WorkoutSession> sessions,
) {
final sorted = List<WorkoutSession>.from(sessions)
..sort((a, b) => b.date.compareTo(a.date));

for (final session in sorted) {
for (final log in session.exercises) {
if (log.exerciseId == exerciseId) {
return log;
}
}
}
return null;
}
29 changes: 8 additions & 21 deletions workout-logger/lib/services/workout_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import 'interfaces/ml_service_interface.dart';
import 'ml_service.dart';
import 'strategies/target_calculator.dart';
import 'managers/program_manager.dart';
import 'utils/exercise_history.dart';

class WorkoutProvider extends ChangeNotifier {
final IStorageService _storage;
Expand Down Expand Up @@ -409,19 +410,12 @@ class WorkoutProvider extends ChangeNotifier {

// ==================== RECOMMENDATIONS ====================

/// Get set recommendations for an exercise
/// Get set recommendations for an exercise.
///
/// Uses the most-recently-dated session that contains this exercise as the
/// basis for the recommendation. Order in `_sessions` is not assumed.
List<SetRecommendation> getRecommendations(String exerciseId) {
// Find last session with this exercise
ExerciseLog? lastLog;
for (var session in _sessions) {
for (var log in session.exercises) {
if (log.exerciseId == exerciseId) {
lastLog = log;
break;
}
}
if (lastLog != null) break;
}
final lastLog = findMostRecentExerciseLog(exerciseId, _sessions);

if (lastLog == null || lastLog.sets.isEmpty) {
return _mlService.getDefaultRecommendations(3);
Expand All @@ -433,16 +427,9 @@ class WorkoutProvider extends ChangeNotifier {
);
}

/// Get last session data for an exercise
/// Get the most recent exercise log for [exerciseId], or null if never logged.
ExerciseLog? getLastSessionForExercise(String exerciseId) {
for (var session in _sessions) {
for (var log in session.exercises) {
if (log.exerciseId == exerciseId) {
return log;
}
}
}
return null;
return findMostRecentExerciseLog(exerciseId, _sessions);
}

// ==================== SESSION MANAGEMENT ====================
Expand Down
2 changes: 1 addition & 1 deletion workout-logger/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ version: 1.0.14+15

environment:
sdk: ^3.9.2
flutter: 3.41.5
flutter: 3.41.6

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
Expand Down
Loading