From 5d56599f7f980c1ae6bd7463d8437b1aadcb04a9 Mon Sep 17 00:00:00 2001 From: Andrew Brogdon Date: Thu, 3 Jan 2019 11:33:55 -0800 Subject: [PATCH] Fleshes out settings screen with groups, items, and navigation (#43) --- .../lib/data/local_veggie_provider.dart | 2 +- veggieseasons/lib/data/preferences.dart | 14 +- veggieseasons/lib/screens/settings.dart | 197 +++++++++++++++++- veggieseasons/lib/widgets/settings_group.dart | 132 ++++++++++++ veggieseasons/lib/widgets/settings_item.dart | 179 ++++++++++++++++ 5 files changed, 513 insertions(+), 11 deletions(-) create mode 100644 veggieseasons/lib/widgets/settings_group.dart create mode 100644 veggieseasons/lib/widgets/settings_item.dart diff --git a/veggieseasons/lib/data/local_veggie_provider.dart b/veggieseasons/lib/data/local_veggie_provider.dart index 302994871..c50f878d5 100644 --- a/veggieseasons/lib/data/local_veggie_provider.dart +++ b/veggieseasons/lib/data/local_veggie_provider.dart @@ -228,7 +228,7 @@ class LocalVeggieProvider { name: 'Squash', imageAssetPath: 'assets/images/squash.jpg', category: VeggieCategory.gourd, - shortDescription: 'Bigger and heartier than summer squashes.', + shortDescription: 'Just slather them in butter and pop \'em in the oven.', accentColor: Color(0x40dbb721), seasons: [Season.winter, Season.autumn], ), diff --git a/veggieseasons/lib/data/preferences.dart b/veggieseasons/lib/data/preferences.dart index 9f20828e0..6b8c5c0c9 100644 --- a/veggieseasons/lib/data/preferences.dart +++ b/veggieseasons/lib/data/preferences.dart @@ -68,12 +68,14 @@ class Preferences extends Model { final prefs = await SharedPreferences.getInstance(); _desiredCalories = prefs.getInt(_caloriesKey) ?? 2000; _preferredCategories.clear(); - final names = prefs.getString(_preferredCategoriesKey) ?? ''; - - for (final name in names.split(',')) { - final index = int.parse(name) ?? 0; - if (VeggieCategory.values[index] != null) { - _preferredCategories.add(VeggieCategory.values[index]); + final names = prefs.getString(_preferredCategoriesKey); + + if (names != null) { + for (final name in names.split(',')) { + final index = int.tryParse(name) ?? -1; + if (VeggieCategory.values[index] != null) { + _preferredCategories.add(VeggieCategory.values[index]); + } } } diff --git a/veggieseasons/lib/screens/settings.dart b/veggieseasons/lib/screens/settings.dart index b0fdbbb1f..a0c864b01 100644 --- a/veggieseasons/lib/screens/settings.dart +++ b/veggieseasons/lib/screens/settings.dart @@ -3,19 +3,208 @@ // found in the LICENSE file. import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:scoped_model/scoped_model.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 = ScopedModel.of(context, rebuildOnChange: true); + final currentPrefs = model.preferredCategories; + + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Text('Preferred Categories'), + previousPageTitle: 'Settings', + ), + backgroundColor: Styles.scaffoldBackground, + child: FutureBuilder>( + future: currentPrefs, + builder: (context, snapshot) { + final items = []; + + 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; -class SettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final model = ScopedModel.of(context, rebuildOnChange: true); + return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - middle: Text('Settings'), + previousPageTitle: 'Settings', ), backgroundColor: Styles.scaffoldBackground, - child: Center( - child: Text('Not yet implemented.'), + child: ListView( + children: [ + FutureBuilder( + future: model.desiredCalories, + builder: (context, snapshot) { + final steps = []; + + for (int 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 { + Widget _buildCaloriesItem(BuildContext context, Preferences prefs) { + return SettingsItem( + label: 'Calorie Target', + icon: SettingsIcon( + backgroundColor: Styles.iconBlue, + icon: Styles.calorieIcon, + ), + content: FutureBuilder( + future: prefs.desiredCalories, + builder: (context, snapshot) { + return Row( + children: [ + Text(snapshot.data?.toString() ?? ''), + SizedBox(width: 8.0), + SettingsNavigationIndicator(), + ], + ); + }, + ), + onPress: () { + Navigator.of(context).push( + CupertinoPageRoute( + builder: (context) => CalorieSettingsScreen(), + title: 'Calorie Target', + ), + ); + }, + ); + } + + Widget _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( + CupertinoPageRoute( + builder: (context) => VeggieCategorySettingsScreen(), + title: 'Preferred Categories', + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final prefs = ScopedModel.of(context, rebuildOnChange: true); + + return CupertinoPageScaffold( + child: Container( + color: Styles.scaffoldBackground, + child: CustomScrollView( + slivers: [ + CupertinoSliverNavigationBar( + largeTitle: Text('Settings'), + ), + SliverSafeArea( + top: false, + sliver: SliverList( + delegate: SliverChildListDelegate( + [ + SettingsGroup( + items: [ + _buildCaloriesItem(context, prefs), + _buildCategoriesItem(context, prefs), + ], + ), + ], + ), + ), + ), + ], + ), ), ); } diff --git a/veggieseasons/lib/widgets/settings_group.dart b/veggieseasons/lib/widgets/settings_group.dart new file mode 100644 index 000000000..f73cc17b6 --- /dev/null +++ b/veggieseasons/lib/widgets/settings_group.dart @@ -0,0 +1,132 @@ +// 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.0, + right: 15.0, + bottom: 6.0, + ), + 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.0, + right: 15.0, + top: 7.5, + ), + child: Text( + title, + style: TextStyle( + color: Styles.settingsGroupSubtitle, + fontSize: 13.0, + letterSpacing: -0.08, + ), + ), + ); + } +} + +class SettingsGroup extends StatelessWidget { + SettingsGroup({ + @required this.items, + this.header, + this.footer, + }) : assert(items != null), + assert(items.length > 0); + + final List items; + final Widget header; + final Widget footer; + + @override + Widget build(BuildContext context) { + final dividedItems = [items[0]]; + + for (int i = 1; i < items.length; i++) { + dividedItems.add(Container( + color: Styles.settingsLineation, + height: 0.3, + )); + dividedItems.add(items[i]); + } + + final List columnChildren = []; + + if (header != null) { + columnChildren.add(header); + } + + columnChildren.add( + Container( + decoration: BoxDecoration( + color: CupertinoColors.white, + border: Border( + top: const BorderSide( + color: Styles.settingsLineation, + width: 0.0, + ), + bottom: const BorderSide( + color: Styles.settingsLineation, + width: 0.0, + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: dividedItems, + ), + ), + ); + + if (footer != null) { + columnChildren.add(footer); + } + + return Padding( + padding: EdgeInsets.only( + top: header == null ? 35.0 : 22.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: columnChildren, + ), + ); + } +} diff --git a/veggieseasons/lib/widgets/settings_item.dart b/veggieseasons/lib/widgets/settings_item.dart new file mode 100644 index 000000000..4ce9b3a93 --- /dev/null +++ b/veggieseasons/lib/widgets/settings_item.dart @@ -0,0 +1,179 @@ +// 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 FutureOr SettingsItemCallback(); + +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.0, + ); + } +} + +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.0), + color: backgroundColor, + ), + child: Center( + child: Icon( + icon, + color: foregroundColor, + size: 20.0, + ), + ), + ); + } +} + +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 createState() => new SettingsItemState(); +} + +class SettingsItemState extends State { + bool pressed = false; + + @override + Widget build(BuildContext context) { + List rowChildren = []; + + if (widget.icon != null) { + rowChildren.add( + Padding( + padding: const EdgeInsets.only( + left: 15.0, + bottom: 2.0, + ), + child: SizedBox( + height: 29.0, + width: 29.0, + child: widget.icon, + ), + ), + ); + } + + Widget titleSection; + + if (widget.subtitle == null) { + titleSection = Padding( + padding: EdgeInsets.only(top: 1.5), + child: Text(widget.label), + ); + } else { + titleSection = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 8.5), + Text(widget.label), + SizedBox(height: 4.0), + Text( + widget.subtitle, + style: TextStyle( + fontSize: 12.0, + letterSpacing: -0.2, + ), + ) + ], + ); + } + + rowChildren.add( + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 15.0, + ), + child: titleSection, + ), + ), + ); + + rowChildren.add( + Padding( + padding: const EdgeInsets.only(right: 11.0), + child: widget.content ?? Container(), + ), + ); + + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + color: pressed ? Styles.settingsItemPressed : Styles.transparentColor, + 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.0 : 57.0, + child: Row( + children: rowChildren, + ), + ), + ), + ); + } +}