Merge branch '29-provider-refactoring' into 'main'

Change State Management to Provider Package

Closes #29

See merge request Crondung/hsma_cpd!20
main
Kai Mannweiler 2023-03-02 15:23:47 +00:00
commit 4bde30f00e
14 changed files with 333 additions and 291 deletions

View File

@ -2,7 +2,9 @@ import 'package:smoke_cess_app/interface/db_record.dart';
import 'package:smoke_cess_app/models/mood.dart';
import 'package:smoke_cess_app/models/relapse.dart';
import 'package:smoke_cess_app/models/sleep.dart';
import 'package:smoke_cess_app/models/workout.dart';
import 'package:smoke_cess_app/services/database_service.dart';
// ignore: depend_on_referenced_packages
import 'package:sqflite_common/sqlite_api.dart';
class DatabaseMock implements DatabaseService {
@ -29,6 +31,12 @@ class DatabaseMock implements DatabaseService {
return Future.value(1);
}
@override
Future<int> addWorkout(Workout workout) {
_workoutRecords.add(workout);
return Future.value(1);
}
@override
Future<int> addRelapse(Relapse relapse) {
_relapseRecords.add(relapse);
@ -36,7 +44,6 @@ class DatabaseMock implements DatabaseService {
}
@override
// TODO: implement database
Future<Database> get database => DatabaseService.instance.database;
@override

View File

@ -1,32 +0,0 @@
import 'package:smoke_cess_app/interface/db_record.dart';
class HIITWorkout implements DatabaseRecord {
Duration _workoutDuration;
String _commentBefore;
String _commentAfter;
DateTime _workoutDate;
HIITWorkout(this._workoutDuration, this._commentBefore, this._commentAfter,
this._workoutDate);
//TODO Felder anpassen
@override
factory HIITWorkout.fromMap(Map<String, dynamic> map) {
return HIITWorkout(map['_workoutDuration'], map['_commentBefore'],
map['_commentAfter'], map['_workoutDate']);
}
@override
String toCSV() =>
"${_workoutDate.toIso8601String()}, $_workoutDuration, $_commentBefore, $_commentAfter";
@override
Map<String, dynamic> toMap() {
return {
'workoutDuration': _workoutDuration,
'commentBefore': _commentBefore,
'commentAfter': _commentAfter,
'workoutDate': _workoutDate,
};
}
}

View File

@ -0,0 +1,28 @@
import 'package:smoke_cess_app/interface/db_record.dart';
class Workout implements DatabaseRecord {
int _motivationBefore;
int _motivationAfter;
DateTime _workoutDate;
Workout(this._motivationBefore, this._motivationAfter, this._workoutDate);
@override
factory Workout.fromMap(Map<String, dynamic> map) {
return Workout(map['_workoutDuration'], map['_motivationBefore'],
map['_motivationAfter']);
}
@override
String toCSV() =>
"${_workoutDate.toIso8601String()}, $_motivationBefore, $_motivationAfter";
@override
Map<String, dynamic> toMap() {
return {
'motivationBefore': _motivationBefore,
'motivationAfter': _motivationAfter,
'workoutDate': _workoutDate,
};
}
}

View File

@ -1,225 +1,32 @@
import 'dart:async';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:smoke_cess_app/providers/timer_provider.dart';
import 'package:smoke_cess_app/widgets/popup_for_start_and_stop.dart';
import 'package:smoke_cess_app/widgets/timer_widget.dart';
import 'package:smoke_cess_app/providers/workout_provider.dart';
import 'package:smoke_cess_app/widgets/mute_button.dart';
import 'package:smoke_cess_app/widgets/workout_timer_widget.dart';
import '../providers/input_provider.dart';
class IntervalTimerPage extends StatefulWidget {
const IntervalTimerPage({Key? key}) : super(key: key);
@override
_IntervalTimerPageState createState() => _IntervalTimerPageState();
}
class _IntervalTimerPageState extends State<IntervalTimerPage> {
final Duration _warmupDuration = const Duration(seconds: 5);
final Duration _cooldownDuration = const Duration(seconds: 5);
final Duration _highIntensityDuration = const Duration(seconds: 4);
final Duration _lowIntensityDuration = const Duration(seconds: 3);
late Duration _totalDuration = const Duration(minutes: 35);
AudioPlayer warmUpPlayer = AudioPlayer();
AudioPlayer workoutPlayer = AudioPlayer();
AudioPlayer coolDownPlayer = AudioPlayer();
final AudioCache _audioCache = AudioCache();
final int _numHighIntensityBlocks = 4;
final int _numLowIntensityBlocks = 3;
Timer? _timer;
int _currentBlock = 0;
Duration _currentDuration = const Duration();
bool _isPaused = true;
@override
void initState() {
_currentDuration = _warmupDuration;
super.initState();
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
void _startTimer() async {
await showDialog(
context: context,
builder: (BuildContext context) {
return ChangeNotifierProvider(
create: (context) => InputProvider(),
child: const TimerStartStopPopup(
title: 'Motivation vor dem Training',
));
},
);
_isPaused = false;
Source source = AssetSource('go.mp3');
await AudioPlayer().play(source);
_timer = Timer.periodic(const Duration(seconds: 1), (_) => _tick());
Future.delayed(const Duration(seconds: 1)).then((value) {
_playWarmUpMusic();
});
}
void _resetTimer() {
() async {
await coolDownPlayer.stop();
await warmUpPlayer.stop();
await workoutPlayer.stop();
}();
_isPaused = true;
_timer?.cancel();
_currentBlock = 0;
_currentDuration = _warmupDuration;
_totalDuration = const Duration(minutes: 35);
setState(() {});
showDialog(
context: context,
builder: (BuildContext context) {
return const TimerStartStopPopup(
title: 'Motivation nach dem Training',
);
},
);
}
Future<void> _playWarmUpMusic() async {
Source source = AssetSource('warmUp.mp3');
await warmUpPlayer.setReleaseMode(ReleaseMode.loop);
await warmUpPlayer.play(source);
}
Future<void> _playWorkoutMusic() async {
await warmUpPlayer.stop();
Future.delayed(const Duration(microseconds: 600)).then((value) async {
Source source = AssetSource('workout.mp3');
await workoutPlayer.setReleaseMode(ReleaseMode.loop);
await workoutPlayer.play(source);
});
}
Future<void> _intervalChange() async {
Source source = AssetSource('beep.mp3');
await AudioPlayer().play(source);
}
void _tick() {
setState(() {
_currentDuration = Duration(
seconds: _currentDuration.inSeconds - 1,
);
_totalDuration = Duration(
seconds: _totalDuration.inSeconds - 1,
);
if (_currentDuration.inSeconds < 1) {
if (_currentBlock < _numHighIntensityBlocks + _numLowIntensityBlocks) {
_intervalChange();
if (_currentBlock == 0) {
_playWorkoutMusic();
}
_currentBlock++;
if (_currentBlock % 2 == 1) {
_currentDuration = _highIntensityDuration;
} else {
_currentDuration = _lowIntensityDuration;
}
} else if (_currentBlock < _numHighIntensityBlocks * 2) {
_intervalChange();
_currentBlock++;
_currentDuration = _cooldownDuration;
() async {
await workoutPlayer.stop();
Source source = AssetSource('cool_down.mp3');
await coolDownPlayer.setReleaseMode(ReleaseMode.loop);
await coolDownPlayer.play(source);
}();
} else {
() async {
Future.delayed(const Duration(microseconds: 900))
.then((value) async {
Source source = AssetSource('finish.mp3');
await AudioPlayer().play(source);
});
}();
_resetTimer();
}
}
});
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final minutes = twoDigits(duration.inMinutes.remainder(60));
final seconds = twoDigits(duration.inSeconds.remainder(60));
return '$minutes:$seconds';
}
String _formatTotalDuration(Duration duration) {
final minutes = duration.inMinutes;
final seconds = duration.inSeconds.remainder(60);
return _formatDuration(Duration(minutes: minutes, seconds: seconds));
}
class IntervalTimerPage extends StatelessWidget {
const IntervalTimerPage({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => TimerProvider(),
child: TimerWidget(
duration: Duration(seconds: 5),
));
return Center(
child: ChangeNotifierProvider(
create: (context) => InputProvider(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_currentBlock == 0
? 'Warm-up'
: _currentBlock % 2 == 1
? 'High Intensity'
: _currentBlock < _numHighIntensityBlocks * 2
? 'Low Intensity'
: 'Cool-down',
style: const TextStyle(fontSize: 32.0),
),
const SizedBox(height: 16.0),
Text(
_formatDuration(_currentDuration),
style: const TextStyle(fontSize: 80.0),
),
const SizedBox(height: 32.0),
Text(
'Total: ${_formatTotalDuration(_totalDuration)}',
style: const TextStyle(fontSize: 24.0),
),
const SizedBox(height: 32.0),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(_isPaused
? Icons.play_arrow_rounded
: Icons.stop_rounded),
iconSize: 48.0,
onPressed: () {
if (_isPaused) {
_startTimer();
} else {
_resetTimer();
}
},
),
// ),
],
),
],
)));
TimerProvider timerProvider = TimerProvider();
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => timerProvider),
ChangeNotifierProvider(
create: (context) => WorkoutProvider(timerProvider)),
],
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Align(
alignment: Alignment.topLeft,
child: MuteButton(),
),
WorkoutTimerWidget()
],
),
);
}
}

View File

@ -17,15 +17,9 @@ class ScannerPage extends StatelessWidget {
List<Mood> moods = await globals.databaseService.getMoodRecords();
List<Sleep> sleeps = await globals.databaseService.getSleepRecords();
List<Relapse> relapses = await globals.databaseService.getRelapseRecords();
for (Mood mood in moods) {
print(mood.toCSV());
}
for (Sleep sleep in sleeps) {
print(sleep.toCSV());
}
for (Relapse relapse in relapses) {
print(relapse.toCSV());
}
moods;
sleeps;
relapses;
}
void loadJSON(BuildContext context) async {

View File

@ -9,7 +9,6 @@ class TimerProvider extends ChangeNotifier {
void startTimer(Duration duration) {
started = true;
print('starting timer');
_timer = Timer.periodic(const Duration(seconds: 1), ((timer) {
if (timer.tick >= duration.inSeconds) {
timer.cancel();
@ -19,5 +18,8 @@ class TimerProvider extends ChangeNotifier {
}));
}
void stopTimer() => _timer?.cancel();
void stopTimer() {
started = false;
_timer?.cancel();
}
}

View File

@ -0,0 +1,123 @@
import 'dart:async';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
import 'package:smoke_cess_app/models/workout.dart';
import 'package:smoke_cess_app/providers/timer_provider.dart';
import '../globals.dart' as globals;
class WorkoutProvider extends ChangeNotifier {
TimerProvider timerProvider;
final AudioPlayer _audioPlayer = AudioPlayer();
late StreamSubscription _onCompleteSubscription;
final Source _finishedSoundSource = AssetSource('finish.mp3');
final Source _beepSoundSource = AssetSource('beep.mp3');
bool isWorkoutStarted = false;
bool isWorkoutComplete = false;
bool isMuted = false;
int motivationBefore = 50;
int motivationAfter = 50;
void mutePlayer() {
isMuted = true;
_audioPlayer.setVolume(0);
notifyListeners();
}
void unMutePlayer() {
isMuted = false;
_audioPlayer.setVolume(1);
notifyListeners();
}
WorkoutProvider(this.timerProvider);
final List<String> _workoutPhases = [
'Warm-Up',
'High Intensity',
'Low Intensity',
'High Intensity',
'Low Intensity',
'High Intensity',
'Low Intensity',
'High Intensity',
'Cool-down'
];
int _workoutPhaseIndex = 0;
String get currentPhase => _workoutPhases[_workoutPhaseIndex];
Duration get currentPhaseDuration =>
_workoutPhaseSettings[currentPhase]!['duration'];
bool get isPhaseComplete =>
timerProvider.elapsedSeconds - currentPhaseDuration.inSeconds == 0;
Color get currentPhaseColor => _workoutPhaseSettings[currentPhase]!['color'];
AssetSource get currentPhaseSource =>
_workoutPhaseSettings[currentPhase]!['source'];
/* bool get isWorkoutComplete =>
_workoutPhaseIndex == _workoutPhases.length - 1 && isPhaseComplete; */
void nextPhase() {
_onCompleteSubscription.cancel();
_audioPlayer.stop();
if (_workoutPhaseIndex < _workoutPhases.length - 1) {
_audioPlayer.play(_beepSoundSource);
_workoutPhaseIndex += 1;
_onCompleteSubscription = _audioPlayer.onPlayerComplete.listen((event) {
_audioPlayer.play(currentPhaseSource);
});
timerProvider.startTimer(currentPhaseDuration);
} else {
//workout completed
_audioPlayer.play(_finishedSoundSource);
stopWorkout();
}
}
void startWorkout() {
isWorkoutStarted = true;
isWorkoutComplete = false;
_audioPlayer.play(_beepSoundSource).whenComplete(() => null);
_onCompleteSubscription = _audioPlayer.onPlayerComplete.listen((event) {
_audioPlayer.play(currentPhaseSource);
});
timerProvider.startTimer(currentPhaseDuration);
}
void stopWorkout() {
isWorkoutStarted = false;
isWorkoutComplete = true;
_audioPlayer.stop();
timerProvider.stopTimer();
notifyListeners();
}
void saveWorkout() {
Workout workout =
Workout(motivationBefore, motivationAfter, DateTime.now());
globals.databaseService.addWorkout(workout);
}
}
Map<String, Map<String, dynamic>> _workoutPhaseSettings = {
'Warm-Up': {
'duration': const Duration(seconds: 5),
'source': AssetSource('warmUp.mp3'),
'color': Colors.green
},
'High Intensity': {
'duration': const Duration(seconds: 4),
'source': AssetSource('workout.mp3'),
'color': Colors.red
},
'Low Intensity': {
'duration': const Duration(seconds: 3),
'source': AssetSource('workout.mp3'),
'color': Colors.orange
},
'Cool-down': {
'duration': const Duration(seconds: 5),
'source': AssetSource('cool_down.mp3'),
'color': Colors.blue
}
};

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:path/path.dart';
import 'package:smoke_cess_app/models/mood.dart';
import 'package:smoke_cess_app/models/workout.dart';
import 'package:smoke_cess_app/models/relapse.dart';
import 'package:sqflite/sqflite.dart';
// ignore: depend_on_referenced_packages
@ -35,7 +36,6 @@ class DatabaseService {
await db.execute(_createWorkoutTable);
}
//TODO use generic function?
Future<List<Mood>> getMoodRecords() async {
Database db = await instance.database;
var moodRecords = await db.query('mood');
@ -73,13 +73,17 @@ class DatabaseService {
return await db.insert('sleep', sleep.toMap());
}
Future<int> addWorkout(Workout workout) async {
Database db = await instance.database;
return await db.insert('workout', workout.toMap());
}
Future<int> addRelapse(Relapse relapse) async {
Database db = await instance.database;
return await db.insert('relapse', relapse.toMap());
}
}
String _createMoodTable = '''
final String _createMoodTable = '''
CREATE TABLE IF NOT EXISTS mood(
id INTEGER PRIMARY KEY,
value INTEGER,
@ -88,7 +92,7 @@ String _createMoodTable = '''
)
''';
String _createSleepTable = '''
final String _createSleepTable = '''
CREATE TABLE IF NOT EXISTS sleep(
id INTEGER PRIMARY KEY,
value INTEGER,
@ -101,7 +105,7 @@ String _createSleepTable = '''
)
''';
String _createRelapseTable = '''
final String _createRelapseTable = '''
CREATE TABLE IF NOT EXISTS relapse(
id INTEGER PRIMARY KEY,
date TEXT,
@ -110,14 +114,12 @@ String _createRelapseTable = '''
)
''';
String _createWorkoutTable = '''
final String _createWorkoutTable = '''
CREATE TABLE IF NOT EXISTS workout(
id INTEGER PRIMARY KEY,
date TEXT,
motivationBefore INTEGER,
commentBefore TEXT,
motivationAfter INTEGER,
commentAfter TEXT,
completed INTEGER
)
''';
}

View File

@ -23,13 +23,6 @@ Future<int?> getMoodQueryMinutes() => _getIntSetting('mood_query_minutes');
Future<int?> getChessHours() => _getIntSetting('chess_hours');
Future<int?> getChessMinutes() => _getIntSetting('chess_minutes');
void _setStringSetting(String settingKey, String settingValue) =>
SharedPreferences.getInstance()
.then((pref) => pref.setString(settingKey, settingValue));
Future<String?> _getStringSetting(String settingKey) =>
SharedPreferences.getInstance().then((pref) => pref.getString(settingKey));
void _setIntSetting(String settingKey, int settingValue) =>
SharedPreferences.getInstance()
.then((pref) => pref.setInt(settingKey, settingValue));

View File

@ -1,7 +1,5 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:smoke_cess_app/models/mood.dart';
import 'package:smoke_cess_app/services/database_service.dart';
import 'package:smoke_cess_app/widgets/slider.dart';
import 'package:smoke_cess_app/widgets/submit_form_button.dart';
import 'package:smoke_cess_app/widgets/text_formfield.dart';

View File

@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:smoke_cess_app/providers/workout_provider.dart';
class MuteButton extends StatelessWidget {
const MuteButton({super.key});
@override
Widget build(BuildContext context) {
WorkoutProvider workoutProvider = context.watch<WorkoutProvider>();
return IconButton(
onPressed: workoutProvider.isMuted
? workoutProvider.unMutePlayer
: workoutProvider.mutePlayer,
icon: Icon(workoutProvider.isMuted
? Icons.volume_off_outlined
: Icons.volume_up_outlined),
);
}
}

View File

@ -1,26 +1,52 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:smoke_cess_app/providers/input_provider.dart';
import 'package:smoke_cess_app/widgets/slider.dart';
import 'package:smoke_cess_app/widgets/text_formfield.dart';
Future showMotivationPopup(
BuildContext context, Function onSave, String title) {
return showDialog(
context: context,
builder: (BuildContext context) {
return ChangeNotifierProvider(
create: (context) => InputProvider(),
child: TimerStartStopPopup(
title: title,
onSaveAction: onSave,
),
);
},
);
}
class TimerStartStopPopup extends StatelessWidget {
final String title;
const TimerStartStopPopup({Key? key, required this.title}) : super(key: key);
final Function onSaveAction;
const TimerStartStopPopup(
{Key? key, required this.title, required this.onSaveAction})
: super(key: key);
@override
Widget build(BuildContext context) {
InputProvider inputProvider = context.watch<InputProvider>();
return AlertDialog(
title: Text(title),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Padding(
padding: const EdgeInsets.only(top: 8),
children: [
const Padding(
padding: EdgeInsets.only(top: 8),
child: MySlider(),
),
SizedBox(height: 16),
MyTextFormField('Beschreibe deinen Motivation'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
onSaveAction(inputProvider.sliderValue);
Navigator.pop(context);
},
child: const Text('Speichern'))
],
),
);

View File

@ -14,11 +14,6 @@ class TimerWidget extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(formatTime(duration.inSeconds - timerProvider.elapsedSeconds)),
ElevatedButton(
onPressed: () => timerProvider.started
? timerProvider.stopTimer()
: timerProvider.startTimer(duration),
child: Text(timerProvider.started ? 'Stop' : 'Start'))
],
);
}

View File

@ -0,0 +1,78 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:smoke_cess_app/providers/workout_provider.dart';
import 'package:smoke_cess_app/widgets/timer_widget.dart';
import '../providers/timer_provider.dart';
import 'popup_for_start_and_stop.dart';
class WorkoutTimerWidget extends StatelessWidget {
const WorkoutTimerWidget({super.key});
@override
Widget build(BuildContext context) {
TimerProvider timerProvider = context.watch<TimerProvider>();
WorkoutProvider workoutProvider = context.watch<WorkoutProvider>();
if (workoutProvider.isPhaseComplete && !workoutProvider.isWorkoutComplete) {
Timer(const Duration(milliseconds: 1), () => workoutProvider.nextPhase());
}
if (workoutProvider.isWorkoutComplete) {
Timer(
const Duration(milliseconds: 1),
() => showMotivationPopup(context, (double value) {
workoutProvider.motivationAfter = value.toInt();
workoutProvider.saveWorkout();
}, 'Motivation nach dem Training'));
}
void handleStartStopWorkout() {
if (!workoutProvider.isWorkoutStarted) {
showMotivationPopup(context, (double value) {
workoutProvider.motivationBefore = value.toInt();
workoutProvider.startWorkout();
}, 'Motivation vor dem Training');
} else {
workoutProvider.stopWorkout();
showMotivationPopup(
context,
(double value) => workoutProvider.motivationAfter = value.toInt(),
'Motivation nach dem Training');
}
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(workoutProvider.currentPhase),
const SizedBox(
height: 20,
),
Stack(
alignment: Alignment.center,
children: [
SizedBox(
height: 100,
width: 100,
child: CircularProgressIndicator(
color: workoutProvider.currentPhaseColor,
value: (workoutProvider.currentPhaseDuration.inSeconds
.toDouble() -
timerProvider.elapsedSeconds) /
workoutProvider.currentPhaseDuration.inSeconds)),
TimerWidget(duration: workoutProvider.currentPhaseDuration),
],
),
const SizedBox(
height: 20,
),
ElevatedButton(
onPressed: handleStartStopWorkout,
child: Text(timerProvider.started ? 'Stop' : 'Start'))
],
);
}
}