Fleshes out settings screen with groups, items, and navigation (#43)

pull/45/head
Andrew Brogdon 6 years ago committed by GitHub
parent 2128608386
commit 5d56599f7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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],
),

@ -68,14 +68,16 @@ class Preferences extends Model {
final prefs = await SharedPreferences.getInstance();
_desiredCalories = prefs.getInt(_caloriesKey) ?? 2000;
_preferredCategories.clear();
final names = prefs.getString(_preferredCategoriesKey) ?? '';
final names = prefs.getString(_preferredCategoriesKey);
if (names != null) {
for (final name in names.split(',')) {
final index = int.parse(name) ?? 0;
final index = int.tryParse(name) ?? -1;
if (VeggieCategory.values[index] != null) {
_preferredCategories.add(VeggieCategory.values[index]);
}
}
}
notifyListeners();
}

@ -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<Preferences>(context, rebuildOnChange: true);
final currentPrefs = model.preferredCategories;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Preferred Categories'),
previousPageTitle: 'Settings',
),
backgroundColor: Styles.scaffoldBackground,
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;
class SettingsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final model = ScopedModel.of<Preferences>(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<int>(
future: model.desiredCalories,
builder: (context, snapshot) {
final steps = <SettingsItem>[];
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<int>(
future: prefs.desiredCalories,
builder: (context, snapshot) {
return Row(
children: <Widget>[
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<Preferences>(context, rebuildOnChange: true);
return CupertinoPageScaffold(
child: Container(
color: Styles.scaffoldBackground,
child: CustomScrollView(
slivers: <Widget>[
CupertinoSliverNavigationBar(
largeTitle: Text('Settings'),
),
SliverSafeArea(
top: false,
sliver: SliverList(
delegate: SliverChildListDelegate(
<Widget>[
SettingsGroup(
items: [
_buildCaloriesItem(context, prefs),
_buildCategoriesItem(context, prefs),
],
),
],
),
),
),
],
),
),
);
}

@ -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<SettingsItem> items;
final Widget header;
final Widget footer;
@override
Widget build(BuildContext context) {
final dividedItems = <Widget>[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<Widget> 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,
),
);
}
}

@ -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<void> 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<StatefulWidget> createState() => new SettingsItemState();
}
class SettingsItemState extends State<SettingsItem> {
bool pressed = false;
@override
Widget build(BuildContext context) {
List<Widget> 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: <Widget>[
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,
),
),
),
);
}
}
Loading…
Cancel
Save