diff --git a/README.md b/README.md index cde2a826..705dd465 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ If this project is useful for you, please consider starring this repository and ## Contributors - Universitat Pompeu Fabra - - John Palmer + - John R.B. Palmer - [github.com/johnpalmer](https://github.com/johnpalmer) - CEAB-CSIC - Pablo Galve Millán @@ -44,7 +44,7 @@ This repository contains the source code development version of Space Mapper. Pr This project is licensed under the [GNU GENERAL PUBLIC LICENSE](https://github.com/ActivitySpaceProject/space_mapper/blob/master/LICENSE) -Copyright 2020 John R.B. Palmer +Copyright 2021 John R.B. Palmer and Pablo Galve Millán Space Mapper is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. diff --git a/Space_Mapper/lib/components/banner_image.dart b/Space_Mapper/lib/components/banner_image.dart new file mode 100644 index 00000000..b5fd6cc6 --- /dev/null +++ b/Space_Mapper/lib/components/banner_image.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class BannerImage extends StatelessWidget { + final String url; + final double height; + + BannerImage({required this.url, required this.height}); + + @override + Widget build(BuildContext context) { + if (url.isEmpty) return Container(); + Image image; + try { + image = Image.network(url, fit: BoxFit.cover); + return Container( + constraints: BoxConstraints.expand(height: height), + child: image, + ); + } catch (e) { + print('could not load image $url'); + return Container(); + } + } +} diff --git a/Space_Mapper/lib/components/survey_tile.dart b/Space_Mapper/lib/components/survey_tile.dart new file mode 100644 index 00000000..6f9c69a4 --- /dev/null +++ b/Space_Mapper/lib/components/survey_tile.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +import '../models/survey.dart'; +import '../styles.dart'; + +const SurveyTileHeight = 100.0; + +class SurveyTile extends StatelessWidget { + final Survey survey; + final bool darkTheme; + + SurveyTile({required this.survey, required this.darkTheme}); + + @override + Widget build(BuildContext context) { + final title = survey.name.toUpperCase(); + final summary = survey.summary; + + return Container( + padding: EdgeInsets.all(0), + height: SurveyTileHeight, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('$title', + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: this.darkTheme + ? Styles.surveyTileTitleDark + : Styles.surveyTileTitleLight), + Text('$summary', + overflow: TextOverflow.ellipsis, + maxLines: 3, + style: Styles.surveyTileCaption), + ], + ), + ); + } +} diff --git a/Space_Mapper/lib/mocks/mock_survey.dart b/Space_Mapper/lib/mocks/mock_survey.dart new file mode 100644 index 00000000..2439e5e3 --- /dev/null +++ b/Space_Mapper/lib/mocks/mock_survey.dart @@ -0,0 +1,39 @@ +import '../models/survey.dart'; + +mixin MockSurvey implements Survey { + static final List items = [ + Survey( + 1, + "Mosquito Alert", + "https://play.google.com/store/apps/details?id=ceab.movelab.tigatrapp&hl=es&gl=US", + "https://www.periodismociudadano.com/wp-content/uploads/2020/11/mosquito-49141_640.jpg", + "Mosquito Alert is a citizen science platform for studying and controlling the tiger mosquito (Aedes albopictus) and the yellow fever mosquito (Aedes aegypti).", + ), + Survey( + 2, + "Max Planck Institute", + "https://www.mpg.de/institutes", + "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/Max_Planck_Institute_for_the_Science_of_Light%2C_new_building%2C_July_2015.jpg/800px-Max_Planck_Institute_for_the_Science_of_Light%2C_new_building%2C_July_2015.jpg", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus a dui leo. Integer volutpat ipsum sed nulla luctus porttitor. Vivamus tincidunt iaculis purus. Sed lacinia faucibus dignissim.", + ), + Survey( + 3, + "Space Mapper Form Test", + "https://ee.kobotoolbox.org/single/asCwpCjZ", + "https://raw.githubusercontent.com/ActivitySpaceProject/space_mapper/master/Assets/images/3.0.2%2B18_screenshots.png", + "Nullam ac est non ante lobortis cursus. Sed nulla leo, venenatis at enim a, iaculis venenatis purus.Nullam ac est non ante lobortis cursus. Sed nulla leo, venenatis at enim a, iaculis venenatis purus.Nullam ac est non ante lobortis cursus. Sed nulla leo, venenatis at enim a, iaculis venenatis purus.Nullam ac est non ante lobortis cursus. Sed nulla leo, venenatis at enim a, iaculis venenatis purus.Nullam ac est non ante lobortis cursus. Sed nulla leo, venenatis at enim a, iaculis venenatis purus.", + ) + ]; + + static Survey fetchFirst() { + return items[0]; + } + + static fetchAll() { + return items; + } + + static Survey fetch(int index) { + return items[index]; + } +} diff --git a/Space_Mapper/lib/models/survey.dart b/Space_Mapper/lib/models/survey.dart new file mode 100644 index 00000000..aa541057 --- /dev/null +++ b/Space_Mapper/lib/models/survey.dart @@ -0,0 +1,21 @@ +class Survey { + final int id; + final String name; + final String webUrl; + final String imageUrl; + final String summary; + Survey(this.id, this.name, this.webUrl, this.imageUrl, this.summary); + + Survey.blank() + : id = 0, + name = ' ', + webUrl = ' ', + imageUrl = ' ', + summary = ' '; + + static Future> fetchAll() async { + // TODO: In a future version we'll fetch data from a web app + List list = []; + return list; + } +} diff --git a/Space_Mapper/lib/styles.dart b/Space_Mapper/lib/styles.dart new file mode 100644 index 00000000..86286a81 --- /dev/null +++ b/Space_Mapper/lib/styles.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +class Styles { + static const _textSizeLarge = 25.0; + static const _textSizeDefault = 16.0; + static const _textSizeSmall = 12.0; + static const horizontalPaddingDefault = 12.0; + static final Color _textColorStrong = _hexToColor('000000'); + static final Color _textColorDefault = _hexToColor('666666'); + static final Color _textColorFaint = _hexToColor('999999'); + static final Color textColorBright = _hexToColor('FFFFFF'); + static final Color accentColor = _hexToColor('FF0000'); + static final String _fontNameDefault = 'Montserrat'; + static final navBarTitle = TextStyle( + fontFamily: _fontNameDefault, + fontWeight: FontWeight.w600, + fontSize: _textSizeDefault, + color: _textColorDefault, + ); + static final headerLarge = TextStyle( + fontFamily: _fontNameDefault, + fontSize: _textSizeLarge, + color: _textColorStrong, + ); + static final textDefault = TextStyle( + fontFamily: _fontNameDefault, + fontSize: _textSizeDefault, + color: _textColorDefault, + ); + + static final textCTAButton = TextStyle( + fontFamily: _fontNameDefault, + fontSize: _textSizeLarge, + color: textColorBright, + ); + + static final surveyTileTitleLight = TextStyle( + fontFamily: _fontNameDefault, + fontSize: _textSizeLarge, + color: _textColorStrong, + ); + static final surveyTileTitleDark = TextStyle( + fontFamily: _fontNameDefault, + fontSize: _textSizeLarge, + color: textColorBright, + ); + + static final surveyTileSubTitle = TextStyle( + fontFamily: _fontNameDefault, + fontSize: _textSizeDefault, + color: accentColor, + ); + static final surveyTileCaption = TextStyle( + fontFamily: _fontNameDefault, + fontSize: _textSizeSmall, + color: _textColorFaint, + ); + + static Color _hexToColor(String code) { + return Color(int.parse(code.substring(0, 6), radix: 16) + 0xFF000000); + } +} diff --git a/Space_Mapper/lib/ui/available_surveys.dart b/Space_Mapper/lib/ui/available_surveys.dart new file mode 100644 index 00000000..73a8f3da --- /dev/null +++ b/Space_Mapper/lib/ui/available_surveys.dart @@ -0,0 +1,106 @@ +import 'package:asm/ui/web_view.dart'; +import 'package:flutter/material.dart'; + +import '../app_localizations.dart'; +import '../components/banner_image.dart'; +import '../components/survey_tile.dart'; +import '../mocks/mock_survey.dart'; +import '../models/survey.dart'; +import '../ui/web_view.dart'; +import '../styles.dart'; + +const ListItemHeight = 245.0; + +class AvailableSurveysScreen extends StatefulWidget { + @override + _AvailableSurveysScreenState createState() => _AvailableSurveysScreenState(); +} + +class _AvailableSurveysScreenState extends State { + List surveys = []; + bool loading = false; + + @override + void initState() { + super.initState(); + loadData(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: + Text(AppLocalizations.of(context)!.translate("take_survey"))), + body: RefreshIndicator( + onRefresh: loadData, + child: Column( + children: [ + renderProgressBar(context), + Expanded(child: renderListView(context)) + ], + ), + )); + } + + Future loadData() async { + if (this.mounted) { + setState(() => this.loading = true); + final surveys = await MockSurvey.fetchAll(); + setState(() { + this.surveys = surveys; + this.loading = false; + }); + } + } + + Widget renderProgressBar(BuildContext context) { + return (this.loading + ? LinearProgressIndicator( + value: null, + backgroundColor: Colors.white, + valueColor: AlwaysStoppedAnimation(Colors.grey)) + : Container()); + } + + Widget renderListView(BuildContext context) { + return ListView.builder( + itemCount: this.surveys.length, itemBuilder: _listViewItemBuilder); + } + + Widget _listViewItemBuilder(BuildContext context, int index) { + final survey = this.surveys[index]; + return GestureDetector( + onTap: () => _navigationToLocationDetail(context, survey.webUrl), + child: Container( + height: ListItemHeight, + child: Stack( + children: [ + BannerImage(url: survey.imageUrl, height: 300.0), + _tileFooter(survey), + ], + ), + ), + ); + } + + void _navigationToLocationDetail(BuildContext context, String surveyWebUrl) { + Navigator.push(context, + MaterialPageRoute(builder: (context) => MyWebView(surveyWebUrl))); + } + + Widget _tileFooter(Survey survey) { + final info = SurveyTile(survey: survey, darkTheme: true); + final overlay = Container( + padding: EdgeInsets.symmetric( + vertical: 5.0, horizontal: Styles.horizontalPaddingDefault), + decoration: BoxDecoration(color: Colors.black.withOpacity(0.5)), + child: info, + ); + return Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [overlay], + ); + } +} diff --git a/Space_Mapper/lib/ui/side_drawer.dart b/Space_Mapper/lib/ui/side_drawer.dart index 8a7de104..3a769ed0 100644 --- a/Space_Mapper/lib/ui/side_drawer.dart +++ b/Space_Mapper/lib/ui/side_drawer.dart @@ -3,13 +3,14 @@ import 'package:asm/app_localizations.dart'; import 'package:asm/models/list_view.dart'; import 'package:asm/ui/list_view.dart'; import 'package:asm/ui/report_an_issue.dart'; -import 'package:asm/ui/web_view.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:share/share.dart'; import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg; +import 'available_surveys.dart'; + class ShareLocation { late final String _timestamp; final double _lat; @@ -82,8 +83,10 @@ class SpaceMapperSideDrawer extends StatelessWidget { title: Text( AppLocalizations.of(context)!.translate("take_survey")), onTap: () { - Navigator.push(context, - MaterialPageRoute(builder: (context) => MyWebView())); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AvailableSurveysScreen())); })), Card( child: ListTile( diff --git a/Space_Mapper/lib/ui/web_view.dart b/Space_Mapper/lib/ui/web_view.dart index af41efad..be1b4858 100644 --- a/Space_Mapper/lib/ui/web_view.dart +++ b/Space_Mapper/lib/ui/web_view.dart @@ -5,7 +5,7 @@ import 'package:webview_flutter/webview_flutter.dart'; const kAndroidUserAgent = 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Mobile Safari/537.36'; -const String selectedUrl = 'https://ee.kobotoolbox.org/single/asCwpCjZ'; + const String userUUID_element = '/asRrkkAw4mUtpTDkjdzZzt/group_survey/userUUID'; const String userUUID_label = userUUID_element + ':label'; @@ -17,15 +17,19 @@ final Set jsChannels = [ }), ].toSet(); +// ignore: must_be_immutable class MyWebView extends StatefulWidget { + String selectedUrl; + MyWebView(this.selectedUrl); @override - _MyWebViewState createState() => _MyWebViewState(); + _MyWebViewState createState() => _MyWebViewState(selectedUrl); } class _MyWebViewState extends State { + String selectedUrl; final Completer _controller = Completer(); - + _MyWebViewState(this.selectedUrl); @override void initState() { super.initState(); diff --git a/Space_Mapper/test/unit/mock_survey_test.dart b/Space_Mapper/test/unit/mock_survey_test.dart new file mode 100644 index 00000000..112fe975 --- /dev/null +++ b/Space_Mapper/test/unit/mock_survey_test.dart @@ -0,0 +1,22 @@ +import 'package:asm/mocks/mock_survey.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('test fetchAny', () { + final mockSurvey = MockSurvey.fetchFirst(); + expect(mockSurvey, isNotNull); + expect(mockSurvey.name, isNotEmpty); + }); + + test('test fetchAll', () { + final mockSurvey = MockSurvey.fetchAll(); + expect(mockSurvey.length, greaterThan(0)); + expect(mockSurvey[0].name, isNotEmpty); + }); + + test('test fetch', () { + final mockSurvey = MockSurvey.fetch(0); + expect(mockSurvey, isNotNull); + expect(mockSurvey.name, isNotEmpty); + }); +}