1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-10 14:58:34 +00:00

State Restoration support for Veggie Seasons app (#433)

This commit is contained in:
Michael Goderbauer
2020-11-02 12:19:21 -08:00
committed by GitHub
parent d30bfd59ec
commit ed1503143e
138 changed files with 816 additions and 430 deletions

View File

@@ -1,73 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:veggieseasons/data/local_veggie_provider.dart';
import 'package:veggieseasons/data/veggie.dart';
class AppState extends ChangeNotifier {
final List<Veggie> _veggies;
AppState() : _veggies = LocalVeggieProvider.veggies;
List<Veggie> get allVeggies => List<Veggie>.from(_veggies);
List<Veggie> get availableVeggies {
var currentSeason = _getSeasonForDate(DateTime.now());
return _veggies.where((v) => v.seasons.contains(currentSeason)).toList();
}
List<Veggie> get favoriteVeggies =>
_veggies.where((v) => v.isFavorite).toList();
List<Veggie> get unavailableVeggies {
var currentSeason = _getSeasonForDate(DateTime.now());
return _veggies.where((v) => !v.seasons.contains(currentSeason)).toList();
}
Veggie getVeggie(int id) => _veggies.singleWhere((v) => v.id == id);
List<Veggie> searchVeggies(String terms) => _veggies
.where((v) => v.name.toLowerCase().contains(terms.toLowerCase()))
.toList();
void setFavorite(int id, bool isFavorite) {
var veggie = getVeggie(id);
veggie.isFavorite = isFavorite;
notifyListeners();
}
static Season _getSeasonForDate(DateTime date) {
// Technically the start and end dates of seasons can vary by a day or so,
// but this is close enough for produce.
switch (date.month) {
case 1:
return Season.winter;
case 2:
return Season.winter;
case 3:
return date.day < 21 ? Season.winter : Season.spring;
case 4:
return Season.spring;
case 5:
return Season.spring;
case 6:
return date.day < 21 ? Season.spring : Season.summer;
case 7:
return Season.summer;
case 8:
return Season.summer;
case 9:
return date.day < 22 ? Season.autumn : Season.winter;
case 10:
return Season.autumn;
case 11:
return Season.autumn;
case 12:
return date.day < 22 ? Season.autumn : Season.winter;
default:
throw ArgumentError('Can\'t return a season for month #${date.month}.');
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,84 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:veggieseasons/data/veggie.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// A model class that mirrors the options in [SettingsScreen] and stores data
/// in shared preferences.
class Preferences extends ChangeNotifier {
// Keys to use with shared preferences.
static const _caloriesKey = 'calories';
static const _preferredCategoriesKey = 'preferredCategories';
// Indicates whether a call to [_loadFromSharedPrefs] is in progress;
Future<void> _loading;
int _desiredCalories = 2000;
final Set<VeggieCategory> _preferredCategories = <VeggieCategory>{};
Future<int> get desiredCalories async {
await _loading;
return _desiredCalories;
}
Future<Set<VeggieCategory>> get preferredCategories async {
await _loading;
return Set.from(_preferredCategories);
}
Future<void> addPreferredCategory(VeggieCategory category) async {
_preferredCategories.add(category);
await _saveToSharedPrefs();
notifyListeners();
}
Future<void> removePreferredCategory(VeggieCategory category) async {
_preferredCategories.remove(category);
await _saveToSharedPrefs();
notifyListeners();
}
Future<void> setDesiredCalories(int calories) async {
_desiredCalories = calories;
await _saveToSharedPrefs();
notifyListeners();
}
void load() {
_loading = _loadFromSharedPrefs();
}
Future<void> _saveToSharedPrefs() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_caloriesKey, _desiredCalories);
// Store preferred categories as a comma-separated string containing their
// indices.
await prefs.setString(_preferredCategoriesKey,
_preferredCategories.map((c) => c.index.toString()).join(','));
}
Future<void> _loadFromSharedPrefs() async {
final prefs = await SharedPreferences.getInstance();
_desiredCalories = prefs.getInt(_caloriesKey) ?? 2000;
_preferredCategories.clear();
final names = prefs.getString(_preferredCategoriesKey);
if (names != null && names.isNotEmpty) {
for (final name in names.split(',')) {
final index = int.tryParse(name) ?? -1;
if (VeggieCategory.values[index] != null) {
_preferredCategories.add(VeggieCategory.values[index]);
}
}
}
notifyListeners();
}
}

View File

@@ -1,131 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:meta/meta.dart';
enum VeggieCategory {
allium,
berry,
citrus,
cruciferous,
fern,
flower,
fruit,
fungus,
gourd,
leafy,
legume,
melon,
root,
stealthFruit,
stoneFruit,
tropical,
tuber,
vegetable,
}
enum Season {
winter,
spring,
summer,
autumn,
}
class Trivia {
final String question;
final List<String> answers;
final int correctAnswerIndex;
const Trivia(this.question, this.answers, this.correctAnswerIndex);
}
const Map<VeggieCategory, String> veggieCategoryNames = {
VeggieCategory.allium: 'Allium',
VeggieCategory.berry: 'Berry',
VeggieCategory.citrus: 'Citrus',
VeggieCategory.cruciferous: 'Cruciferous',
VeggieCategory.fern: 'Technically a fern',
VeggieCategory.flower: 'Flower',
VeggieCategory.fruit: 'Fruit',
VeggieCategory.fungus: 'Fungus',
VeggieCategory.gourd: 'Gourd',
VeggieCategory.leafy: 'Leafy',
VeggieCategory.legume: 'Legume',
VeggieCategory.melon: 'Melon',
VeggieCategory.root: 'Root vegetable',
VeggieCategory.stealthFruit: 'Stealth fruit',
VeggieCategory.stoneFruit: 'Stone fruit',
VeggieCategory.tropical: 'Tropical',
VeggieCategory.tuber: 'Tuber',
VeggieCategory.vegetable: 'Vegetable',
};
const Map<Season, String> seasonNames = {
Season.winter: 'Winter',
Season.spring: 'Spring',
Season.summer: 'Summer',
Season.autumn: 'Autumn',
};
class Veggie {
Veggie({
@required this.id,
@required this.name,
@required this.imageAssetPath,
@required this.category,
@required this.shortDescription,
@required this.accentColor,
@required this.seasons,
@required this.vitaminAPercentage,
@required this.vitaminCPercentage,
@required this.servingSize,
@required this.caloriesPerServing,
@required this.trivia,
this.isFavorite = false,
});
final int id;
final String name;
/// Each veggie has an associated image asset that's used as a background
/// image and icon.
final String imageAssetPath;
final VeggieCategory category;
/// A short, snappy line.
final String shortDescription;
/// A color value to use when constructing UI elements to match the image
/// found at [imageAssetPath].
final Color accentColor;
/// Seasons during which a veggie is harvested.
final List<Season> seasons;
/// Percentage of the FDA's recommended daily value of vitamin A for someone
/// with a 2,000 calorie diet.
final int vitaminAPercentage;
/// Percentage of the FDA's recommended daily value of vitamin C for someone
/// with a 2,000 calorie diet.
final int vitaminCPercentage;
/// A text description of a single serving (e.g. '1 apple' or '1/2 cup').
final String servingSize;
/// Calories per serving (as described in [servingSize]).
final int caloriesPerServing;
/// Whether or not the veggie has been saved to the user's garden (i.e. marked
/// as a favorite).
bool isFavorite;
/// A set of trivia questions and answers related to the veggie.
final List<Trivia> trivia;
String get categoryName => veggieCategoryNames[category];
}

View File

@@ -1,35 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart' show DeviceOrientation, SystemChrome;
import 'package:provider/provider.dart';
import 'package:veggieseasons/data/app_state.dart';
import 'package:veggieseasons/data/preferences.dart';
import 'package:veggieseasons/screens/home.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => AppState(),
),
ChangeNotifierProvider(
create: (_) => Preferences()..load(),
),
],
child: CupertinoApp(
debugShowCheckedModeBanner: false,
home: HomeScreen(),
),
),
);
}

View File

@@ -1,315 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:veggieseasons/data/app_state.dart';
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);
final Veggie veggie;
final Preferences prefs;
// Creates a [Text] widget to display a veggie's "percentage of your daily
// value of this vitamin" data adjusted for the user's preferred calorie
// target.
Widget _buildVitaminText(int standardPercentage, Future<int> targetCalories) {
return FutureBuilder<int>(
future: targetCalories,
builder: (context, snapshot) {
final target = snapshot?.data ?? 2000;
final percent = standardPercentage * 2000 ~/ target;
final themeData = CupertinoTheme.of(context);
return Text(
'$percent% DV',
textAlign: TextAlign.end,
style: Styles.detailsServingValueText(themeData),
);
},
);
}
@override
Widget build(BuildContext context) {
final themeData = CupertinoTheme.of(context);
return Column(
children: [
SizedBox(height: 16),
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(
left: 9,
bottom: 4,
),
child: Text(
'Serving info',
style: Styles.detailsServingHeaderText,
),
),
),
Container(
decoration: BoxDecoration(
border: Border.all(color: Styles.servingInfoBorderColor),
),
padding: const EdgeInsets.all(8),
child: Column(
children: [
Table(
children: [
TableRow(
children: [
TableCell(
child: Text(
'Serving size:',
style: Styles.detailsServingLabelText(themeData),
),
),
TableCell(
child: Text(
veggie.servingSize,
textAlign: TextAlign.end,
style: Styles.detailsServingValueText(themeData),
),
),
],
),
TableRow(
children: [
TableCell(
child: Text(
'Calories:',
style: Styles.detailsServingLabelText(themeData),
),
),
TableCell(
child: Text(
'${veggie.caloriesPerServing} kCal',
textAlign: TextAlign.end,
style: Styles.detailsServingValueText(themeData),
),
),
],
),
TableRow(
children: [
TableCell(
child: Text(
'Vitamin A:',
style: Styles.detailsServingLabelText(themeData),
),
),
TableCell(
child: _buildVitaminText(
veggie.vitaminAPercentage,
prefs.desiredCalories,
),
),
],
),
TableRow(
children: [
TableCell(
child: Text(
'Vitamin C:',
style: Styles.detailsServingLabelText(themeData),
),
),
TableCell(
child: _buildVitaminText(
veggie.vitaminCPercentage,
prefs.desiredCalories,
),
),
],
),
],
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: FutureBuilder(
future: prefs.desiredCalories,
builder: (context, snapshot) {
return Text(
'Percent daily values based on a diet of '
'${snapshot?.data ?? '2,000'} calories.',
style: Styles.detailsServingNoteText(themeData),
);
},
),
),
],
),
)
],
);
}
}
class InfoView extends StatelessWidget {
final int id;
const InfoView(this.id);
@override
Widget build(BuildContext context) {
final appState = Provider.of<AppState>(context);
final prefs = Provider.of<Preferences>(context);
final veggie = appState.getVeggie(id);
final themeData = CupertinoTheme.of(context);
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
FutureBuilder<Set<VeggieCategory>>(
future: prefs.preferredCategories,
builder: (context, snapshot) {
return Text(
veggie.categoryName.toUpperCase(),
style: (snapshot.hasData &&
snapshot.data.contains(veggie.category))
? Styles.detailsPreferredCategoryText(themeData)
: Styles.detailsCategoryText(themeData),
);
},
),
Spacer(),
for (Season season in veggie.seasons) ...[
SizedBox(width: 12),
Padding(
padding: Styles.seasonIconPadding[season],
child: Icon(
Styles.seasonIconData[season],
semanticLabel: seasonNames[season],
color: Styles.seasonColors[season],
),
),
],
],
),
SizedBox(height: 8),
Text(
veggie.name,
style: Styles.detailsTitleText(themeData),
),
SizedBox(height: 8),
Text(
veggie.shortDescription,
style: Styles.detailsDescriptionText(themeData),
),
ServingInfoChart(veggie, prefs),
SizedBox(height: 24),
Row(
mainAxisSize: MainAxisSize.min,
children: [
CupertinoSwitch(
value: veggie.isFavorite,
onChanged: (value) {
appState.setFavorite(id, value);
},
),
SizedBox(width: 8),
Text(
'Save to Garden',
style: CupertinoTheme.of(context).textTheme.textStyle,
),
],
),
],
),
);
}
}
class DetailsScreen extends StatefulWidget {
final int id;
DetailsScreen(this.id);
@override
_DetailsScreenState createState() => _DetailsScreenState();
}
class _DetailsScreenState extends State<DetailsScreen> {
int _selectedViewIndex = 0;
Widget _buildHeader(BuildContext context, AppState model) {
final veggie = model.getVeggie(widget.id);
return SizedBox(
height: 150,
child: Stack(
children: [
Positioned(
right: 0,
left: 0,
child: Image.asset(
veggie.imageAssetPath,
fit: BoxFit.cover,
semanticLabel: 'A background image of ${veggie.name}',
),
),
Positioned(
top: 16,
left: 16,
child: SafeArea(
child: CloseButton(() {
Navigator.of(context).pop();
}),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final appState = Provider.of<AppState>(context);
return CupertinoPageScaffold(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: ListView(
children: [
_buildHeader(context, appState),
SizedBox(height: 20),
CupertinoSegmentedControl<int>(
children: {
0: Text('Facts & Info'),
1: Text('Trivia'),
},
groupValue: _selectedViewIndex,
onValueChanged: (value) {
setState(() => _selectedViewIndex = value);
},
),
_selectedViewIndex == 0
? InfoView(widget.id)
: TriviaView(widget.id),
],
),
),
],
),
);
}
}

View File

@@ -1,49 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:veggieseasons/data/app_state.dart';
import 'package:veggieseasons/data/veggie.dart';
import 'package:veggieseasons/styles.dart';
import 'package:veggieseasons/widgets/veggie_headline.dart';
class FavoritesScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CupertinoTabView(
builder: (context) {
final model = Provider.of<AppState>(context);
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('My Garden'),
),
child: Center(
child: model.favoriteVeggies.isEmpty
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
'You haven\'t added any favorite veggies to your garden yet.',
style: Styles.headlineDescription(
CupertinoTheme.of(context)),
),
)
: ListView(
children: [
SizedBox(height: 24),
for (Veggie veggie in model.favoriteVeggies)
Padding(
padding: EdgeInsets.fromLTRB(16, 0, 16, 24),
child: VeggieHeadline(veggie),
),
],
),
),
);
},
);
}
}

View File

@@ -1,47 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:veggieseasons/screens/favorites.dart';
import 'package:veggieseasons/screens/list.dart';
import 'package:veggieseasons/screens/search.dart';
import 'package:veggieseasons/screens/settings.dart';
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CupertinoTabScaffold(
tabBar: CupertinoTabBar(items: [
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.book),
label: 'My Garden',
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.search),
label: 'Search',
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.settings),
label: 'Settings',
),
]),
tabBuilder: (context, index) {
if (index == 0) {
return ListScreen();
} else if (index == 1) {
return FavoritesScreen();
} else if (index == 2) {
return SearchScreen();
} else {
return SettingsScreen();
}
},
);
}
}

View File

@@ -1,79 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:veggieseasons/data/app_state.dart';
import 'package:veggieseasons/data/preferences.dart';
import 'package:veggieseasons/data/veggie.dart';
import 'package:veggieseasons/styles.dart';
import 'package:veggieseasons/widgets/veggie_card.dart';
class ListScreen extends StatelessWidget {
Widget _generateVeggieRow(Veggie veggie, Preferences prefs,
{bool inSeason = true}) {
return Padding(
padding: EdgeInsets.only(left: 16, right: 16, bottom: 24),
child: FutureBuilder<Set<VeggieCategory>>(
future: prefs.preferredCategories,
builder: (context, snapshot) {
final data = snapshot.data ?? <VeggieCategory>{};
return VeggieCard(veggie, inSeason, data.contains(veggie.category));
}),
);
}
@override
Widget build(BuildContext context) {
return CupertinoTabView(
builder: (context) {
var dateString = DateFormat('MMMM y').format(DateTime.now());
final appState = Provider.of<AppState>(context);
final prefs = Provider.of<Preferences>(context);
final themeData = CupertinoTheme.of(context);
return SafeArea(
bottom: false,
child: ListView.builder(
itemCount: appState.allVeggies.length + 2,
itemBuilder: (context, index) {
if (index == 0) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(dateString.toUpperCase(), style: Styles.minorText),
Text('In season today',
style: Styles.headlineText(themeData)),
],
),
);
} else if (index <= appState.availableVeggies.length) {
return _generateVeggieRow(
appState.availableVeggies[index - 1],
prefs,
);
} else if (index <= appState.availableVeggies.length + 1) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 16),
child: Text('Not in season',
style: Styles.headlineText(themeData)),
);
} else {
var relativeIndex =
index - (appState.availableVeggies.length + 2);
return _generateVeggieRow(
appState.unavailableVeggies[relativeIndex], prefs,
inSeason: false);
}
},
),
);
},
);
}
}

View File

@@ -1,105 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:veggieseasons/data/app_state.dart';
import 'package:veggieseasons/data/veggie.dart';
import 'package:veggieseasons/styles.dart';
import 'package:veggieseasons/widgets/search_bar.dart';
import 'package:veggieseasons/widgets/veggie_headline.dart';
class SearchScreen extends StatefulWidget {
@override
_SearchScreenState createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
final controller = TextEditingController();
final focusNode = FocusNode();
String terms = '';
@override
void initState() {
super.initState();
controller.addListener(_onTextChanged);
}
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
void _onTextChanged() {
setState(() => terms = controller.text);
}
Widget _createSearchBox() {
return Padding(
padding: const EdgeInsets.all(8),
child: SearchBar(
controller: controller,
focusNode: focusNode,
),
);
}
Widget _buildSearchResults(List<Veggie> veggies) {
if (veggies.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
'No veggies matching your search terms were found.',
style: Styles.headlineDescription(CupertinoTheme.of(context)),
),
),
);
}
return ListView.builder(
itemCount: veggies.length + 1,
itemBuilder: (context, i) {
if (i == 0) {
return Visibility(
// This invisible and otherwise unnecessary search box is used to
// pad the list entries downward, so none will be underneath the
// real search box when the list is at its top scroll position.
child: _createSearchBox(),
visible: false,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
);
} else {
return Padding(
padding: EdgeInsets.only(left: 16, right: 16, bottom: 24),
child: VeggieHeadline(veggies[i - 1]),
);
}
},
);
}
@override
Widget build(BuildContext context) {
final model = Provider.of<AppState>(context);
return CupertinoTabView(
builder: (context) {
return SafeArea(
bottom: false,
child: Stack(
children: [
_buildSearchResults(model.searchVeggies(terms)),
_createSearchBox(),
],
),
);
},
);
}
}

View File

@@ -1,214 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import 'package:veggieseasons/data/preferences.dart';
import 'package:veggieseasons/data/veggie.dart';
import 'package:veggieseasons/styles.dart';
import 'package:veggieseasons/widgets/settings_group.dart';
import 'package:veggieseasons/widgets/settings_item.dart';
class VeggieCategorySettingsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final model = Provider.of<Preferences>(context);
final currentPrefs = model.preferredCategories;
var brightness = CupertinoTheme.brightnessOf(context);
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Preferred Categories'),
previousPageTitle: 'Settings',
),
backgroundColor: Styles.scaffoldBackground(brightness),
child: FutureBuilder<Set<VeggieCategory>>(
future: currentPrefs,
builder: (context, snapshot) {
final items = <SettingsItem>[];
for (final category in VeggieCategory.values) {
CupertinoSwitch toggle;
// It's possible that category data hasn't loaded from shared prefs
// yet, so display it if possible and fall back to disabled switches
// otherwise.
if (snapshot.hasData) {
toggle = CupertinoSwitch(
value: snapshot.data.contains(category),
onChanged: (value) {
if (value) {
model.addPreferredCategory(category);
} else {
model.removePreferredCategory(category);
}
},
);
} else {
toggle = CupertinoSwitch(
value: false,
onChanged: null,
);
}
items.add(SettingsItem(
label: veggieCategoryNames[category],
content: toggle,
));
}
return ListView(
children: [
SettingsGroup(
items: items,
),
],
);
},
),
);
}
}
class CalorieSettingsScreen extends StatelessWidget {
static const max = 1000;
static const min = 2600;
static const step = 200;
@override
Widget build(BuildContext context) {
final model = Provider.of<Preferences>(context);
var brightness = CupertinoTheme.brightnessOf(context);
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
previousPageTitle: 'Settings',
),
backgroundColor: Styles.scaffoldBackground(brightness),
child: ListView(
children: [
FutureBuilder<int>(
future: model.desiredCalories,
builder: (context, snapshot) {
final steps = <SettingsItem>[];
for (var cals = max; cals < min; cals += step) {
steps.add(
SettingsItem(
label: cals.toString(),
icon: SettingsIcon(
icon: Styles.checkIcon,
foregroundColor: snapshot.hasData && snapshot.data == cals
? CupertinoColors.activeBlue
: Styles.transparentColor,
backgroundColor: Styles.transparentColor,
),
onPress: snapshot.hasData
? () => model.setDesiredCalories(cals)
: null,
),
);
}
return SettingsGroup(
items: steps,
header: SettingsGroupHeader('Available calorie levels'),
footer: SettingsGroupFooter('These are used for serving '
'calculations'),
);
},
),
],
),
);
}
}
class SettingsScreen extends StatelessWidget {
SettingsItem _buildCaloriesItem(BuildContext context, Preferences prefs) {
return SettingsItem(
label: 'Calorie Target',
icon: SettingsIcon(
backgroundColor: Styles.iconBlue,
icon: Styles.calorieIcon,
),
content: FutureBuilder<int>(
future: prefs.desiredCalories,
builder: (context, snapshot) {
return Row(
children: [
Text(
snapshot.data?.toString() ?? '',
style: CupertinoTheme.of(context).textTheme.textStyle,
),
SizedBox(width: 8),
SettingsNavigationIndicator(),
],
);
},
),
onPress: () {
Navigator.of(context).push<void>(
CupertinoPageRoute(
builder: (context) => CalorieSettingsScreen(),
title: 'Calorie Target',
),
);
},
);
}
SettingsItem _buildCategoriesItem(BuildContext context, Preferences prefs) {
return SettingsItem(
label: 'Preferred Categories',
subtitle: 'What types of veggies you prefer!',
icon: SettingsIcon(
backgroundColor: Styles.iconGold,
icon: Styles.preferenceIcon,
),
content: SettingsNavigationIndicator(),
onPress: () {
Navigator.of(context).push<void>(
CupertinoPageRoute(
builder: (context) => VeggieCategorySettingsScreen(),
title: 'Preferred Categories',
),
);
},
);
}
@override
Widget build(BuildContext context) {
final prefs = Provider.of<Preferences>(context);
return CupertinoPageScaffold(
child: Container(
color: Styles.scaffoldBackground(CupertinoTheme.brightnessOf(context)),
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar(
largeTitle: Text('Settings'),
),
SliverSafeArea(
top: false,
sliver: SliverList(
delegate: SliverChildListDelegate(
<Widget>[
SettingsGroup(
items: [
_buildCaloriesItem(context, prefs),
_buildCategoriesItem(context, prefs),
],
),
],
),
),
),
],
),
),
);
}
}

View File

@@ -1,306 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:veggieseasons/data/veggie.dart';
abstract class Styles {
static TextStyle headlineText(CupertinoThemeData themeData) => TextStyle(
color: themeData.textTheme.textStyle.color,
fontFamily: 'NotoSans',
fontSize: 32,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.bold,
);
static const minorText = TextStyle(
color: Color.fromRGBO(128, 128, 128, 1),
fontFamily: 'NotoSans',
fontSize: 16,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
);
static TextStyle headlineName(CupertinoThemeData themeData) => TextStyle(
color: themeData.textTheme.textStyle.color,
fontFamily: 'NotoSans',
fontSize: 24,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.bold,
);
static TextStyle headlineDescription(CupertinoThemeData themeData) =>
TextStyle(
color: themeData.textTheme.textStyle.color,
fontFamily: 'NotoSans',
fontSize: 16,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
);
static const cardTitleText = TextStyle(
color: Color.fromRGBO(0, 0, 0, 0.9),
fontFamily: 'NotoSans',
fontSize: 32,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.bold,
);
static const cardCategoryText = TextStyle(
color: Color.fromRGBO(255, 255, 255, 0.9),
fontFamily: 'NotoSans',
fontSize: 16,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
);
static const cardDescriptionText = TextStyle(
color: Color.fromRGBO(0, 0, 0, 0.9),
fontFamily: 'NotoSans',
fontSize: 16,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
);
static TextStyle detailsTitleText(CupertinoThemeData themeData) => TextStyle(
color: themeData.textTheme.textStyle.color,
fontFamily: 'NotoSans',
fontSize: 30,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.bold,
);
static TextStyle detailsPreferredCategoryText(CupertinoThemeData themeData) =>
TextStyle(
color: themeData.textTheme.textStyle.color,
fontFamily: 'NotoSans',
fontSize: 16,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.bold,
);
static TextStyle detailsCategoryText(CupertinoThemeData themeData) =>
TextStyle(
color: themeData.textTheme.textStyle.color,
fontFamily: 'NotoSans',
fontSize: 16,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
);
static TextStyle detailsDescriptionText(CupertinoThemeData themeData) =>
TextStyle(
color: themeData.textTheme.textStyle.color,
fontFamily: 'NotoSans',
fontSize: 16,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
);
static const detailsBoldDescriptionText = TextStyle(
color: Color.fromRGBO(0, 0, 0, 0.9),
fontFamily: 'NotoSans',
fontSize: 16,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.bold,
);
static const detailsServingHeaderText = TextStyle(
color: Color.fromRGBO(176, 176, 176, 1),
fontFamily: 'NotoSans',
fontSize: 16,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.bold,
);
static TextStyle detailsServingLabelText(CupertinoThemeData themeData) =>
TextStyle(
color: themeData.textTheme.textStyle.color,
fontFamily: 'NotoSans',
fontSize: 16,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.bold,
);
static TextStyle detailsServingValueText(CupertinoThemeData themeData) =>
TextStyle(
color: themeData.textTheme.textStyle.color,
fontFamily: 'NotoSans',
fontSize: 16,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
);
static TextStyle detailsServingNoteText(CupertinoThemeData themeData) =>
TextStyle(
color: themeData.textTheme.textStyle.color,
fontFamily: 'NotoSans',
fontSize: 16,
fontStyle: FontStyle.italic,
fontWeight: FontWeight.normal,
);
static TextStyle triviaFinishedTitleText(CupertinoThemeData themeData) =>
TextStyle(
color: themeData.textTheme.textStyle.color,
fontFamily: 'NotoSans',
fontSize: 32,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
);
static TextStyle triviaFinishedText(CupertinoThemeData themeData) =>
TextStyle(
color: themeData.textTheme.textStyle.color,
fontFamily: 'NotoSans',
fontSize: 16,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
);
static TextStyle triviaFinishedBigText(CupertinoThemeData themeData) =>
TextStyle(
color: themeData.textTheme.textStyle.color,
fontFamily: 'NotoSans',
fontSize: 48,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
);
static const appBackground = Color(0xffd0d0d0);
static Color scaffoldBackground(Brightness brightness) =>
brightness == Brightness.light
? CupertinoColors.lightBackgroundGray
: null;
static Color searchBackground(CupertinoThemeData themeData) =>
themeData.barBackgroundColor;
static const frostedBackground = Color(0xccf8f8f8);
static const closeButtonUnpressed = Color(0xff101010);
static const closeButtonPressed = Color(0xff808080);
static TextStyle searchText(CupertinoThemeData themeData) => TextStyle(
color: themeData.textTheme.textStyle.color,
fontFamily: 'NotoSans',
fontSize: 14,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
);
static TextStyle settingsItemText(CupertinoThemeData themeData) =>
themeData.textTheme.textStyle;
static TextStyle settingsItemSubtitleText(CupertinoThemeData themeData) =>
TextStyle(
fontSize: 12,
letterSpacing: -0.2,
color: themeData.textTheme.textStyle.color,
);
static const Color searchCursorColor = Color.fromRGBO(0, 122, 255, 1);
static const Color searchIconColor = Color.fromRGBO(128, 128, 128, 1);
static const seasonColors = <Season, Color>{
Season.winter: Color(0xff336dcc),
Season.spring: Color(0xff2fa02b),
Season.summer: Color(0xff287213),
Season.autumn: Color(0xff724913),
};
// While handy, some of the Font Awesome icons sometimes bleed over their
// allotted bounds. This padding is used to adjust for that.
static const seasonIconPadding = {
Season.winter: EdgeInsets.only(right: 0),
Season.spring: EdgeInsets.only(right: 4),
Season.summer: EdgeInsets.only(right: 6),
Season.autumn: EdgeInsets.only(right: 0),
};
static const seasonIconData = {
Season.winter: FontAwesomeIcons.snowflake,
Season.spring: FontAwesomeIcons.leaf,
Season.summer: FontAwesomeIcons.umbrellaBeach,
Season.autumn: FontAwesomeIcons.canadianMapleLeaf,
};
static const seasonBorder = Border(
top: BorderSide(color: Color(0xff606060)),
left: BorderSide(color: Color(0xff606060)),
bottom: BorderSide(color: Color(0xff606060)),
right: BorderSide(color: Color(0xff606060)),
);
static const uncheckedIcon = IconData(
0xf372,
fontFamily: CupertinoIcons.iconFont,
fontPackage: CupertinoIcons.iconFontPackage,
);
static const checkedIcon = IconData(
0xf373,
fontFamily: CupertinoIcons.iconFont,
fontPackage: CupertinoIcons.iconFontPackage,
);
static const transparentColor = Color(0x00000000);
static const shadowColor = Color(0xa0000000);
static const shadowGradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [transparentColor, shadowColor],
);
static const Color settingsMediumGray = Color(0xffc7c7c7);
static const Color settingsItemPressed = Color(0xffd9d9d9);
static Color settingsItemColor(Brightness brightness) =>
brightness == Brightness.light
? CupertinoColors.tertiarySystemBackground
: CupertinoColors.darkBackgroundGray;
static Color settingsLineation(Brightness brightness) =>
brightness == Brightness.light ? Color(0xffbcbbc1) : Color(0xFF4C4B4B);
static const Color settingsBackground = Color(0xffefeff4);
static const Color settingsGroupSubtitle = Color(0xff777777);
static const Color iconBlue = Color(0xff0000ff);
static const Color iconGold = Color(0xffdba800);
static const preferenceIcon = IconData(
0xf443,
fontFamily: CupertinoIcons.iconFont,
fontPackage: CupertinoIcons.iconFontPackage,
);
static const calorieIcon = IconData(
0xf3bb,
fontFamily: CupertinoIcons.iconFont,
fontPackage: CupertinoIcons.iconFontPackage,
);
static const checkIcon = IconData(
0xf383,
fontFamily: CupertinoIcons.iconFont,
fontPackage: CupertinoIcons.iconFontPackage,
);
static const servingInfoBorderColor = Color(0xffb0b0b0);
static const ColorFilter desaturatedColorFilter =
// 222222 is a random color that has low color saturation.
ColorFilter.mode(Color(0xFF222222), BlendMode.saturation);
}

View File

@@ -1,132 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:veggieseasons/styles.dart';
/// Partially overlays and then blurs its child's background.
class FrostedBox extends StatelessWidget {
const FrostedBox({
this.child,
Key key,
}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: DecoratedBox(
decoration: BoxDecoration(
color: Styles.frostedBackground,
),
child: child,
),
);
}
}
/// An Icon that implicitly animates changes to its color.
class ColorChangingIcon extends ImplicitlyAnimatedWidget {
const ColorChangingIcon(
this.icon, {
this.color = CupertinoColors.black,
this.size,
@required Duration duration,
Key key,
}) : assert(icon != null),
assert(color != null),
assert(duration != null),
super(key: key, duration: duration);
final Color color;
final IconData icon;
final double size;
@override
_ColorChangingIconState createState() => _ColorChangingIconState();
}
class _ColorChangingIconState
extends AnimatedWidgetBaseState<ColorChangingIcon> {
ColorTween _colorTween;
@override
Widget build(BuildContext context) {
return Icon(
widget.icon,
semanticLabel: 'Close button',
size: widget.size,
color: _colorTween?.evaluate(animation),
);
}
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_colorTween = visitor(
_colorTween,
widget.color,
(dynamic value) => ColorTween(begin: value as Color),
) as ColorTween;
}
}
/// A simple "close this modal" button that invokes a callback when pressed.
class CloseButton extends StatefulWidget {
const CloseButton(this.onPressed);
final VoidCallback onPressed;
@override
CloseButtonState createState() {
return CloseButtonState();
}
}
class CloseButtonState extends State<CloseButton> {
bool tapInProgress = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (details) {
setState(() => tapInProgress = true);
},
onTapUp: (details) {
setState(() => tapInProgress = false);
widget.onPressed();
},
onTapCancel: () {
setState(() => tapInProgress = false);
},
child: ClipOval(
child: FrostedBox(
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
),
child: Center(
child: ColorChangingIcon(
CupertinoIcons.clear_thick,
duration: Duration(milliseconds: 300),
color: tapInProgress
? Styles.closeButtonPressed
: Styles.closeButtonUnpressed,
size: 20,
),
),
),
),
),
);
}
}

View File

@@ -1,72 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:veggieseasons/styles.dart';
class SearchBar extends StatelessWidget {
final TextEditingController controller;
final FocusNode focusNode;
SearchBar({
@required this.controller,
@required this.focusNode,
});
@override
Widget build(BuildContext context) {
final themeData = CupertinoTheme.of(context);
return ClipRRect(
borderRadius: BorderRadius.circular(10),
child: BackdropFilter(
child: DecoratedBox(
decoration: BoxDecoration(
color: Styles.searchBackground(themeData),
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 8,
),
child: Row(
children: [
ExcludeSemantics(
child: Icon(
CupertinoIcons.search,
color: Styles.searchIconColor,
),
),
Expanded(
child: CupertinoTextField(
controller: controller,
focusNode: focusNode,
decoration: null,
style: Styles.searchText(themeData),
cursorColor: Styles.searchCursorColor,
),
),
GestureDetector(
onTap: () {
controller.clear();
},
child: Icon(
CupertinoIcons.clear_thick_circled,
semanticLabel: 'Clear search terms',
color: Styles.searchIconColor,
),
),
],
),
),
),
filter: ImageFilter.blur(sigmaY: 5, sigmaX: 5),
),
);
}
}

View File

@@ -1,121 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:veggieseasons/styles.dart';
import 'settings_item.dart';
// The widgets in this file present a group of Cupertino-style settings items to
// the user. In the future, the Cupertino package in the Flutter SDK will
// include dedicated widgets for this purpose, but for now they're done here.
//
// See https://github.com/flutter/flutter/projects/29 for more info.
class SettingsGroupHeader extends StatelessWidget {
const SettingsGroupHeader(this.title);
final String title;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
left: 15,
right: 15,
bottom: 6,
),
child: Text(
title.toUpperCase(),
style: TextStyle(
color: CupertinoColors.inactiveGray,
fontSize: 13.5,
letterSpacing: -0.5,
),
),
);
}
}
class SettingsGroupFooter extends StatelessWidget {
const SettingsGroupFooter(this.title);
final String title;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
left: 15,
right: 15,
top: 7.5,
),
child: Text(
title,
style: TextStyle(
color: Styles.settingsGroupSubtitle,
fontSize: 13,
letterSpacing: -0.08,
),
),
);
}
}
class SettingsGroup extends StatelessWidget {
SettingsGroup({
@required this.items,
this.header,
this.footer,
}) : assert(items != null),
assert(items.isNotEmpty);
final List<SettingsItem> items;
final Widget header;
final Widget footer;
@override
Widget build(BuildContext context) {
var brightness = CupertinoTheme.brightnessOf(context);
final dividedItems = <Widget>[items[0]];
for (var i = 1; i < items.length; i++) {
dividedItems.add(Container(
color: Styles.settingsLineation(brightness),
height: 0.3,
));
dividedItems.add(items[i]);
}
return Padding(
padding: EdgeInsets.only(
top: header == null ? 35 : 22,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (header != null) header,
Container(
decoration: BoxDecoration(
color: CupertinoColors.white,
border: Border(
top: BorderSide(
color: Styles.settingsLineation(brightness),
width: 0,
),
bottom: BorderSide(
color: Styles.settingsLineation(brightness),
width: 0,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: dividedItems,
),
),
if (footer != null) footer,
],
),
);
}
}

View File

@@ -1,167 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:veggieseasons/styles.dart';
// The widgets in this file present a Cupertino-style settings item to the user.
// In the future, the Cupertino package in the Flutter SDK will include
// dedicated widgets for this purpose, but for now they're done here.
//
// See https://github.com/flutter/flutter/projects/29 for more info.
typedef SettingsItemCallback = FutureOr<void> Function();
class SettingsNavigationIndicator extends StatelessWidget {
const SettingsNavigationIndicator({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Icon(
CupertinoIcons.forward,
color: Styles.settingsMediumGray,
size: 21,
);
}
}
class SettingsIcon extends StatelessWidget {
const SettingsIcon({
@required this.icon,
this.foregroundColor = CupertinoColors.white,
this.backgroundColor = CupertinoColors.black,
Key key,
}) : assert(icon != null),
super(key: key);
final Color backgroundColor;
final Color foregroundColor;
final IconData icon;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: backgroundColor,
),
child: Center(
child: Icon(
icon,
color: foregroundColor,
size: 20,
),
),
);
}
}
class SettingsItem extends StatefulWidget {
const SettingsItem({
@required this.label,
this.icon,
this.content,
this.subtitle,
this.onPress,
Key key,
}) : assert(label != null),
super(key: key);
final String label;
final Widget icon;
final Widget content;
final String subtitle;
final SettingsItemCallback onPress;
@override
State<StatefulWidget> createState() => SettingsItemState();
}
class SettingsItemState extends State<SettingsItem> {
bool pressed = false;
@override
Widget build(BuildContext context) {
var themeData = CupertinoTheme.of(context);
var brightness = CupertinoTheme.brightnessOf(context);
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
color: Styles.settingsItemColor(brightness),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () async {
if (widget.onPress != null) {
setState(() {
pressed = true;
});
await widget.onPress();
Future.delayed(
Duration(milliseconds: 150),
() {
setState(() {
pressed = false;
});
},
);
}
},
child: SizedBox(
height: widget.subtitle == null ? 44 : 57,
child: Row(
children: [
if (widget.icon != null)
Padding(
padding: const EdgeInsets.only(
left: 15,
bottom: 2,
),
child: SizedBox(
height: 29,
width: 29,
child: widget.icon,
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: 15,
),
child: widget.subtitle != null
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(height: 8.5),
Text(
widget.label,
style: Styles.settingsItemText(themeData),
),
SizedBox(height: 4),
Text(
widget.subtitle,
style: Styles.settingsItemSubtitleText(themeData),
),
],
)
: Padding(
padding: EdgeInsets.only(top: 1.5),
child: Text(
widget.label,
style: Styles.settingsItemText(themeData),
),
),
),
),
Padding(
padding: const EdgeInsets.only(right: 11),
child: widget.content ?? Container(),
),
],
),
),
),
);
}
}

View File

@@ -1,213 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.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<TriviaView> {
/// 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 = Provider.of<AppState>(context);
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() {
final themeData = CupertinoTheme.of(context);
return Padding(
padding: const EdgeInsets.all(32),
child: Column(
children: [
Text(
'All done!',
style: Styles.triviaFinishedTitleText(themeData),
),
SizedBox(height: 16),
Text(
'You answered',
style: Styles.triviaFinishedText(themeData),
),
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
'$score',
style: Styles.triviaFinishedBigText(themeData),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
' of ',
style: Styles.triviaFinishedText(themeData),
),
),
Text(
'${veggie.trivia.length}',
style: Styles.triviaFinishedBigText(themeData),
),
],
),
Text(
'questions correctly!',
style: Styles.triviaFinishedText(themeData),
),
SizedBox(height: 16),
CupertinoButton(
child: Text('Try Again'),
onPressed: () => _resetGame(),
),
],
),
);
}
// Presents the current trivia's question and answer choices.
Widget _buildQuestionView() {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
SizedBox(height: 16),
Text(
currentTrivia.question,
style: CupertinoTheme.of(context).textTheme.textStyle,
),
SizedBox(height: 32),
for (int i = 0; i < currentTrivia.answers.length; i++)
Padding(
padding: const EdgeInsets.all(8),
child: CupertinoButton(
color: CupertinoColors.activeBlue,
child: Text(
currentTrivia.answers[i],
textAlign: TextAlign.center,
),
onPressed: () => _processAnswer(i),
),
),
],
),
);
}
// 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),
child: Column(
children: [
Text(
status == PlayerStatus.wasCorrect
? 'That\'s right!'
: 'Sorry, that wasn\'t the right answer.',
style: CupertinoTheme.of(context).textTheme.textStyle,
),
SizedBox(height: 16),
CupertinoButton(
child: Text('Next Question'),
onPressed: () => setState(() {
triviaIndex++;
status = PlayerStatus.readyToAnswer;
}),
),
],
),
);
}
}

View File

@@ -1,179 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:veggieseasons/data/veggie.dart';
import 'package:veggieseasons/screens/details.dart';
import 'package:veggieseasons/styles.dart';
class FrostyBackground extends StatelessWidget {
const FrostyBackground({
this.color,
this.intensity = 25,
this.child,
});
final Color color;
final double intensity;
final Widget child;
@override
Widget build(BuildContext context) {
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: intensity, sigmaY: intensity),
child: DecoratedBox(
decoration: BoxDecoration(
color: color,
),
child: child,
),
),
);
}
}
/// A Card-like Widget that responds to tap events by animating changes to its
/// elevation and invoking an optional [onPressed] callback.
class PressableCard extends StatefulWidget {
const PressableCard({
@required this.child,
this.borderRadius = const BorderRadius.all(Radius.circular(5)),
this.upElevation = 2,
this.downElevation = 0,
this.shadowColor = CupertinoColors.black,
this.duration = const Duration(milliseconds: 100),
this.onPressed,
Key key,
}) : assert(child != null),
assert(borderRadius != null),
assert(upElevation != null),
assert(downElevation != null),
assert(shadowColor != null),
assert(duration != null),
super(key: key);
final VoidCallback onPressed;
final Widget child;
final BorderRadius borderRadius;
final double upElevation;
final double downElevation;
final Color shadowColor;
final Duration duration;
@override
_PressableCardState createState() => _PressableCardState();
}
class _PressableCardState extends State<PressableCard> {
bool cardIsDown = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() => cardIsDown = false);
if (widget.onPressed != null) {
widget.onPressed();
}
},
onTapDown: (details) => setState(() => cardIsDown = true),
onTapCancel: () => setState(() => cardIsDown = false),
child: AnimatedPhysicalModel(
elevation: cardIsDown ? widget.downElevation : widget.upElevation,
borderRadius: widget.borderRadius,
shape: BoxShape.rectangle,
shadowColor: widget.shadowColor,
duration: widget.duration,
color: CupertinoColors.lightBackgroundGray,
child: ClipRRect(
borderRadius: widget.borderRadius,
child: widget.child,
),
),
);
}
}
class VeggieCard extends StatelessWidget {
VeggieCard(this.veggie, this.isInSeason, this.isPreferredCategory);
/// Veggie to be displayed by the card.
final Veggie veggie;
/// If the veggie is in season, it's displayed more prominently and the
/// image is fully saturated. Otherwise, it's reduced and de-saturated.
final bool isInSeason;
/// Whether [veggie] falls into one of user's preferred [VeggieCategory]s
final bool isPreferredCategory;
Widget _buildDetails() {
return FrostyBackground(
color: Color(0x90ffffff),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
veggie.name,
style: Styles.cardTitleText,
),
Text(
veggie.shortDescription,
style: Styles.cardDescriptionText,
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return PressableCard(
onPressed: () {
Navigator.of(context).push<void>(CupertinoPageRoute(
builder: (context) => DetailsScreen(veggie.id),
fullscreenDialog: true,
));
},
child: Stack(
children: [
Semantics(
label: 'A card background featuring ${veggie.name}',
child: Container(
height: isInSeason ? 300 : 150,
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
colorFilter:
isInSeason ? null : Styles.desaturatedColorFilter,
image: AssetImage(
veggie.imageAssetPath,
),
),
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: _buildDetails(),
),
],
),
);
}
}

View File

@@ -1,112 +0,0 @@
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:veggieseasons/data/veggie.dart';
import 'package:veggieseasons/screens/details.dart';
import 'package:veggieseasons/styles.dart';
class ZoomClipAssetImage extends StatelessWidget {
const ZoomClipAssetImage(
{@required this.zoom,
this.height,
this.width,
@required this.imageAsset});
final double zoom;
final double height;
final double width;
final String imageAsset;
@override
Widget build(BuildContext context) {
return Container(
height: height,
width: width,
alignment: Alignment.center,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: OverflowBox(
maxHeight: height * zoom,
maxWidth: width * zoom,
child: Image.asset(
imageAsset,
fit: BoxFit.fill,
),
),
),
);
}
}
class VeggieHeadline extends StatelessWidget {
final Veggie veggie;
const VeggieHeadline(this.veggie);
List<Widget> _buildSeasonDots(List<Season> seasons) {
var widgets = <Widget>[];
for (var season in seasons) {
widgets.add(SizedBox(width: 4));
widgets.add(
Container(
height: 10,
width: 10,
decoration: BoxDecoration(
color: Styles.seasonColors[season],
borderRadius: BorderRadius.circular(5),
),
),
);
}
return widgets;
}
@override
Widget build(BuildContext context) {
final themeData = CupertinoTheme.of(context);
return GestureDetector(
onTap: () => Navigator.of(context).push<void>(CupertinoPageRoute(
builder: (context) => DetailsScreen(veggie.id),
fullscreenDialog: true,
)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ZoomClipAssetImage(
imageAsset: veggie.imageAssetPath,
zoom: 2.4,
height: 72,
width: 72,
),
SizedBox(width: 8),
Flexible(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
veggie.name,
style: Styles.headlineName(themeData),
),
..._buildSeasonDots(veggie.seasons),
],
),
Text(
veggie.shortDescription,
style: Styles.headlineDescription(themeData),
),
],
),
)
],
),
);
}
}