diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c44bc95..5bb832c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,16 +1,17 @@ PODS: - Flutter (1.0.0) - - FMDB/SQLCipher (2.7.5): - - SQLCipher - - moor_ffi (0.0.1): + - geolocator (5.3.0): + - Flutter + - google_api_availability (2.0.3): + - Flutter + - location_permissions (2.0.5): - Flutter - path_provider (0.0.1): - Flutter - path_provider_macos (0.0.1): - Flutter - - sqflite (0.0.1): + - share (0.5.2): - Flutter - - FMDB/SQLCipher (~> 2.7.5) - SQLCipher (4.1.0): - SQLCipher/standard (= 4.1.0) - SQLCipher/common (4.1.0) @@ -19,36 +20,42 @@ PODS: DEPENDENCIES: - Flutter (from `Flutter`) - - moor_ffi (from `.symlinks/plugins/moor_ffi/ios`) + - geolocator (from `.symlinks/plugins/geolocator/ios`) + - google_api_availability (from `.symlinks/plugins/google_api_availability/ios`) + - location_permissions (from `.symlinks/plugins/location_permissions/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) - path_provider_macos (from `.symlinks/plugins/path_provider_macos/ios`) - - sqflite (from `.symlinks/plugins/sqflite/ios`) + - share (from `.symlinks/plugins/share/ios`) - SQLCipher (~> 4.1.0) SPEC REPOS: trunk: - - FMDB - SQLCipher EXTERNAL SOURCES: Flutter: :path: Flutter - moor_ffi: - :path: ".symlinks/plugins/moor_ffi/ios" + geolocator: + :path: ".symlinks/plugins/geolocator/ios" + google_api_availability: + :path: ".symlinks/plugins/google_api_availability/ios" + location_permissions: + :path: ".symlinks/plugins/location_permissions/ios" path_provider: :path: ".symlinks/plugins/path_provider/ios" path_provider_macos: :path: ".symlinks/plugins/path_provider_macos/ios" - sqflite: - :path: ".symlinks/plugins/sqflite/ios" + share: + :path: ".symlinks/plugins/share/ios" SPEC CHECKSUMS: Flutter: 0e3d915762c693b495b44d77113d4970485de6ec - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - moor_ffi: d66c9470c18e9cb333423bbcb493c105c6c774c6 + geolocator: 7cdcf71180b80913b3cd84ab715d3b5365b378af + google_api_availability: 526574c9a5a0ae541e18c65f98e47afc11f53c8b + location_permissions: 4a49d4e5bec5b653643e551ab77963cc99bb0e4a path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0 - sqflite: dc1dd7fd0c3603c33bd1c0b2e90d38182e3ddb37 + share: bae0a282aab4483288913fc4dc0b935d4b491f2e SQLCipher: efbdb52cdbe340bcd892b1b14297df4e07241b7f PODFILE CHECKSUM: c1f56ec578e7dc933512aa9b9b86b80eb2a5b47d diff --git a/lib/src/app.dart b/lib/src/app.dart index b858655..3377f94 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -2,7 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:coronavirus_diary/src/blocs/preferences/preferences.dart'; +import 'package:coronavirus_diary/src/blocs/checkup/checkup.dart'; import 'package:coronavirus_diary/src/blocs/questions/questions.dart'; +import 'package:coronavirus_diary/src/data/repositories/checkups.dart'; import 'package:coronavirus_diary/src/data/repositories/questions.dart'; import 'package:coronavirus_diary/src/ui/assets/theme.dart'; import 'package:coronavirus_diary/src/ui/router.dart'; @@ -21,17 +23,23 @@ class DiaryApp extends StatelessWidget { create: (context) { return QuestionsBloc( questionsRepository: QuestionsRepository(), - ); + )..add(LoadQuestions()); }, ), ], child: BlocBuilder( builder: (context, state) { - return MaterialApp( - title: 'Coronavirus Diary', - theme: appTheme, - routes: appRoutes, - initialRoute: initialRoute, + return BlocProvider( + create: (context) => CheckupBloc( + preferencesState: state, + checkupsRepository: CheckupsRepository(), + ), + child: MaterialApp( + title: 'Coronavirus Diary', + theme: appTheme, + routes: appRoutes, + initialRoute: initialRoute, + ), ); }, ), diff --git a/lib/src/blocs/checkup/checkup_bloc.dart b/lib/src/blocs/checkup/checkup_bloc.dart index 0fc8d03..ba01216 100644 --- a/lib/src/blocs/checkup/checkup_bloc.dart +++ b/lib/src/blocs/checkup/checkup_bloc.dart @@ -4,14 +4,20 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:coronavirus_diary/src/blocs/preferences/preferences.dart'; import 'package:coronavirus_diary/src/data/models/checkups.dart'; +import 'package:coronavirus_diary/src/data/repositories/checkups.dart'; import 'checkup.dart'; +export 'package:coronavirus_diary/src/data/models/assessments.dart'; export 'package:coronavirus_diary/src/data/models/checkups.dart'; class CheckupBloc extends Bloc { final PreferencesState preferencesState; + final CheckupsRepository checkupsRepository; - CheckupBloc({this.preferencesState}); + CheckupBloc({ + this.preferencesState, + this.checkupsRepository, + }); @override CheckupState get initialState => CheckupStateNotCreated(); @@ -35,17 +41,21 @@ class CheckupBloc extends Bloc { } Stream _mapStartCheckupToState(StartCheckup event) async* { - // Create checkup using API yield CheckupStateCreating(); - yield CheckupStateInProgress( - checkup: Checkup( - userId: preferencesState.preferences.userId, - ), - ); + + // Create checkup using API + final Checkup newCheckup = await checkupsRepository.createCheckup(Checkup( + userId: preferencesState.preferences.userId, + )); + + yield CheckupStateInProgress(checkup: newCheckup); } Stream _mapUpdateLocalCheckupToState( UpdateLocalCheckup event) async* { + // Exit if checkup is not in progress + if (state is! CheckupStateInProgress) return; + // Don't send to API yet print(event.updatedCheckup); yield CheckupStateInProgress( @@ -55,13 +65,40 @@ class CheckupBloc extends Bloc { Stream _mapUpdateRemoteCheckupToState( UpdateRemoteCheckup event) async* { - // Patch checkup using API - print('Saving checkup to server'); + // Exit if checkup is not in progress + if (state is! CheckupStateInProgress) return; + + // Retrieve current checkup + final CheckupStateInProgress currentState = state; + final Checkup currentCheckup = currentState.checkup; + + // Patch checkup using API and return it (to handle server-side updates) + final Checkup checkup = + await checkupsRepository.updateCheckup(currentCheckup); + yield CheckupStateInProgress(checkup: checkup); } Stream _mapCompleteCheckupToState( CompleteCheckup event) async* { - // Patch checkup using API - yield CheckupStateCompleted(); + // Exit if checkup is not in progress + if (state is! CheckupStateInProgress) return; + + // Notify app that we are waiting to submit data + yield CheckupStateCompleting(); + + // Retrieve current checkup + final CheckupStateInProgress currentState = state; + final Checkup currentCheckup = currentState.checkup; + + // Make sure checkup is up to date on server + final Checkup checkup = + await checkupsRepository.updateCheckup(currentCheckup); + + // Complete checkup + final Assessment assessment = + await checkupsRepository.completeCheckup(checkup.id); + + // Complete checkup using API + yield CheckupStateCompleted(assessment: assessment); } } diff --git a/lib/src/blocs/checkup/checkup_state.dart b/lib/src/blocs/checkup/checkup_state.dart index 87e1c82..95aaa7e 100644 --- a/lib/src/blocs/checkup/checkup_state.dart +++ b/lib/src/blocs/checkup/checkup_state.dart @@ -1,3 +1,4 @@ +import 'package:coronavirus_diary/src/data/models/assessments.dart'; import 'package:coronavirus_diary/src/data/models/checkups.dart'; abstract class CheckupState { @@ -17,4 +18,13 @@ class CheckupStateInProgress extends CheckupState { String toString() => 'CheckupStateInProgress { checkup: $checkup }'; } -class CheckupStateCompleted extends CheckupState {} +class CheckupStateCompleting extends CheckupState {} + +class CheckupStateCompleted extends CheckupState { + final Assessment assessment; + + const CheckupStateCompleted({this.assessment}); + + @override + String toString() => 'CheckupStateCompleted { assessment: $assessment }'; +} diff --git a/lib/src/blocs/preferences/preferences_state.dart b/lib/src/blocs/preferences/preferences_state.dart index 0550886..f7b71ef 100644 --- a/lib/src/blocs/preferences/preferences_state.dart +++ b/lib/src/blocs/preferences/preferences_state.dart @@ -4,4 +4,7 @@ class PreferencesState { final Preferences preferences; const PreferencesState({this.preferences}); + + @override + String toString() => 'PreferencesState { preferences: $preferences }'; } diff --git a/lib/src/data/models/assessments.dart b/lib/src/data/models/assessments.dart new file mode 100644 index 0000000..59f627b --- /dev/null +++ b/lib/src/data/models/assessments.dart @@ -0,0 +1,22 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'assessments.g.dart'; + +@JsonSerializable() +class Assessment { + DateTime processed; + bool matchesPuiSymptoms; + + Assessment({ + this.processed, + this.matchesPuiSymptoms, + }); + + factory Assessment.fromJson(Map json) => + _$AssessmentFromJson(json); + Map toJson() => _$AssessmentToJson(this); + + @override + String toString() => + 'Assessment { processed: $processed, matchesPuiSymptoms: $matchesPuiSymptoms }'; +} diff --git a/lib/src/data/models/assessments.g.dart b/lib/src/data/models/assessments.g.dart new file mode 100644 index 0000000..db5ba8f --- /dev/null +++ b/lib/src/data/models/assessments.g.dart @@ -0,0 +1,22 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'assessments.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Assessment _$AssessmentFromJson(Map json) { + return Assessment( + processed: json['processed'] == null + ? null + : DateTime.parse(json['processed'] as String), + matchesPuiSymptoms: json['matches_pui_symptoms'] as bool, + ); +} + +Map _$AssessmentToJson(Assessment instance) => + { + 'processed': instance.processed?.toIso8601String(), + 'matches_pui_symptoms': instance.matchesPuiSymptoms, + }; diff --git a/lib/src/data/models/checkups.dart b/lib/src/data/models/checkups.dart index 5381267..a7c2d5f 100644 --- a/lib/src/data/models/checkups.dart +++ b/lib/src/data/models/checkups.dart @@ -30,7 +30,7 @@ class Checkup { @override String toString() => - 'Checkup { id: $id, created: $created, dataContributionPreference: $dataContributionPreference, ' + 'Checkup { id: $id, userId: $userId, created: $created, dataContributionPreference: $dataContributionPreference, ' 'location: $location, subjectiveResponses: $subjectiveResponses, vitalsResponses: $vitalsResponses }'; } diff --git a/lib/src/data/models/preferences.dart b/lib/src/data/models/preferences.dart index 0b67f3e..e8ae5a3 100644 --- a/lib/src/data/models/preferences.dart +++ b/lib/src/data/models/preferences.dart @@ -2,25 +2,37 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:uuid/uuid.dart'; import 'package:uuid/uuid_util.dart'; +import 'assessments.dart'; + part 'preferences.g.dart'; @JsonSerializable() class Preferences { final String userId; + final Assessment lastAssessment; Preferences({ userId, + this.lastAssessment, }) : userId = userId ?? Uuid().v4(options: { 'grng': UuidUtil.cryptoRNG, }); - Preferences.clone(Preferences preferences) - : this( - userId: preferences.userId, - ); + Preferences cloneWith({ + Assessment lastAssessment, + }) { + return Preferences( + userId: this.userId, + lastAssessment: lastAssessment ?? this.lastAssessment, + ); + } factory Preferences.fromJson(Map json) => _$PreferencesFromJson(json); Map toJson() => _$PreferencesToJson(this); + + @override + String toString() => + 'Preferences { userId: $userId, lastAssessment: $lastAssessment }'; } diff --git a/lib/src/data/models/preferences.g.dart b/lib/src/data/models/preferences.g.dart index 61c850e..ef185c0 100644 --- a/lib/src/data/models/preferences.g.dart +++ b/lib/src/data/models/preferences.g.dart @@ -9,10 +9,14 @@ part of 'preferences.dart'; Preferences _$PreferencesFromJson(Map json) { return Preferences( userId: json['user_id'], + lastAssessment: json['last_assessment'] == null + ? null + : Assessment.fromJson(json['last_assessment'] as Map), ); } Map _$PreferencesToJson(Preferences instance) => { 'user_id': instance.userId, + 'last_assessment': instance.lastAssessment?.toJson(), }; diff --git a/lib/src/data/repositories/checkups.dart b/lib/src/data/repositories/checkups.dart new file mode 100644 index 0000000..bc64bc2 --- /dev/null +++ b/lib/src/data/repositories/checkups.dart @@ -0,0 +1,25 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:coronavirus_diary/src/data/models/assessments.dart'; +import 'package:coronavirus_diary/src/data/models/checkups.dart'; + +class CheckupsRepository { + Future createCheckup(Checkup checkup) async { + await Future.delayed(Duration(seconds: 1)); + return checkup; + } + + Future updateCheckup(Checkup updatedCheckup) async { + await Future.delayed(Duration(seconds: 1)); + return updatedCheckup; + } + + Future completeCheckup(String id) async { + await Future.delayed(Duration(seconds: 2)); + return Assessment( + processed: DateTime.now(), + matchesPuiSymptoms: Random().nextBool(), + ); + } +} diff --git a/lib/src/ui/router.dart b/lib/src/ui/router.dart index b7c5f36..9b9b5ea 100644 --- a/lib/src/ui/router.dart +++ b/lib/src/ui/router.dart @@ -1,12 +1,15 @@ +import 'screens/assessment/assessment.dart'; import 'screens/checkup/checkup.dart'; import 'screens/home/home.dart'; +export 'screens/assessment/assessment.dart'; export 'screens/checkup/checkup.dart'; export 'screens/home/home.dart'; var appRoutes = { HomeScreen.routeName: (context) => HomeScreen(), CheckupScreen.routeName: (context) => CheckupScreen(), + AssessmentScreen.routeName: (context) => AssessmentScreen(), }; const initialRoute = HomeScreen.routeName; diff --git a/lib/src/ui/screens/assessment/assessment.dart b/lib/src/ui/screens/assessment/assessment.dart new file mode 100644 index 0000000..8dfbc71 --- /dev/null +++ b/lib/src/ui/screens/assessment/assessment.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +import 'package:coronavirus_diary/src/data/models/assessments.dart'; +import 'assessments/index.dart'; + +class AssessmentScreenArguments { + final Assessment assessment; + + AssessmentScreenArguments({this.assessment}); +} + +class AssessmentScreen extends StatelessWidget { + static const routeName = '/assessment'; + + @override + Widget build(BuildContext context) { + final AssessmentScreenArguments args = + ModalRoute.of(context).settings.arguments; + + return Scaffold( + appBar: AppBar( + title: Text('Your Personalized Assessment'), + ), + body: getAssessmentView(args.assessment), + ); + } +} diff --git a/lib/src/ui/screens/assessment/assessments/index.dart b/lib/src/ui/screens/assessment/assessments/index.dart new file mode 100644 index 0000000..6dfa33c --- /dev/null +++ b/lib/src/ui/screens/assessment/assessments/index.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +import 'package:coronavirus_diary/src/data/models/assessments.dart'; +import 'positive.dart'; +import 'negative.dart'; + +export 'positive.dart'; +export 'negative.dart'; + +Widget getAssessmentView(Assessment assessment) { + if (assessment.matchesPuiSymptoms) { + return PositiveAssessment(); + } else { + return NegativeAssessment(); + } +} diff --git a/lib/src/ui/screens/assessment/assessments/negative.dart b/lib/src/ui/screens/assessment/assessments/negative.dart new file mode 100644 index 0000000..80b7a5b --- /dev/null +++ b/lib/src/ui/screens/assessment/assessments/negative.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +import 'package:coronavirus_diary/src/ui/widgets/share.dart'; + +class NegativeAssessment extends StatefulWidget { + @override + _NegativeAssessmentState createState() => _NegativeAssessmentState(); +} + +class _NegativeAssessmentState extends State { + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + margin: EdgeInsets.only(bottom: 10), + padding: EdgeInsets.symmetric(horizontal: 40), + child: FaIcon( + FontAwesomeIcons.solidSmile, + color: Colors.white, + size: 70, + ), + ), + Container( + margin: EdgeInsets.only(bottom: 20), + padding: EdgeInsets.symmetric(horizontal: 40), + child: Text( + "You don't meet testing criteria", + style: Theme.of(context).textTheme.title, + textAlign: TextAlign.center, + ), + ), + Container( + margin: EdgeInsets.only(bottom: 20), + padding: EdgeInsets.symmetric(horizontal: 40), + child: Text( + "If you continue to experience symptoms, please check in tomorrow.", + style: Theme.of(context).textTheme.body2.copyWith(fontSize: 16), + textAlign: TextAlign.center, + ), + ), + Container( + margin: EdgeInsets.only(bottom: 40), + padding: EdgeInsets.symmetric(horizontal: 40), + child: Text( + "If they become serious, please consult a physician.", + style: Theme.of(context).textTheme.body2.copyWith(fontSize: 16), + textAlign: TextAlign.center, + ), + ), + ShareApp(), + ], + ), + ); + } +} diff --git a/lib/src/ui/screens/assessment/assessments/positive.dart b/lib/src/ui/screens/assessment/assessments/positive.dart new file mode 100644 index 0000000..0010704 --- /dev/null +++ b/lib/src/ui/screens/assessment/assessments/positive.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +import 'package:coronavirus_diary/src/ui/widgets/share.dart'; + +class PositiveAssessment extends StatefulWidget { + @override + _PositiveAssessmentState createState() => _PositiveAssessmentState(); +} + +class _PositiveAssessmentState extends State { + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + margin: EdgeInsets.only(bottom: 5), + padding: EdgeInsets.symmetric(horizontal: 40), + child: Text( + '🤔', + style: TextStyle(fontSize: 70), + ), + ), + Container( + margin: EdgeInsets.only(bottom: 20), + padding: EdgeInsets.symmetric(horizontal: 40), + child: Text( + "Please contact your physician", + style: Theme.of(context).textTheme.title, + textAlign: TextAlign.center, + ), + ), + Container( + margin: EdgeInsets.only(bottom: 20), + padding: EdgeInsets.symmetric(horizontal: 40), + child: Text( + "You are showing symptoms that may be of concern. " + "Please limit your contact with other people until you have a chance to follow up with a physician.", + style: Theme.of(context).textTheme.body2.copyWith(fontSize: 16), + textAlign: TextAlign.center, + ), + ), + Container( + margin: EdgeInsets.only(bottom: 40), + padding: EdgeInsets.symmetric(horizontal: 40), + child: Text( + "Do not panic. This is only a preliminary assessment and not a formal medical diagnosis.", + style: Theme.of(context).textTheme.body2.copyWith(fontSize: 16), + textAlign: TextAlign.center, + ), + ), + ShareApp(), + ], + ), + ); + } +} diff --git a/lib/src/ui/screens/checkup/checkup.dart b/lib/src/ui/screens/checkup/checkup.dart index 82c15e4..6feadcb 100644 --- a/lib/src/ui/screens/checkup/checkup.dart +++ b/lib/src/ui/screens/checkup/checkup.dart @@ -1,37 +1,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_hud/flutter_hud.dart'; import 'package:provider/provider.dart'; import 'package:coronavirus_diary/src/blocs/checkup/checkup.dart'; import 'package:coronavirus_diary/src/blocs/preferences/preferences.dart'; -import 'package:coronavirus_diary/src/blocs/questions/questions.dart'; +import 'package:coronavirus_diary/src/ui/router.dart'; import 'package:coronavirus_diary/src/ui/widgets/loading_indicator.dart'; import 'checkup_loaded_body.dart'; -class CheckupScreen extends StatelessWidget { +class CheckupScreen extends StatefulWidget { static const routeName = '/checkup'; @override - Widget build(BuildContext context) { - // Initializing the bloc provider here so that the bloc is - // accessible to all functions in the checkup screen body - return BlocBuilder( - builder: (context, state) { - return BlocProvider( - create: (context) => CheckupBloc(preferencesState: state), - child: CheckupScreenBody(), - ); - }, - ); - } -} - -class CheckupScreenBody extends StatefulWidget { - @override - _CheckupScreenBodyState createState() => _CheckupScreenBodyState(); + _CheckupScreenState createState() => _CheckupScreenState(); } -class _CheckupScreenBodyState extends State { +class _CheckupScreenState extends State { // Storing the page controller at this level so that we can access it // across the entire checkup experience PageController _pageController; @@ -50,67 +35,96 @@ class _CheckupScreenBodyState extends State { Widget _getUnloadedBody( CheckupState checkupState, - QuestionsState questionsState, ) { if (checkupState is CheckupStateNotCreated) { context.bloc().add(StartCheckup()); } - if (questionsState is QuestionsStateNotLoaded) { - context.bloc().add(LoadQuestions()); - } return LoadingIndicator('Loading your health checkup'); } - Widget _getErrorBody(QuestionsState state) { - Widget errorBody; + Widget _getErrorBody() { + return Center( + child: Padding( + padding: EdgeInsets.all(20), + child: Text( + 'There was an error retrieving the checkup experience. Please try again later.', + textAlign: TextAlign.center, + ), + ), + ); + } - if (state is QuestionsStateLoaded && state.questions.length == 0) { - errorBody = Text( - 'The checkup experience is not currently available. Please try again later.', + void _handleCheckupCompletion( + PreferencesState preferencesState, + CheckupStateCompleted checkupState, + ) { + // Remember assessment + if (preferencesState.preferences.lastAssessment != + checkupState.assessment) { + Preferences newPreferences = preferencesState.preferences.cloneWith( + lastAssessment: checkupState.assessment, ); - } else { - errorBody = Text( - 'There was an error retrieving the checkup experience. Please try again later.'); + context.bloc().add(UpdatePreferences(newPreferences)); } - return errorBody; + // Navigate to assessment view + Navigator.pushReplacementNamed( + context, + AssessmentScreen.routeName, + arguments: AssessmentScreenArguments( + assessment: checkupState.assessment, + ), + ); } - Widget _getBody(CheckupState checkupState, QuestionsState questionsState) { - if (questionsState is QuestionsStateNotLoaded || - questionsState is QuestionsStateLoading || - checkupState is! CheckupStateInProgress) { - return _getUnloadedBody(checkupState, questionsState); - } else if (questionsState is QuestionsStateLoaded && - questionsState.questions.length > 0) { - return CheckupLoadedBody(); - } else { - return _getErrorBody(questionsState); + Widget _getBody( + PreferencesState preferencesState, + CheckupState checkupState, + ) { + switch (checkupState.runtimeType) { + case CheckupStateNotCreated: + case CheckupStateCreating: + return _getUnloadedBody(checkupState); + case CheckupStateInProgress: + case CheckupStateCompleting: + return CheckupLoadedBody(); + case CheckupStateCompleted: + _handleCheckupCompletion(preferencesState, checkupState); + return null; + default: + return _getErrorBody(); } } @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { - final CheckupState checkupState = state; + final PreferencesState preferencesState = state; - return BlocBuilder( + return BlocBuilder( builder: (context, state) { - final QuestionsState questionsState = state; + final CheckupState checkupState = state; - return ChangeNotifierProvider( - create: (context) => _pageController, - child: Scaffold( - appBar: AppBar( - title: Text('Your Health Checkup'), - ), - backgroundColor: Theme.of(context).primaryColor, - body: _getBody( - checkupState, - questionsState, - ), - ), + return WidgetHUD( + showHUD: checkupState is CheckupStateCompleting, + hud: HUD(label: 'Loading your assessment'), + builder: (context) { + return ChangeNotifierProvider( + create: (context) => _pageController, + child: Scaffold( + appBar: AppBar( + title: Text('Your Health Checkup'), + leading: IconButton( + icon: Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ), + backgroundColor: Theme.of(context).primaryColor, + body: _getBody(preferencesState, checkupState), + ), + ); + }, ); }, ); diff --git a/lib/src/ui/screens/checkup/checkup_loaded_body.dart b/lib/src/ui/screens/checkup/checkup_loaded_body.dart index 89087a4..c9697e6 100644 --- a/lib/src/ui/screens/checkup/checkup_loaded_body.dart +++ b/lib/src/ui/screens/checkup/checkup_loaded_body.dart @@ -54,17 +54,14 @@ class _CheckupLoadedBodyState extends State { if (checkupState.checkup.dataContributionPreference == true) { await _saveCurrentLocation(checkupState); } + } else if (currentIndex > 0 && currentIndex < steps.length - 1) { + context.bloc().add(UpdateRemoteCheckup()); } setState(() { currentIndex = index; currentStep = steps[index]; }); - - // Destination-specific actions - if (currentIndex > 1) { - context.bloc().add(UpdateRemoteCheckup()); - } } @override diff --git a/lib/src/ui/screens/checkup/checkup_progress_bar.dart b/lib/src/ui/screens/checkup/checkup_progress_bar.dart index a63a466..4ccae00 100644 --- a/lib/src/ui/screens/checkup/checkup_progress_bar.dart +++ b/lib/src/ui/screens/checkup/checkup_progress_bar.dart @@ -1,20 +1,34 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; +import 'package:coronavirus_diary/src/blocs/checkup/checkup.dart'; + class CheckupProgressBar extends StatelessWidget { final int currentIndex; final int stepsLength; + final bool isLastPage; + final double percentComplete; const CheckupProgressBar({ this.currentIndex, this.stepsLength, - }); + }) : isLastPage = currentIndex == stepsLength - 1, + percentComplete = (currentIndex) / (stepsLength - 1); + + _handleNextButton(BuildContext context) { + if (isLastPage) { + context.bloc().add(CompleteCheckup()); + } else { + Provider.of(context, listen: false).nextPage( + duration: Duration(milliseconds: 400), + curve: Curves.easeInOut, + ); + } + } @override Widget build(BuildContext context) { - double percentComplete = (currentIndex) / (stepsLength - 1); - bool isLastPage = currentIndex == stepsLength - 1; - // Remember to update this if steps are added that do not count towards the total String percentCompleteText = 'Step ${currentIndex} of ${stepsLength - 1}'; @@ -38,12 +52,7 @@ class CheckupProgressBar extends StatelessWidget { ), ), RaisedButton( - onPressed: () => - Provider.of(context, listen: false) - .nextPage( - duration: Duration(milliseconds: 400), - curve: Curves.easeInOut, - ), + onPressed: () => _handleNextButton(context), child: Text(isLastPage ? 'Submit' : 'Continue'), ), ], diff --git a/lib/src/ui/screens/home/home.dart b/lib/src/ui/screens/home/home.dart index 2b9b6d4..b1dfe95 100644 --- a/lib/src/ui/screens/home/home.dart +++ b/lib/src/ui/screens/home/home.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:coronavirus_diary/src/blocs/preferences/preferences.dart'; import 'package:coronavirus_diary/src/ui/router.dart'; +import 'package:coronavirus_diary/src/ui/widgets/share.dart'; class HomeScreen extends StatefulWidget { static const routeName = '/'; @@ -12,6 +14,105 @@ class HomeScreen extends StatefulWidget { } class _HomeScreenState extends State { + Widget _getBody(PreferencesState state) { + final DateTime now = DateTime.now(); + final DateTime today = DateTime(now.year, now.month, now.day); + + if (state.preferences.lastAssessment == null || + state.preferences.lastAssessment.processed.isBefore(today)) { + return Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 40), + margin: EdgeInsets.only(bottom: 20), + child: FaIcon( + FontAwesomeIcons.questionCircle, + color: Colors.white, + size: 80, + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 40), + margin: EdgeInsets.only(bottom: 20), + child: Text( + "Concerned about your health?", + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.title, + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 40), + margin: EdgeInsets.only(bottom: 20), + child: Text( + "Are you experiencing symptoms? Have you been in contact with someone who is infected?", + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.body2, + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 40), + margin: EdgeInsets.only(bottom: 40), + width: double.infinity, + child: RaisedButton( + onPressed: () => + Navigator.pushNamed(context, CheckupScreen.routeName), + child: Text('Check up on your health'), + ), + ), + ShareApp(), + ], + ); + } else { + return Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 40), + margin: EdgeInsets.only(bottom: 20), + child: FaIcon( + FontAwesomeIcons.checkCircle, + color: Colors.white, + size: 80, + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 40), + margin: EdgeInsets.only(bottom: 20), + child: Text( + 'You have completed your checkup for today!', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.title, + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 40), + margin: EdgeInsets.only(bottom: 20), + child: Text( + 'If you continue to experience symptoms, please check back tomorrow.', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.subtitle, + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 40), + margin: EdgeInsets.only(bottom: 40), + width: double.infinity, + child: RaisedButton( + onPressed: () => Navigator.pushNamed( + context, + AssessmentScreen.routeName, + arguments: AssessmentScreenArguments( + assessment: state.preferences.lastAssessment, + ), + ), + child: Text('View my assessment'), + ), + ), + ShareApp(), + ], + ); + } + } + @override Widget build(BuildContext context) { return BlocBuilder( @@ -20,21 +121,11 @@ class _HomeScreenState extends State { appBar: AppBar( title: Text('Coronavirus Diary'), ), - body: Container( - padding: EdgeInsets.all(20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - width: double.infinity, - child: RaisedButton( - onPressed: () => - Navigator.pushNamed(context, CheckupScreen.routeName), - child: Text('Start checkup'), - ), - ), - ], + body: SingleChildScrollView( + child: Container( + padding: EdgeInsets.symmetric(vertical: 40), + alignment: Alignment.center, + child: _getBody(state), ), ), ); diff --git a/lib/src/ui/widgets/share.dart b/lib/src/ui/widgets/share.dart new file mode 100644 index 0000000..901ab68 --- /dev/null +++ b/lib/src/ui/widgets/share.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/fa_icon.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:share/share.dart'; + +class ShareApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + void _shareApp() { + Share.share( + 'Worried that you might have COVID-19? ' + 'Download this app to checkup on your health and support your community: APP_LINK', + ); + } + + return Container( + color: Colors.white.withOpacity(0.2), + padding: EdgeInsets.symmetric( + horizontal: 40, + vertical: 20, + ), + child: Column( + children: [ + Container( + margin: EdgeInsets.only(bottom: 10), + child: FaIcon( + FontAwesomeIcons.heartbeat, + color: Colors.red, + size: 40, + ), + ), + Container( + margin: EdgeInsets.only(bottom: 10), + child: Text( + "Protect Your Community", + style: Theme.of(context).textTheme.title, + textAlign: TextAlign.center, + ), + ), + Container( + margin: EdgeInsets.only(bottom: 20), + child: Text( + "Share this app with your friends, coworkers, and family (especially grandparents).", + style: Theme.of(context).textTheme.body1.copyWith( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + RaisedButton( + onPressed: _shareApp, + child: Text('Share now'), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 8fe50b0..b44805c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -188,6 +188,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.2.0" + flutter_hud: + dependency: "direct main" + description: + name: flutter_hud + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.1" flutter_test: dependency: "direct dev" description: flutter @@ -480,6 +487,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.23.1" + share: + dependency: "direct main" + description: + name: share + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3+6" shelf: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1522e5f..209c2d8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: flutter: sdk: flutter flutter_bloc: ^3.2.0 + flutter_hud: ^0.2.1 flutter_xlider: ^3.2.0 font_awesome_flutter: ^8.7.0 geolocator: ^5.3.0 @@ -30,6 +31,7 @@ dependencies: path_provider: ^1.6.5 path: ^1.6.4 provider: ^4.0.4 + share: ^0.6.3+6 uuid: ^2.0.4 dev_dependencies: