diff --git a/veggieseasons/lib/data/local_veggie_provider.dart b/veggieseasons/lib/data/local_veggie_provider.dart index cb8442210..f4b922986 100644 --- a/veggieseasons/lib/data/local_veggie_provider.dart +++ b/veggieseasons/lib/data/local_veggie_provider.dart @@ -19,6 +19,35 @@ class LocalVeggieProvider { vitaminCPercentage: 8, servingSize: 'One large apple', caloriesPerServing: 130, + trivia: [ + Trivia( + 'A peck of apples (that\'s a real unit of mesaurement!) weighs approximately how many pounds?', + [ + '10 pounds', + '20 pounds', + '30 pounds', + ], + 0, + ), + Trivia( + 'Which of these is an actual variety of apples?', + [ + 'Dancing Turkey', + 'Winter Banana', + 'Red Sloth', + ], + 1, + ), + Trivia( + 'In Greek mythology, Paris gives a golden apple marked "To the Fairest" to a goddess. Which one?', + [ + 'Hera', + 'Athena', + 'Aphrodite', + ], + 2, + ), + ], ), Veggie( id: 2, @@ -32,6 +61,7 @@ class LocalVeggieProvider { vitaminCPercentage: 25, servingSize: '1 medium artichoke', caloriesPerServing: 60, + trivia: [], ), Veggie( id: 3, @@ -45,6 +75,7 @@ class LocalVeggieProvider { vitaminCPercentage: 15, servingSize: '5 spears', caloriesPerServing: 20, + trivia: [], ), Veggie( id: 4, @@ -58,6 +89,7 @@ class LocalVeggieProvider { vitaminCPercentage: 4, servingSize: '1/5 medium avocado', caloriesPerServing: 50, + trivia: [], ), Veggie( id: 5, @@ -71,6 +103,7 @@ class LocalVeggieProvider { vitaminCPercentage: 4, servingSize: '1 cup', caloriesPerServing: 62, + trivia: [], ), Veggie( id: 6, @@ -84,6 +117,7 @@ class LocalVeggieProvider { vitaminCPercentage: 80, servingSize: '1/4 medium canteloupe', caloriesPerServing: 50, + trivia: [], ), Veggie( id: 7, @@ -97,6 +131,7 @@ class LocalVeggieProvider { vitaminCPercentage: 100, servingSize: '1/6 medium head', caloriesPerServing: 25, + trivia: [], ), Veggie( id: 8, @@ -110,6 +145,7 @@ class LocalVeggieProvider { vitaminCPercentage: 2, servingSize: '1/2 cup, chopped', caloriesPerServing: 4, + trivia: [], ), Veggie( id: 9, @@ -123,6 +159,7 @@ class LocalVeggieProvider { vitaminCPercentage: 2, servingSize: '1 large fig', caloriesPerServing: 50, + trivia: [], ), Veggie( id: 10, @@ -136,6 +173,7 @@ class LocalVeggieProvider { vitaminCPercentage: 2, servingSize: '3/4 cup', caloriesPerServing: 90, + trivia: [], ), Veggie( id: 11, @@ -149,6 +187,7 @@ class LocalVeggieProvider { vitaminCPercentage: 190, servingSize: '1 medium pepper', caloriesPerServing: 25, + trivia: [], ), Veggie( id: 12, @@ -162,6 +201,7 @@ class LocalVeggieProvider { vitaminCPercentage: 100, servingSize: '1 pepper', caloriesPerServing: 20, + trivia: [], ), Veggie( id: 13, @@ -175,6 +215,7 @@ class LocalVeggieProvider { vitaminCPercentage: 134, servingSize: '1 cup, chopped', caloriesPerServing: 33, + trivia: [], ), Veggie( id: 14, @@ -188,6 +229,7 @@ class LocalVeggieProvider { vitaminCPercentage: 240, servingSize: '2 medium kiwis', caloriesPerServing: 90, + trivia: [], ), Veggie( id: 15, @@ -201,6 +243,7 @@ class LocalVeggieProvider { vitaminCPercentage: 40, servingSize: '1 medium lemon', caloriesPerServing: 15, + trivia: [], ), Veggie( id: 16, @@ -214,6 +257,7 @@ class LocalVeggieProvider { vitaminCPercentage: 35, servingSize: '1 medium lime', caloriesPerServing: 20, + trivia: [], ), Veggie( id: 17, @@ -227,6 +271,7 @@ class LocalVeggieProvider { vitaminCPercentage: 203, servingSize: '1 fruit', caloriesPerServing: 201, + trivia: [], ), Veggie( id: 18, @@ -240,6 +285,7 @@ class LocalVeggieProvider { vitaminCPercentage: 2, servingSize: '5 medium \'shrooms', caloriesPerServing: 20, + trivia: [], ), Veggie( id: 19, @@ -253,6 +299,7 @@ class LocalVeggieProvider { vitaminCPercentage: 15, servingSize: '1 medium nectarine', caloriesPerServing: 60, + trivia: [], ), Veggie( id: 20, @@ -266,6 +313,7 @@ class LocalVeggieProvider { vitaminCPercentage: 27, servingSize: '1 fruit', caloriesPerServing: 32, + trivia: [], ), Veggie( id: 21, @@ -279,6 +327,7 @@ class LocalVeggieProvider { vitaminCPercentage: 10, servingSize: '2 medium plums', caloriesPerServing: 70, + trivia: [], ), Veggie( id: 22, @@ -292,6 +341,7 @@ class LocalVeggieProvider { vitaminCPercentage: 45, servingSize: '1 medium spud', caloriesPerServing: 110, + trivia: [], ), Veggie( id: 23, @@ -305,6 +355,7 @@ class LocalVeggieProvider { vitaminCPercentage: 10, servingSize: '2 cups shredded', caloriesPerServing: 20, + trivia: [], ), Veggie( id: 24, @@ -318,6 +369,7 @@ class LocalVeggieProvider { vitaminCPercentage: 30, servingSize: '7 radishes', caloriesPerServing: 10, + trivia: [], ), Veggie( id: 25, @@ -331,6 +383,7 @@ class LocalVeggieProvider { vitaminCPercentage: 48, servingSize: '1 cup diced butternut', caloriesPerServing: 63, + trivia: [], ), Veggie( id: 26, @@ -345,6 +398,7 @@ class LocalVeggieProvider { vitaminCPercentage: 160, servingSize: '8 medium strawberries', caloriesPerServing: 50, + trivia: [], ), Veggie( id: 27, @@ -358,6 +412,7 @@ class LocalVeggieProvider { vitaminCPercentage: 181, servingSize: '1 medium tangelo', caloriesPerServing: 60, + trivia: [], ), Veggie( id: 28, @@ -371,6 +426,7 @@ class LocalVeggieProvider { vitaminCPercentage: 40, servingSize: '1 medium tomato', caloriesPerServing: 25, + trivia: [], ), Veggie( id: 29, @@ -384,6 +440,7 @@ class LocalVeggieProvider { vitaminCPercentage: 25, servingSize: '2 cups diced', caloriesPerServing: 80, + trivia: [], ), Veggie( id: 30, @@ -397,6 +454,7 @@ class LocalVeggieProvider { vitaminCPercentage: 190, servingSize: '1 medium pepper', caloriesPerServing: 25, + trivia: [], ), ]; } diff --git a/veggieseasons/lib/data/veggie.dart b/veggieseasons/lib/data/veggie.dart index c711b35c4..48e2d6af3 100644 --- a/veggieseasons/lib/data/veggie.dart +++ b/veggieseasons/lib/data/veggie.dart @@ -33,6 +33,14 @@ enum Season { autumn, } +class Trivia { + final String question; + final List answers; + final int correctAnswerIndex; + + const Trivia(this.question, this.answers, this.correctAnswerIndex); +} + const Map veggieCategoryNames = { VeggieCategory.allium: 'Allium', VeggieCategory.berry: 'Berry', @@ -67,6 +75,7 @@ class Veggie { @required this.vitaminCPercentage, @required this.servingSize, @required this.caloriesPerServing, + @required this.trivia, this.isFavorite = false, }); @@ -80,7 +89,7 @@ class Veggie { final VeggieCategory category; - /// A short, snappy line, possibly with trivia. + /// A short, snappy line. final String shortDescription; /// A color value to use when constructing UI elements to match the image @@ -108,5 +117,8 @@ class Veggie { /// as a favorite). bool isFavorite; + /// A set of trivia questions and answers related to the veggie. + final List trivia; + String get categoryName => veggieCategoryNames[category]; } diff --git a/veggieseasons/lib/screens/details.dart b/veggieseasons/lib/screens/details.dart index c90e73d61..49b8a3838 100644 --- a/veggieseasons/lib/screens/details.dart +++ b/veggieseasons/lib/screens/details.dart @@ -10,6 +10,7 @@ import 'package:veggieseasons/data/preferences.dart'; import 'package:veggieseasons/data/veggie.dart'; import 'package:veggieseasons/styles.dart'; import 'package:veggieseasons/widgets/close_button.dart'; +import 'package:veggieseasons/widgets/trivia.dart'; class ServingInfoChart extends StatelessWidget { const ServingInfoChart(this.veggie, this.prefs); @@ -211,20 +212,6 @@ class InfoView extends StatelessWidget { } } -class TriviaView extends StatelessWidget { - final int id; - - const TriviaView(this.id); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(32.0), - child: Text('Trivia goes here.'), - ); - } -} - class DetailsScreen extends StatefulWidget { final int id; diff --git a/veggieseasons/lib/styles.dart b/veggieseasons/lib/styles.dart index af88938b8..dc004f3de 100644 --- a/veggieseasons/lib/styles.dart +++ b/veggieseasons/lib/styles.dart @@ -127,6 +127,30 @@ abstract class Styles { fontWeight: FontWeight.normal, ); + static const triviaFinishedTitleText = TextStyle( + color: Color.fromRGBO(0, 0, 0, 0.9), + fontFamily: 'NotoSans', + fontSize: 32.0, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + ); + + static const triviaFinishedText = TextStyle( + color: Color.fromRGBO(0, 0, 0, 0.9), + fontFamily: 'NotoSans', + fontSize: 16.0, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + ); + + static const triviaFinishedBigText = TextStyle( + color: Color.fromRGBO(0, 0, 0, 0.9), + fontFamily: 'NotoSans', + fontSize: 48.0, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.normal, + ); + static const appBackground = Color(0xffd0d0d0); static const scaffoldBackground = Color(0xfff0f0f0); diff --git a/veggieseasons/lib/widgets/trivia.dart b/veggieseasons/lib/widgets/trivia.dart new file mode 100644 index 000000000..766b1b365 --- /dev/null +++ b/veggieseasons/lib/widgets/trivia.dart @@ -0,0 +1,210 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/widgets.dart'; +import 'package:scoped_model/scoped_model.dart'; +import 'package:veggieseasons/data/app_state.dart'; +import 'package:veggieseasons/data/veggie.dart'; +import 'package:veggieseasons/styles.dart'; + +/// Presents a series of trivia questions about a particular widget, and tracks +/// the user's score. +class TriviaView extends StatefulWidget { + final int id; + + const TriviaView(this.id); + + @override + _TriviaViewState createState() => _TriviaViewState(); +} + +/// Possible states of the game. +enum PlayerStatus { + readyToAnswer, + wasCorrect, + wasIncorrect, +} + +class _TriviaViewState extends State { + /// Current app state. This is used to fetch veggie data. + AppState appState; + + /// The veggie trivia about which to show. + Veggie veggie; + + /// Index of the current trivia question. + int triviaIndex = 0; + + /// User's score on the current veggie. + int score = 0; + + /// Trivia question currently being displayed. + Trivia get currentTrivia => veggie.trivia[triviaIndex]; + + /// The current state of the game. + PlayerStatus status = PlayerStatus.readyToAnswer; + + // Called at init and again if any dependencies (read: InheritedWidgets) on + // on which this object relies are changed. + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + final newAppState = + ScopedModel.of(context, rebuildOnChange: true); + + setState(() { + appState = newAppState; + veggie = appState.getVeggie(widget.id); + }); + } + + // Called when the widget associated with this object is swapped out for a new + // one. If the new widget has a different Veggie ID value, the state object + // needs to do a little work to reset itself for the new Veggie. + @override + void didUpdateWidget(TriviaView oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.id != widget.id) { + setState(() { + veggie = appState.getVeggie(widget.id); + }); + + _resetGame(); + } + } + + @override + Widget build(BuildContext context) { + if (triviaIndex >= veggie.trivia.length) { + return _buildFinishedView(); + } else if (status == PlayerStatus.readyToAnswer) { + return _buildQuestionView(); + } else { + return _buildResultView(); + } + } + + void _resetGame() { + setState(() { + triviaIndex = 0; + score = 0; + status = PlayerStatus.readyToAnswer; + }); + } + + void _processAnswer(int answerIndex) { + setState(() { + if (answerIndex == currentTrivia.correctAnswerIndex) { + status = PlayerStatus.wasCorrect; + score++; + } else { + status = PlayerStatus.wasIncorrect; + } + }); + } + + // Widget shown when the game is over. It includes the score and a button to + // restart everything. + Widget _buildFinishedView() { + return Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + children: [ + Text( + 'All done!', + style: Styles.triviaFinishedTitleText, + ), + SizedBox(height: 16.0), + Text( + 'You answered', + style: Styles.triviaFinishedText, + ), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + '$score', + style: Styles.triviaFinishedBigText, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + ' of ', + style: Styles.triviaFinishedText, + ), + ), + Text( + '${veggie.trivia.length}', + style: Styles.triviaFinishedBigText, + ), + ], + ), + Text( + 'questions correctly!', + style: Styles.triviaFinishedText, + ), + SizedBox(height: 16.0), + CupertinoButton( + child: Text('Try Again'), + onPressed: () => _resetGame(), + ), + ], + ), + ); + } + + // Presents the current trivia's question and answer choices. + Widget _buildQuestionView() { + List buttons = []; + + for (int i = 0; i < currentTrivia.answers.length; i++) { + buttons.add(Padding( + padding: const EdgeInsets.all(8.0), + child: CupertinoButton( + color: CupertinoColors.activeBlue, + child: Text( + currentTrivia.answers[i], + textAlign: TextAlign.center, + ), + onPressed: () => _processAnswer(i), + ), + )); + } + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + SizedBox(height: 16.0), + Text(currentTrivia.question), + SizedBox(height: 32.0), + ]..addAll(buttons), + ), + ); + } + + // Shows whether the last answer was right or wrong and prompts the user to + // continue through the game. + Widget _buildResultView() { + return Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + children: [ + Text(status == PlayerStatus.wasCorrect + ? 'That\'s right!' + : 'Sorry, that wasn\'t the right answer.'), + SizedBox(height: 16.0), + CupertinoButton( + child: Text('Next Question'), + onPressed: () => setState(() { + triviaIndex++; + status = PlayerStatus.readyToAnswer; + }), + ), + ], + ), + ); + } +}