feat: implement autoregulation for kettlebell training
This commit is contained in:
parent
a3e36b1975
commit
4fac48e81e
9 changed files with 417 additions and 117 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:kettlebell_tracker/providers/settings_provider.dart';
|
||||||
import 'package:kettlebell_tracker/screens/home_screen.dart';
|
import 'package:kettlebell_tracker/screens/home_screen.dart';
|
||||||
import 'package:kettlebell_tracker/screens/settings_screen.dart';
|
import 'package:kettlebell_tracker/screens/settings_screen.dart';
|
||||||
import 'package:kettlebell_tracker/screens/training_screen.dart';
|
import 'package:kettlebell_tracker/screens/training_screen.dart';
|
||||||
|
|
@ -58,6 +59,14 @@ class _MainScreenState extends ConsumerState<MainScreen> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
Future.microtask(() {
|
||||||
|
ref.read(settingsProvider.notifier).loadSettings();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ class TrainingSession {
|
||||||
final double weightRight;
|
final double weightRight;
|
||||||
final int repsPerSet;
|
final int repsPerSet;
|
||||||
final int duration; // in seconds
|
final int duration; // in seconds
|
||||||
final String notes;
|
final String program;
|
||||||
|
final int blockDay;
|
||||||
|
|
||||||
TrainingSession({
|
TrainingSession({
|
||||||
this.id,
|
this.id,
|
||||||
|
|
@ -16,7 +17,8 @@ class TrainingSession {
|
||||||
required this.weightRight,
|
required this.weightRight,
|
||||||
required this.repsPerSet,
|
required this.repsPerSet,
|
||||||
required this.duration,
|
required this.duration,
|
||||||
required this.notes,
|
required this.program,
|
||||||
|
required this.blockDay,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convert a TrainingSession into a Map. The keys must correspond to the names of the
|
// Convert a TrainingSession into a Map. The keys must correspond to the names of the
|
||||||
|
|
@ -30,7 +32,8 @@ class TrainingSession {
|
||||||
'weightRight': weightRight,
|
'weightRight': weightRight,
|
||||||
'repsPerSet': repsPerSet,
|
'repsPerSet': repsPerSet,
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
'notes': notes,
|
'program': program,
|
||||||
|
'blockDay': blockDay,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,12 +47,37 @@ class TrainingSession {
|
||||||
weightRight: map['weightRight'],
|
weightRight: map['weightRight'],
|
||||||
repsPerSet: map['repsPerSet'],
|
repsPerSet: map['repsPerSet'],
|
||||||
duration: map['duration'],
|
duration: map['duration'],
|
||||||
notes: map['notes'],
|
program: map['program'] ?? 'giant_1.0',
|
||||||
|
blockDay: map['blockDay'] ?? 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TrainingSession copyWith({
|
||||||
|
int? id,
|
||||||
|
DateTime? date,
|
||||||
|
int? sets,
|
||||||
|
double? weightLeft,
|
||||||
|
double? weightRight,
|
||||||
|
int? repsPerSet,
|
||||||
|
int? duration,
|
||||||
|
String? program,
|
||||||
|
int? blockDay,
|
||||||
|
}) {
|
||||||
|
return TrainingSession(
|
||||||
|
id: id ?? this.id,
|
||||||
|
date: date ?? this.date,
|
||||||
|
sets: sets ?? this.sets,
|
||||||
|
weightLeft: weightLeft ?? this.weightLeft,
|
||||||
|
weightRight: weightRight ?? this.weightRight,
|
||||||
|
repsPerSet: repsPerSet ?? this.repsPerSet,
|
||||||
|
duration: duration ?? this.duration,
|
||||||
|
program: program ?? this.program,
|
||||||
|
blockDay: blockDay ?? this.blockDay,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'TrainingSession{id: $id, date: $date, sets: $sets, notes: $notes}';
|
return 'TrainingSession{id: $id, date: $date, sets: $sets}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,34 @@
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
class SettingsState {
|
class SettingsState {
|
||||||
final int trainingTimeMinutes;
|
final int trainingTimeMinutes;
|
||||||
final double weightLeft;
|
final double weightLeft;
|
||||||
final double weightRight;
|
final double weightRight;
|
||||||
final int repsPerSet;
|
|
||||||
final int goalSets;
|
final int goalSets;
|
||||||
final String notes;
|
final String initialProgram;
|
||||||
|
|
||||||
SettingsState({
|
SettingsState({
|
||||||
this.trainingTimeMinutes = 20,
|
this.trainingTimeMinutes = 20,
|
||||||
this.weightLeft = 16.0,
|
this.weightLeft = 16.0,
|
||||||
this.weightRight = 16.0,
|
this.weightRight = 16.0,
|
||||||
this.repsPerSet = 5,
|
|
||||||
this.goalSets = 5,
|
this.goalSets = 5,
|
||||||
this.notes = "",
|
this.initialProgram = 'giant_1.0',
|
||||||
});
|
});
|
||||||
|
|
||||||
SettingsState copyWith({
|
SettingsState copyWith({
|
||||||
int? trainingTimeMinutes,
|
int? trainingTimeMinutes,
|
||||||
double? weightLeft,
|
double? weightLeft,
|
||||||
double? weightRight,
|
double? weightRight,
|
||||||
int? repsPerSet,
|
|
||||||
int? goalSets,
|
int? goalSets,
|
||||||
String? notes,
|
String? initialProgram,
|
||||||
}) {
|
}) {
|
||||||
return SettingsState(
|
return SettingsState(
|
||||||
trainingTimeMinutes: trainingTimeMinutes ?? this.trainingTimeMinutes,
|
trainingTimeMinutes: trainingTimeMinutes ?? this.trainingTimeMinutes,
|
||||||
weightLeft: weightLeft ?? this.weightLeft,
|
weightLeft: weightLeft ?? this.weightLeft,
|
||||||
weightRight: weightRight ?? this.weightRight,
|
weightRight: weightRight ?? this.weightRight,
|
||||||
repsPerSet: repsPerSet ?? this.repsPerSet,
|
|
||||||
goalSets: goalSets ?? this.goalSets,
|
goalSets: goalSets ?? this.goalSets,
|
||||||
notes: notes ?? this.notes,
|
initialProgram: initialProgram ?? this.initialProgram,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -39,9 +36,30 @@ class SettingsState {
|
||||||
class SettingsNotifier extends StateNotifier<SettingsState> {
|
class SettingsNotifier extends StateNotifier<SettingsState> {
|
||||||
SettingsNotifier() : super(SettingsState());
|
SettingsNotifier() : super(SettingsState());
|
||||||
|
|
||||||
void updateSettings(SettingsState newSettings) {
|
Future<void> loadSettings() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
state = SettingsState(
|
||||||
|
trainingTimeMinutes: prefs.getInt('trainingTimeMinutes') ?? 20,
|
||||||
|
weightLeft: prefs.getDouble('weightLeft') ?? 16.0,
|
||||||
|
weightRight: prefs.getDouble('weightRight') ?? 16.0,
|
||||||
|
goalSets: prefs.getInt('goalSets') ?? 5,
|
||||||
|
initialProgram: prefs.getString('initialProgram') ?? 'giant_1.0',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveSettings(SettingsState newSettings) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setInt('trainingTimeMinutes', newSettings.trainingTimeMinutes);
|
||||||
|
await prefs.setDouble('weightLeft', newSettings.weightLeft);
|
||||||
|
await prefs.setDouble('weightRight', newSettings.weightRight);
|
||||||
|
await prefs.setInt('goalSets', newSettings.goalSets);
|
||||||
|
await prefs.setString('initialProgram', newSettings.initialProgram);
|
||||||
state = newSettings;
|
state = newSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updateSettings(SettingsState newSettings) async {
|
||||||
|
await saveSettings(newSettings);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final settingsProvider = StateNotifierProvider<SettingsNotifier, SettingsState>(
|
final settingsProvider = StateNotifierProvider<SettingsNotifier, SettingsState>(
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@ class TrainingState {
|
||||||
final double progress;
|
final double progress;
|
||||||
final int secondsSinceLastSet;
|
final int secondsSinceLastSet;
|
||||||
final DateTime? lastSetTimestamp;
|
final DateTime? lastSetTimestamp;
|
||||||
|
final String currentProgram;
|
||||||
|
final int currentBlockDay;
|
||||||
|
final int currentReps;
|
||||||
|
final int totalTrainingDays;
|
||||||
|
|
||||||
TrainingState({
|
TrainingState({
|
||||||
this.isTrainingRunning = false,
|
this.isTrainingRunning = false,
|
||||||
|
|
@ -28,6 +32,10 @@ class TrainingState {
|
||||||
this.progress = 0.0,
|
this.progress = 0.0,
|
||||||
this.secondsSinceLastSet = 0,
|
this.secondsSinceLastSet = 0,
|
||||||
this.lastSetTimestamp,
|
this.lastSetTimestamp,
|
||||||
|
this.currentProgram = 'giant_1.0',
|
||||||
|
this.currentBlockDay = 1,
|
||||||
|
this.currentReps = 5,
|
||||||
|
this.totalTrainingDays = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
TrainingState copyWith({
|
TrainingState copyWith({
|
||||||
|
|
@ -42,6 +50,10 @@ class TrainingState {
|
||||||
int? secondsSinceLastSet,
|
int? secondsSinceLastSet,
|
||||||
DateTime? lastSetTimestamp,
|
DateTime? lastSetTimestamp,
|
||||||
bool clearLastSetTimestamp = false,
|
bool clearLastSetTimestamp = false,
|
||||||
|
String? currentProgram,
|
||||||
|
int? currentBlockDay,
|
||||||
|
int? currentReps,
|
||||||
|
int? totalTrainingDays,
|
||||||
}) {
|
}) {
|
||||||
return TrainingState(
|
return TrainingState(
|
||||||
isTrainingRunning: isTrainingRunning ?? this.isTrainingRunning,
|
isTrainingRunning: isTrainingRunning ?? this.isTrainingRunning,
|
||||||
|
|
@ -57,6 +69,10 @@ class TrainingState {
|
||||||
lastSetTimestamp: clearLastSetTimestamp
|
lastSetTimestamp: clearLastSetTimestamp
|
||||||
? null
|
? null
|
||||||
: lastSetTimestamp ?? this.lastSetTimestamp,
|
: lastSetTimestamp ?? this.lastSetTimestamp,
|
||||||
|
currentProgram: currentProgram ?? this.currentProgram,
|
||||||
|
currentBlockDay: currentBlockDay ?? this.currentBlockDay,
|
||||||
|
currentReps: currentReps ?? this.currentReps,
|
||||||
|
totalTrainingDays: totalTrainingDays ?? this.totalTrainingDays,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -65,14 +81,71 @@ class TrainingNotifier extends StateNotifier<TrainingState> {
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
TrainingNotifier(this.ref) : super(TrainingState());
|
TrainingNotifier(this.ref) : super(TrainingState());
|
||||||
|
|
||||||
void startTraining(int minutes, int reps, int goal) {
|
void _updateProgram() {
|
||||||
|
int newTotalDays = state.totalTrainingDays + 1;
|
||||||
|
String newProgram = state.currentProgram;
|
||||||
|
int newDay = (state.currentBlockDay % 3) + 1;
|
||||||
|
int newReps = state.currentReps;
|
||||||
|
|
||||||
|
if (newTotalDays > 0 && newTotalDays % 12 == 0) {
|
||||||
|
switch (state.currentProgram) {
|
||||||
|
case 'giant_1.0':
|
||||||
|
newProgram = 'giant_1.1';
|
||||||
|
break;
|
||||||
|
case 'giant_1.1':
|
||||||
|
newProgram = 'giant_1.2';
|
||||||
|
break;
|
||||||
|
case 'giant_1.2':
|
||||||
|
newProgram = 'ksk_1.0';
|
||||||
|
break;
|
||||||
|
case 'ksk_1.0':
|
||||||
|
newProgram = 'ksk_1.1';
|
||||||
|
break;
|
||||||
|
case 'ksk_1.1':
|
||||||
|
newProgram = 'ksk_1.2';
|
||||||
|
break;
|
||||||
|
case 'ksk_1.2':
|
||||||
|
newProgram = 'giant_1.0';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
newProgram = 'giant_1.0';
|
||||||
|
}
|
||||||
|
newDay = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newProgram == 'giant_1.0') {
|
||||||
|
newReps = [5, 6, 4][newDay - 1];
|
||||||
|
} else if (newProgram == 'giant_1.1') {
|
||||||
|
newReps = [6, 8, 7][newDay - 1];
|
||||||
|
} else if (newProgram == 'giant_1.2') {
|
||||||
|
newReps = [7, 9, 8][newDay - 1];
|
||||||
|
} else if (newProgram == 'ksk_1.0') {
|
||||||
|
newReps = [5, 6, 4][newDay - 1];
|
||||||
|
} else if (newProgram == 'ksk_1.1') {
|
||||||
|
newReps = [6, 8, 7][newDay - 1];
|
||||||
|
} else if (newProgram == 'ksk_1.2') {
|
||||||
|
newReps = [7, 9, 8][newDay - 1];
|
||||||
|
} else {
|
||||||
|
newReps = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.copyWith(
|
||||||
|
currentProgram: newProgram,
|
||||||
|
currentBlockDay: newDay,
|
||||||
|
currentReps: newReps,
|
||||||
|
totalTrainingDays: newTotalDays,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void startTraining(int minutes, int goal) {
|
||||||
|
_updateProgram();
|
||||||
final duration = minutes * 60;
|
final duration = minutes * 60;
|
||||||
state = TrainingState(
|
state = state.copyWith(
|
||||||
isTrainingRunning: true,
|
isTrainingRunning: true,
|
||||||
initialDurationSeconds: duration,
|
initialDurationSeconds: duration,
|
||||||
remainingSeconds: duration,
|
remainingSeconds: duration,
|
||||||
repsPerSet: reps,
|
|
||||||
goalSets: goal,
|
goalSets: goal,
|
||||||
|
repsPerSet: state.currentReps,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,11 +180,19 @@ class TrainingNotifier extends StateNotifier<TrainingState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> finishTraining(TrainingSession session) async {
|
Future<void> finishTraining(TrainingSession session) async {
|
||||||
await DatabaseHelper().saveTraining(session);
|
final updatedSession = session.copyWith(
|
||||||
await sendTrainingToBackend(
|
program: state.currentProgram,
|
||||||
reps: session.repsPerSet,
|
blockDay: state.currentBlockDay,
|
||||||
rest: session.duration / session.sets,
|
);
|
||||||
sets: session.sets);
|
await DatabaseHelper().saveTraining(updatedSession);
|
||||||
|
try {
|
||||||
|
await sendTrainingToBackend(
|
||||||
|
reps: session.repsPerSet,
|
||||||
|
rest: session.duration / session.sets,
|
||||||
|
sets: session.sets);
|
||||||
|
} catch (e) {
|
||||||
|
print("Error sending information to backend");
|
||||||
|
}
|
||||||
ref.refresh(historyProvider);
|
ref.refresh(historyProvider);
|
||||||
resetTraining();
|
resetTraining();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,11 +86,6 @@ class HistoryItemCard extends StatelessWidget {
|
||||||
'Reps pro Satz: ${session.repsPerSet}'),
|
'Reps pro Satz: ${session.repsPerSet}'),
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
Icons.timer, 'Dauer: ${formatDuration(session.duration)}'),
|
Icons.timer, 'Dauer: ${formatDuration(session.duration)}'),
|
||||||
if (session.notes.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildInfoRow(Icons.note, 'Notizen: ${session.notes}',
|
|
||||||
isNote: true),
|
|
||||||
]
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||||
late TextEditingController _trainingTimeController;
|
late TextEditingController _trainingTimeController;
|
||||||
late TextEditingController _weightLeftController;
|
late TextEditingController _weightLeftController;
|
||||||
late TextEditingController _weightRightController;
|
late TextEditingController _weightRightController;
|
||||||
late TextEditingController _repsController;
|
|
||||||
late TextEditingController _goalSetsController;
|
late TextEditingController _goalSetsController;
|
||||||
late TextEditingController _notesController;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -35,13 +33,9 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||||
_weightRightController = TextEditingController(
|
_weightRightController = TextEditingController(
|
||||||
text: settings.weightRight.toString(),
|
text: settings.weightRight.toString(),
|
||||||
);
|
);
|
||||||
_repsController = TextEditingController(
|
|
||||||
text: settings.repsPerSet.toString(),
|
|
||||||
);
|
|
||||||
_goalSetsController = TextEditingController(
|
_goalSetsController = TextEditingController(
|
||||||
text: settings.goalSets.toString(),
|
text: settings.goalSets.toString(),
|
||||||
);
|
);
|
||||||
_notesController = TextEditingController(text: settings.notes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -49,9 +43,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||||
_trainingTimeController.dispose();
|
_trainingTimeController.dispose();
|
||||||
_weightLeftController.dispose();
|
_weightLeftController.dispose();
|
||||||
_weightRightController.dispose();
|
_weightRightController.dispose();
|
||||||
_repsController.dispose();
|
|
||||||
_goalSetsController.dispose();
|
_goalSetsController.dispose();
|
||||||
_notesController.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,9 +53,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||||
trainingTimeMinutes: int.parse(_trainingTimeController.text),
|
trainingTimeMinutes: int.parse(_trainingTimeController.text),
|
||||||
weightLeft: double.parse(_weightLeftController.text),
|
weightLeft: double.parse(_weightLeftController.text),
|
||||||
weightRight: double.parse(_weightRightController.text),
|
weightRight: double.parse(_weightRightController.text),
|
||||||
repsPerSet: int.parse(_repsController.text),
|
|
||||||
goalSets: int.parse(_goalSetsController.text),
|
goalSets: int.parse(_goalSetsController.text),
|
||||||
notes: _notesController.text,
|
|
||||||
);
|
);
|
||||||
ref.read(settingsProvider.notifier).updateSettings(newSettings);
|
ref.read(settingsProvider.notifier).updateSettings(newSettings);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|
@ -123,12 +113,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
CustomTextField(
|
|
||||||
controller: _repsController,
|
|
||||||
labelText: "Reps pro Satz",
|
|
||||||
icon: Icons.repeat,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
controller: _goalSetsController,
|
controller: _goalSetsController,
|
||||||
|
|
@ -139,25 +123,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
|
||||||
CustomCard(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Zusätzliche Notizen",
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
CustomTextField(
|
|
||||||
controller: _notesController,
|
|
||||||
labelText: "Notizen für nächstes Training",
|
|
||||||
icon: Icons.note,
|
|
||||||
maxLines: 4,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: _saveSettings,
|
onPressed: _saveSettings,
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ class _TrainingScreenState extends ConsumerState<TrainingScreen> {
|
||||||
final settings = ref.read(settingsProvider);
|
final settings = ref.read(settingsProvider);
|
||||||
ref.read(trainingProvider.notifier).startTraining(
|
ref.read(trainingProvider.notifier).startTraining(
|
||||||
settings.trainingTimeMinutes,
|
settings.trainingTimeMinutes,
|
||||||
settings.repsPerSet,
|
|
||||||
settings.goalSets,
|
settings.goalSets,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -82,9 +81,10 @@ class _TrainingScreenState extends ConsumerState<TrainingScreen> {
|
||||||
sets: state.setsDone,
|
sets: state.setsDone,
|
||||||
weightLeft: settings.weightLeft,
|
weightLeft: settings.weightLeft,
|
||||||
weightRight: settings.weightRight,
|
weightRight: settings.weightRight,
|
||||||
repsPerSet: settings.repsPerSet,
|
repsPerSet: state.currentReps,
|
||||||
duration: state.initialDurationSeconds - state.remainingSeconds,
|
duration: state.initialDurationSeconds - state.remainingSeconds,
|
||||||
notes: settings.notes,
|
program: state.currentProgram,
|
||||||
|
blockDay: state.currentBlockDay,
|
||||||
);
|
);
|
||||||
|
|
||||||
ref.read(trainingProvider.notifier).finishTraining(session);
|
ref.read(trainingProvider.notifier).finishTraining(session);
|
||||||
|
|
@ -121,7 +121,6 @@ class _TrainingScreenState extends ConsumerState<TrainingScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final trainingState = ref.watch(trainingProvider);
|
final trainingState = ref.watch(trainingProvider);
|
||||||
final settings = ref.watch(settingsProvider);
|
|
||||||
final bool isTrainingRunning = trainingState.isTrainingRunning;
|
final bool isTrainingRunning = trainingState.isTrainingRunning;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -129,47 +128,66 @@ class _TrainingScreenState extends ConsumerState<TrainingScreen> {
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
// Header-Bereich mit Programm-Infos
|
||||||
CustomCard(
|
CustomCard(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Verbleibende Zeit: ${_formatDuration(trainingState.remainingSeconds)}',
|
'${trainingState.currentProgram.toUpperCase()}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 26,
|
fontSize: 28,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: AppTheme.oneDarkPrimary,
|
color: AppTheme.oneDarkPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
Text(
|
||||||
'Sätze: ${trainingState.setsDone}',
|
'Block Tag: ${trainingState.currentBlockDay}',
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
color: AppTheme.oneDarkYellow,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Reps pro Satz: ${settings.repsPerSet}',
|
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
color: AppTheme.oneDarkAccent,
|
color: AppTheme.oneDarkAccent,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
Text(
|
||||||
'Zeit seit letztem Satz: ${_formatDuration(trainingState.secondsSinceLastSet)}',
|
'Reps pro Satz: ${trainingState.currentReps}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: AppTheme.oneDarkYellow,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
|
||||||
|
// Timer und Fortschritt
|
||||||
|
CustomCard(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Verbleibende Zeit',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
color: AppTheme.oneDarkTextWeak,
|
color: AppTheme.oneDarkTextWeak,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
Text(
|
||||||
|
_formatDuration(trainingState.remainingSeconds),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 40,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppTheme.oneDarkPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
value: trainingState.progress,
|
value: trainingState.progress,
|
||||||
minHeight: 10,
|
minHeight: 12,
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
color: AppTheme.oneDarkGreen,
|
||||||
|
backgroundColor: AppTheme.oneDarkTextWeak,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -182,51 +200,58 @@ class _TrainingScreenState extends ConsumerState<TrainingScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 18),
|
||||||
|
|
||||||
|
// Aktions-Buttons
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: isTrainingRunning ? null : _startTraining,
|
onPressed: isTrainingRunning ? null : _startTraining,
|
||||||
icon: const Icon(Icons.play_arrow),
|
icon: const Icon(Icons.play_arrow, size: 28),
|
||||||
label: const Text('Start'),
|
label: const Text('Start', style: TextStyle(fontSize: 18)),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppTheme.oneDarkGreen,
|
backgroundColor: AppTheme.oneDarkGreen,
|
||||||
foregroundColor: AppTheme.oneDarkTextWeak,
|
foregroundColor: Colors.white,
|
||||||
|
minimumSize: const Size(110, 48),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: !isTrainingRunning ? null : _completeSet,
|
onPressed: !isTrainingRunning ? null : _completeSet,
|
||||||
icon: const Icon(Icons.check_circle),
|
icon: const Icon(Icons.check_circle, size: 28),
|
||||||
label: const Text('Satz'),
|
label: const Text('Satz', style: TextStyle(fontSize: 18)),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppTheme.oneDarkPrimary,
|
backgroundColor: AppTheme.oneDarkPrimary,
|
||||||
foregroundColor: AppTheme.oneDarkTextWeak,
|
foregroundColor: Colors.white,
|
||||||
|
minimumSize: const Size(110, 48),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: !isTrainingRunning ? null : _finishTraining,
|
onPressed: !isTrainingRunning ? null : _finishTraining,
|
||||||
icon: const Icon(Icons.stop),
|
icon: const Icon(Icons.stop, size: 28),
|
||||||
label: const Text('Beenden'),
|
label: const Text('Beenden', style: TextStyle(fontSize: 18)),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppTheme.oneDarkRed,
|
backgroundColor: AppTheme.oneDarkRed,
|
||||||
foregroundColor: AppTheme.oneDarkTextWeak,
|
foregroundColor: Colors.white,
|
||||||
|
minimumSize: const Size(110, 48),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 22),
|
||||||
const Divider(),
|
|
||||||
|
// Satz-Historie
|
||||||
Text(
|
Text(
|
||||||
"Satz-Historie",
|
"Satz-Historie",
|
||||||
style: Theme.of(
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
context,
|
color: AppTheme.oneDarkAccent,
|
||||||
).textTheme.titleLarge?.copyWith(color: AppTheme.oneDarkAccent),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
CustomCard(
|
CustomCard(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 150,
|
height: 180,
|
||||||
child: trainingState.setTimes.isEmpty
|
child: trainingState.setTimes.isEmpty
|
||||||
? const Center(
|
? const Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -234,13 +259,25 @@ class _TrainingScreenState extends ConsumerState<TrainingScreen> {
|
||||||
style: TextStyle(color: AppTheme.oneDarkTextWeak),
|
style: TextStyle(color: AppTheme.oneDarkTextWeak),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: ListView.builder(
|
: ListView.separated(
|
||||||
itemCount: trainingState.setTimes.length,
|
itemCount: trainingState.setTimes.length,
|
||||||
|
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final setTime = trainingState.setTimes[index];
|
final setTime = trainingState.setTimes[index];
|
||||||
return Center(
|
return ListTile(
|
||||||
child: Text(
|
leading: CircleAvatar(
|
||||||
'#${index + 1} um ${DateFormat.Hms().format(setTime)} (${settings.repsPerSet} Reps)',
|
backgroundColor: AppTheme.oneDarkGreen,
|
||||||
|
child: Text(
|
||||||
|
'${index + 1}',
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'um ${DateFormat.Hms().format(setTime)}',
|
||||||
|
style: const TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
trailing: Text(
|
||||||
|
'${trainingState.currentReps} Reps',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: AppTheme.oneDarkGreen,
|
color: AppTheme.oneDarkGreen,
|
||||||
|
|
@ -256,4 +293,156 @@ class _TrainingScreenState extends ConsumerState<TrainingScreen> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @override
|
||||||
|
// Widget build(BuildContext context) {
|
||||||
|
// final trainingState = ref.watch(trainingProvider);
|
||||||
|
// final settings = ref.watch(settingsProvider);
|
||||||
|
// final bool isTrainingRunning = trainingState.isTrainingRunning;
|
||||||
|
//
|
||||||
|
// return Scaffold(
|
||||||
|
// appBar: AppBar(title: const Text('Aktuelles Training')),
|
||||||
|
// body: SingleChildScrollView(
|
||||||
|
// padding: const EdgeInsets.all(16.0),
|
||||||
|
// child: Column(
|
||||||
|
// children: [
|
||||||
|
// CustomCard(
|
||||||
|
// child: Column(
|
||||||
|
// children: [
|
||||||
|
// Text(
|
||||||
|
// 'Programm: ${trainingState.currentProgram.toUpperCase()}',
|
||||||
|
// style: const TextStyle(
|
||||||
|
// fontSize: 20,
|
||||||
|
// color: AppTheme.oneDarkYellow,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// Text(
|
||||||
|
// 'Block Tag: ${trainingState.currentBlockDay}',
|
||||||
|
// style: const TextStyle(
|
||||||
|
// fontSize: 18,
|
||||||
|
// color: AppTheme.oneDarkAccent,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// Text(
|
||||||
|
// 'Verbleibende Zeit: ${_formatDuration(trainingState.remainingSeconds)}',
|
||||||
|
// style: const TextStyle(
|
||||||
|
// fontSize: 26,
|
||||||
|
// fontWeight: FontWeight.bold,
|
||||||
|
// color: AppTheme.oneDarkPrimary,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 12),
|
||||||
|
// Text(
|
||||||
|
// 'Sätze: ${trainingState.setsDone}',
|
||||||
|
// style: const TextStyle(
|
||||||
|
// fontSize: 24,
|
||||||
|
// color: AppTheme.oneDarkYellow,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 8),
|
||||||
|
// Text(
|
||||||
|
// 'Reps pro Satz: ${trainingState.currentReps}',
|
||||||
|
// style: const TextStyle(
|
||||||
|
// fontSize: 20,
|
||||||
|
// color: AppTheme.oneDarkAccent,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 12),
|
||||||
|
// Text(
|
||||||
|
// 'Zeit seit letztem Satz: ${_formatDuration(trainingState.secondsSinceLastSet)}',
|
||||||
|
// style: const TextStyle(
|
||||||
|
// fontSize: 18,
|
||||||
|
// color: AppTheme.oneDarkTextWeak,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 20),
|
||||||
|
// LinearProgressIndicator(
|
||||||
|
// value: trainingState.progress,
|
||||||
|
// minHeight: 10,
|
||||||
|
// borderRadius: BorderRadius.circular(5),
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 8),
|
||||||
|
// Text(
|
||||||
|
// 'Fortschritt: ${(trainingState.progress * 100).toStringAsFixed(0)}%',
|
||||||
|
// style: const TextStyle(
|
||||||
|
// fontSize: 16,
|
||||||
|
// color: AppTheme.oneDarkText,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 20),
|
||||||
|
// Row(
|
||||||
|
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
// children: [
|
||||||
|
// ElevatedButton.icon(
|
||||||
|
// onPressed: isTrainingRunning ? null : _startTraining,
|
||||||
|
// icon: const Icon(Icons.play_arrow),
|
||||||
|
// label: const Text('Start'),
|
||||||
|
// style: ElevatedButton.styleFrom(
|
||||||
|
// backgroundColor: AppTheme.oneDarkGreen,
|
||||||
|
// foregroundColor: AppTheme.oneDarkTextWeak,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ElevatedButton.icon(
|
||||||
|
// onPressed: !isTrainingRunning ? null : _completeSet,
|
||||||
|
// icon: const Icon(Icons.check_circle),
|
||||||
|
// label: const Text('Satz'),
|
||||||
|
// style: ElevatedButton.styleFrom(
|
||||||
|
// backgroundColor: AppTheme.oneDarkPrimary,
|
||||||
|
// foregroundColor: AppTheme.oneDarkTextWeak,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ElevatedButton.icon(
|
||||||
|
// onPressed: !isTrainingRunning ? null : _finishTraining,
|
||||||
|
// icon: const Icon(Icons.stop),
|
||||||
|
// label: const Text('Beenden'),
|
||||||
|
// style: ElevatedButton.styleFrom(
|
||||||
|
// backgroundColor: AppTheme.oneDarkRed,
|
||||||
|
// foregroundColor: AppTheme.oneDarkTextWeak,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 20),
|
||||||
|
// const Divider(),
|
||||||
|
// Text(
|
||||||
|
// "Satz-Historie",
|
||||||
|
// style: Theme.of(
|
||||||
|
// context,
|
||||||
|
// ).textTheme.titleLarge?.copyWith(color: AppTheme.oneDarkAccent),
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 10),
|
||||||
|
// CustomCard(
|
||||||
|
// child: SizedBox(
|
||||||
|
// height: 150,
|
||||||
|
// child: trainingState.setTimes.isEmpty
|
||||||
|
// ? const Center(
|
||||||
|
// child: Text(
|
||||||
|
// "Noch keine Sätze in dieser Einheit.",
|
||||||
|
// style: TextStyle(color: AppTheme.oneDarkTextWeak),
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
// : ListView.builder(
|
||||||
|
// itemCount: trainingState.setTimes.length,
|
||||||
|
// itemBuilder: (context, index) {
|
||||||
|
// final setTime = trainingState.setTimes[index];
|
||||||
|
// return Center(
|
||||||
|
// child: Text(
|
||||||
|
// '#${index + 1} um ${DateFormat.Hms().format(setTime)} (${trainingState.currentReps} Reps)',
|
||||||
|
// style: const TextStyle(
|
||||||
|
// fontSize: 16,
|
||||||
|
// color: AppTheme.oneDarkGreen,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,19 @@ Future<void> sendTrainingToBackend({
|
||||||
required int sets,
|
required int sets,
|
||||||
}) async {
|
}) async {
|
||||||
final uuid = await DeviceIdService.getOrCreateUUID();
|
final uuid = await DeviceIdService.getOrCreateUUID();
|
||||||
final url = Uri.parse('http://192.169.178.43:8000/trainings/');
|
final url = Uri.parse('http://192.169.178.43:8080/trainings/');
|
||||||
final response = await http.post(
|
final response = await http
|
||||||
url,
|
.post(
|
||||||
headers: {'Content-Type': 'application/json'},
|
url,
|
||||||
body: jsonEncode({
|
headers: {'Content-Type': 'application/json'},
|
||||||
'reps': reps,
|
body: jsonEncode({
|
||||||
'rest': rest,
|
'reps': reps,
|
||||||
'sets': sets,
|
'rest': rest,
|
||||||
'uuid': uuid,
|
'sets': sets,
|
||||||
}),
|
'uuid': uuid,
|
||||||
);
|
}),
|
||||||
|
)
|
||||||
|
.timeout(const Duration(seconds: 5));
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
print('Training erfolgreich an Backend gesendet.');
|
print('Training erfolgreich an Backend gesendet.');
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,19 @@ class DatabaseHelper {
|
||||||
final dbPath = await getDatabasesPath();
|
final dbPath = await getDatabasesPath();
|
||||||
final path = join(dbPath, 'giant_training.db');
|
final path = join(dbPath, 'giant_training.db');
|
||||||
|
|
||||||
return await openDatabase(path, version: 1, onCreate: _onCreate);
|
return await openDatabase(
|
||||||
|
path,
|
||||||
|
version: 2, // Version erhöhen!
|
||||||
|
onCreate: _onCreate,
|
||||||
|
onUpgrade: _onUpgrade,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUpgrade(Database db, int oldVersion, int newVersion) async {
|
||||||
|
if (oldVersion < 2) {
|
||||||
|
await db.execute('ALTER TABLE training ADD COLUMN program TEXT');
|
||||||
|
await db.execute('ALTER TABLE training ADD COLUMN blockDay INTEGER');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the database table
|
// Create the database table
|
||||||
|
|
@ -34,7 +46,8 @@ class DatabaseHelper {
|
||||||
weightRight REAL,
|
weightRight REAL,
|
||||||
repsPerSet INTEGER,
|
repsPerSet INTEGER,
|
||||||
duration INTEGER,
|
duration INTEGER,
|
||||||
notes TEXT
|
program TEXT,
|
||||||
|
blockDay INTEGER
|
||||||
)
|
)
|
||||||
''');
|
''');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue