Commit 5e7c5045 authored by Eric Seidel's avatar Eric Seidel

Remove examples/fitness

We originally wrote examples/fitness to be an app which all
members of the Flutter team could carry on their phones and
use every day.  It served us well for testing of Text input,
Keyboards and writing/reading JSON.  It's never actually become
a real carry app, so lets just let it go for now.

@abarth @collinjackson
parent ba7c9928
.DS_Store
.atom/
.idea
.packages
.pub/
build/
ios/.generated/
packages
pubspec.lock
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2015 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.domokit.fitness" android:versionCode="4" android:versionName="0.0.4">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="21" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- for GCM -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<!-- Supposedly this permission prevents other apps from receiving our
messages, but it doesn't seem to have any effect. -->
<permission android:name="org.domokit.fitness.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="org.domokit.fitness.permission.C2D_MESSAGE" />
<!-- end for GCM -->
<application android:icon="@mipmap/ic_launcher" android:label="Fitness" android:name="org.domokit.sky.shell.SkyApplication">
<activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize" android:hardwareAccelerated="true" android:launchMode="singleTask" android:name="org.domokit.sky.shell.SkyActivity" android:theme="@android:style/Theme.Black.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name="org.domokit.sky.shell.UpdateService"
android:exported="false"
android:process=":remote"/>
<!-- for GCM -->
<receiver
android:name="com.google.android.gms.gcm.GcmReceiver"
android:exported="true"
android:permission="com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<category android:name="org.domokit.sky.shell" />
</intent-filter>
</receiver>
<service
android:name="org.domokit.gcm.GcmListenerService"
android:exported="false" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter>
</service>
<service
android:name="org.domokit.gcm.InstanceIDListenerService"
android:exported="false">
<intent-filter>
<action android:name="com.google.android.gms.iid.InstanceID"/>
</intent-filter>
</service>
<service
android:name="org.domokit.gcm.RegistrationIntentService"
android:exported="false">
</service>
</application>
</manifest>
Icon image comes from:
https://openclipart.org/detail/22309/apple-icon
and is public domain.
Icon resources were generated using:
http://romannurik.github.io/AndroidAssetStudio/icons-launcher.html
with settings:
http://romannurik.github.io/AndroidAssetStudio/icons-launcher.html#foreground.type=image&foreground.space.trim=1&foreground.space.pad=0&foreColor=607d8b%2C0&crop=0&backgroundShape=none&backColor=ffffff%2C100&effects=none
which produces art under CC 3.0:
http://creativecommons.org/licenses/by/3.0/
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2015 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.domokit.mine_digger">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="21" />
<uses-permission android:name="android.permission.INTERNET"/>
<application android:name="org.domokit.sky.shell.SkyApplication" android:label="Mine Digger">
<activity android:name="org.domokit.sky.shell.SkyActivity"
android:launchMode="singleTask"
android:theme="@android:style/Theme.Black.NoTitleBar"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize"
android:hardwareAccelerated="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
Still barely works
Fixed crash when entering an invalid number
Made date list look less-awful.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
name: fitness
version: 0.0.1
update-url: http://localhost:9888/
material-design-icons:
- name: action/assessment
- name: action/help
- name: action/settings
- name: action/view_list
- name: av/stop
- name: content/add
- name: maps/directions_run
- name: navigation/arrow_back
- name: navigation/close
- name: navigation/menu
- name: navigation/more_vert
{
"images" : [
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-Small@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-Small@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-Small-40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-Small-40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-60@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-Small.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-Small@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-Small-40.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-Small-40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-76.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-83.5@2x.png",
"scale" : "2x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "icon_16x16.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "icon_16x16@2x.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "icon_32x32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "icon_32x32@2x.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "icon_128x128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "icon_128x128@2x.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "icon_256x256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "icon_256x256@2x.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "icon_512x512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "icon_512x512@2x.png",
"scale" : "2x"
}
]
}
\ No newline at end of file
This diff was suppressed by a .gitattributes entry.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>Runner</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.example.fitness</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Fitness</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="9531" systemVersion="15C50" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9529"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
<viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Forked from https://github.com/dart-lang/sdk/blob/master/samples-dev/swarm/swarm_ui_lib/util/DateUtils.dart
class DateUtils {
static const WEEKDAYS = const ['Monday', 'Tuesday', 'Wednesday', 'Thursday',
'Friday', 'Saturday', 'Sunday'];
static const YESTERDAY = 'Yesterday';
static const MS_IN_WEEK = DateTime.DAYS_PER_WEEK * Duration.MILLISECONDS_PER_DAY;
// TODO(jmesserly): locale specific date format
static String _twoDigits(int n) {
if (n >= 10)
return '$n';
return '0$n';
}
/// Formats a time in H:MM A format
static String toHourMinutesString(Duration duration) {
assert(duration.inDays == 0);
int hours = duration.inHours;
String a;
if (hours >= 12) {
a = 'pm';
if (hours != 12)
hours -= 12;
} else {
a = 'am';
if (hours == 0)
hours += 12;
}
String twoDigits(int n) {
if (n >= 10)
return '$n';
return '0$n';
}
String mm = twoDigits(duration.inMinutes.remainder(Duration.MINUTES_PER_HOUR));
return '$hours:$mm $a';
}
/// A date/time formatter that takes into account the current date/time:
/// - if it's from today, just show the time
/// - if it's from yesterday, just show 'Yesterday'
/// - if it's from the same week, just show the weekday
/// - otherwise, show just the date
static String toRecentTimeString(DateTime then) {
bool datesAreEqual(DateTime d1, DateTime d2) {
return (d1.year == d2.year) &&
(d1.month == d2.month) &&
(d1.day == d2.day);
}
final now = new DateTime.now();
if (datesAreEqual(then, now)) {
return toHourMinutesString(new Duration(
days: 0,
hours: then.hour,
minutes: then.minute,
seconds: then.second,
milliseconds: then.millisecond)
);
}
final today = new DateTime(now.year, now.month, now.day, 0, 0, 0, 0);
Duration delta = today.difference(then);
if (delta.inMilliseconds < Duration.MILLISECONDS_PER_DAY) {
return YESTERDAY;
} else if (delta.inMilliseconds < MS_IN_WEEK) {
return WEEKDAYS[then.weekday];
} else {
String twoDigitMonth = _twoDigits(then.month);
String twoDigitDay = _twoDigits(then.day);
return '${then.year}-$twoDigitMonth-$twoDigitDay';
}
}
static String toDateString(DateTime then) {
// TODO(jmesserly): locale specific date format
String twoDigitMonth = _twoDigits(then.month);
String twoDigitDay = _twoDigits(then.day);
return '${then.year}-$twoDigitMonth-$twoDigitDay';
}
}
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
part of fitness;
class FitnessItemList extends StatelessComponent {
FitnessItemList({ Key key, this.items, this.onDismissed }) : super(key: key) {
assert(items != null);
assert(onDismissed != null);
}
final List<FitnessItem> items;
final FitnessItemHandler onDismissed;
Widget build(BuildContext context) {
return new ScrollableList(
padding: const EdgeDims.all(4.0),
itemExtent: kFitnessItemHeight,
children: items.map((FitnessItem item) => item.toRow(onDismissed: onDismissed))
);
}
}
class DialogMenuItem extends StatelessComponent {
DialogMenuItem(this.children, { Key key, this.onPressed }) : super(key: key);
List<Widget> children;
Function onPressed;
Widget build(BuildContext context) {
return new Container(
height: 48.0,
child: new InkWell(
onTap: onPressed,
child: new Padding(
padding: const EdgeDims.symmetric(horizontal: 16.0),
child: new Row(children: children)
)
)
);
}
}
class FeedFragment extends StatefulComponent {
FeedFragment({ this.userData, this.onItemCreated, this.onItemDeleted });
final UserData userData;
final FitnessItemHandler onItemCreated;
final FitnessItemHandler onItemDeleted;
FeedFragmentState createState() => new FeedFragmentState();
}
class FeedFragmentState extends State<FeedFragment> {
FitnessMode _fitnessMode = FitnessMode.feed;
void _handleFitnessModeChange(FitnessMode value) {
setState(() {
_fitnessMode = value;
});
Navigator.pop(context);
}
Widget _buildDrawer() {
return new Drawer(
child: new Block(children: <Widget>[
new DrawerHeader(child: new Text('Fitness')),
new DrawerItem(
icon: 'action/view_list',
onPressed: () => _handleFitnessModeChange(FitnessMode.feed),
selected: _fitnessMode == FitnessMode.feed,
child: new Text('Feed')),
new DrawerItem(
icon: 'action/assessment',
onPressed: () => _handleFitnessModeChange(FitnessMode.chart),
selected: _fitnessMode == FitnessMode.chart,
child: new Text('Chart')),
new DrawerDivider(),
new DrawerItem(
icon: 'action/settings',
onPressed: _handleShowSettings,
child: new Text('Settings')),
new DrawerItem(
icon: 'action/help',
child: new Text('Help & Feedback'))
])
);
}
void _handleShowSettings() {
Navigator.popAndPushNamed(context, '/settings');
}
// TODO(jackson): We should be localizing
String get fitnessModeTitle {
switch(_fitnessMode) {
case FitnessMode.feed: return "Feed";
case FitnessMode.chart: return "Chart";
}
}
Widget buildToolBar() {
return new ToolBar(
center: new Text(fitnessModeTitle)
);
}
void _handleItemDismissed(FitnessItem item) {
config.onItemDeleted(item);
Scaffold.of(context).showSnackBar(new SnackBar(
content: new Text("Item deleted."),
action: new SnackBarAction(
label: "UNDO",
onPressed: () {
config.onItemCreated(item);
}
)
));
}
Widget buildChart() {
double startX;
double endX;
double startY;
double endY;
List<Point> dataSet = new List<Point>();
for (FitnessItem item in config.userData.items) {
if (item is Measurement) {
double x = item.when.millisecondsSinceEpoch.toDouble();
double y = item.weight;
if (startX == null || startX > x)
startX = x;
if (endX == null || endX < x)
endX = x;
if (startY == null || startY > y)
startY = y;
if (endY == null || endY < y)
endY = y;
dataSet.add(new Point(x, y));
}
}
if (config.userData.goalWeight != null && config.userData.goalWeight > 0.0) {
startY = math.min(startY, config.userData.goalWeight);
endY = math.max(endY, config.userData.goalWeight);
}
playfair.ChartData data = new playfair.ChartData(
startX: startX,
startY: startY,
endX: endX,
endY: endY,
dataSet: dataSet,
numHorizontalGridlines: 5,
roundToPlaces: 1,
indicatorLine: config.userData.goalWeight,
indicatorText: "GOAL WEIGHT"
);
return new playfair.Chart(data: data);
}
Widget buildBody() {
TextStyle style = Theme.of(context).text.title;
if (config.userData == null)
return new Container();
if (config.userData.items.length == 0) {
return new Row(
children: <Widget>[new Text("No data yet.\nAdd some!", style: style)],
justifyContent: FlexJustifyContent.center
);
}
switch (_fitnessMode) {
case FitnessMode.feed:
return new FitnessItemList(
items: config.userData.items.reversed.toList(),
onDismissed: _handleItemDismissed
);
case FitnessMode.chart:
return new Container(
padding: const EdgeDims.all(20.0),
child: buildChart()
);
}
}
void _handleActionButtonPressed() {
showDialog(context: context, child: new AddItemDialog()).then((routeName) {
if (routeName != null)
Navigator.pushNamed(context, routeName);
});
}
Widget buildFloatingActionButton() {
switch (_fitnessMode) {
case FitnessMode.feed:
return new FloatingActionButton(
child: new Icon(icon: 'content/add'),
onPressed: _handleActionButtonPressed
);
case FitnessMode.chart:
return null;
}
}
Widget build(BuildContext context) {
return new Scaffold(
toolBar: buildToolBar(),
body: buildBody(),
floatingActionButton: buildFloatingActionButton(),
drawer: _buildDrawer()
);
}
}
class AddItemDialog extends StatefulComponent {
AddItemDialogState createState() => new AddItemDialogState();
}
class AddItemDialogState extends State<AddItemDialog> {
// TODO(jackson): Internationalize
static final Map<String, String> _labels = <String, String>{
'/measurements/new': 'Measure',
'/meals/new': 'Eat',
};
String _addItemRoute = _labels.keys.first;
void _handleAddItemRouteChanged(String routeName) {
setState(() {
_addItemRoute = routeName;
});
}
Widget build(BuildContext context) {
List<Widget> menuItems = <Widget>[];
for (String routeName in _labels.keys) {
menuItems.add(new DialogMenuItem(<Widget>[
new Flexible(child: new Text(_labels[routeName])),
new Radio<String>(value: routeName, groupValue: _addItemRoute, onChanged: _handleAddItemRouteChanged),
], onPressed: () => _handleAddItemRouteChanged(routeName)));
}
return new Dialog(
title: new Text("What are you doing?"),
content: new Block(children: menuItems),
actions: <Widget>[
new FlatButton(
child: new Text('CANCEL'),
onPressed: () {
Navigator.pop(context);
}
),
new FlatButton(
child: new Text('ADD'),
onPressed: () {
Navigator.pop(context, _addItemRoute);
}
),
]
);
}
}
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
part of fitness;
typedef void FitnessItemHandler(FitnessItem item);
// TODO(eseidel): This should be a constant on a SingleLineTile class
// https://www.google.com/design/spec/components/lists.html#lists-specs
const double kFitnessItemHeight = 48.0;
abstract class FitnessItem {
FitnessItem.fromJson(Map json) : when = DateTime.parse(json['when']);
FitnessItem({ this.when }) {
assert(when != null);
}
final DateTime when;
Map toJson() => { 'when' : when.toIso8601String() };
// TODO(jackson): Internationalize
String get displayDate => DateUtils.toDateString(when);
FitnessItemRow toRow({ FitnessItemHandler onDismissed });
}
abstract class FitnessItemRow extends StatelessComponent {
FitnessItemRow({ FitnessItem item, this.onDismissed })
: this.item = item,
super(key: new ValueKey<DateTime>(item.when)) {
assert(onDismissed != null);
}
final FitnessItem item;
final FitnessItemHandler onDismissed;
Widget buildContent(BuildContext context);
Widget build(BuildContext context) {
return new Dismissable(
onDismissed: () => onDismissed(item),
child: new Container(
height: kFitnessItemHeight,
// TODO(eseidel): Padding top should be 16px for a single-line tile:
// https://www.google.com/design/spec/components/lists.html#lists-specs
padding: const EdgeDims.all(10.0),
// TODO(eseidel): This line should be drawn by the list as it should
// stay put even when the tile is dismissed!
decoration: new BoxDecoration(
border: new Border(
bottom: new BorderSide(color: Theme.of(context).dividerColor)
)
),
child: buildContent(context)
)
);
}
}
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
part of fitness;
enum FitnessMode { feed, chart }
enum BackupMode { enabled, disabled }
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library fitness;
import 'package:playfair/playfair.dart' as playfair;
import 'package:flutter/material.dart';
import 'user_data.dart';
import 'date_utils.dart';
import 'dart:async';
import 'dart:math' as math;
part 'feed.dart';
part 'fitness_item.dart';
part 'fitness_types.dart';
part 'meal.dart';
part 'measurement.dart';
part 'settings.dart';
abstract class UserData {
BackupMode get backupMode;
double get goalWeight;
List<FitnessItem> get items;
}
class UserDataImpl extends UserData {
UserDataImpl();
List<FitnessItem> _items = <FitnessItem>[];
BackupMode _backupMode;
BackupMode get backupMode => _backupMode;
void set backupMode(BackupMode value) {
_backupMode = value;
}
double _goalWeight;
double get goalWeight => _goalWeight;
void set goalWeight(double value) {
_goalWeight = value;
}
List<FitnessItem> get items => _items;
void sort() {
_items.sort((FitnessItem a, FitnessItem b) => a.when.compareTo(b.when));
}
void add(FitnessItem item) {
_items.add(item);
sort();
}
void remove(FitnessItem item) {
_items.remove(item);
}
Future save() => saveFitnessData(this);
UserDataImpl.fromJson(Map json) {
json['items'].forEach((item) {
_items.add(new Measurement.fromJson(item));
});
try {
_backupMode = BackupMode.values.firstWhere((BackupMode mode) {
return mode.toString() == json['backupMode'];
});
} catch(e) {
print("Failed to load backup mode: $e");
}
_goalWeight = json['goalWeight'];
}
Map toJson() {
Map json = new Map();
json['items'] = _items.map((FitnessItem item) => item.toJson()).toList();
json['backupMode'] = _backupMode.toString();
json['goalWeight'] = _goalWeight;
return json;
}
}
class FitnessApp extends StatefulComponent {
FitnessAppState createState() => new FitnessAppState();
}
class FitnessAppState extends State<FitnessApp> {
UserDataImpl _userData;
void initState() {
super.initState();
loadFitnessData().then((UserData data) {
setState(() => _userData = data);
}).catchError((e) {
print("Failed to load data: $e");
setState(() => _userData = new UserDataImpl());
});
}
void _handleItemCreated(FitnessItem item) {
setState(() {
_userData.add(item);
_userData.save();
});
}
void _handleItemDeleted(FitnessItem item) {
setState(() {
_userData.remove(item);
_userData.save();
});
}
void settingsUpdater({ BackupMode backup, double goalWeight }) {
setState(() {
if (backup != null)
_userData.backupMode = backup;
if (goalWeight != null)
_userData.goalWeight = goalWeight;
_userData.save();
});
}
Widget build(BuildContext context) {
return new MaterialApp(
theme: new ThemeData(
brightness: ThemeBrightness.light,
primarySwatch: Colors.indigo,
accentColor: Colors.pinkAccent[200]
),
title: 'Fitness',
routes: <String, RouteBuilder>{
'/': (RouteArguments args) {
return new FeedFragment(
userData: _userData,
onItemCreated: _handleItemCreated,
onItemDeleted: _handleItemDeleted
);
},
'/meals/new': (RouteArguments args) {
return new MealFragment(
onCreated: _handleItemCreated
);
},
'/measurements/new': (RouteArguments args) {
return new MeasurementFragment(
onCreated: _handleItemCreated
);
},
'/settings': (RouteArguments args) {
return new SettingsFragment(
userData: _userData,
updater: settingsUpdater
);
}
}
);
}
}
main() {
runApp(new FitnessApp());
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
part of fitness;
class Meal extends FitnessItem {
Meal({ DateTime when, this.description }) : super(when: when);
final String description;
FitnessItemRow toRow({ FitnessItemHandler onDismissed }) {
return new MealRow(meal: this, onDismissed: onDismissed);
}
}
class MealRow extends FitnessItemRow {
MealRow({ Meal meal, FitnessItemHandler onDismissed })
: super(item: meal, onDismissed: onDismissed);
Widget buildContent(BuildContext context) {
Meal meal = item;
List<Widget> children = <Widget>[
new Flexible(
child: new Text(
meal.description,
style: const TextStyle(textAlign: TextAlign.right)
)
),
new Flexible(
child: new Text(
meal.displayDate,
style: Theme.of(context).text.caption.copyWith(textAlign: TextAlign.right)
)
)
];
return new Row(
children: children,
alignItems: FlexAlignItems.baseline,
textBaseline: DefaultTextStyle.of(context).textBaseline
);
}
}
class MealFragment extends StatefulComponent {
MealFragment({ this.onCreated });
FitnessItemHandler onCreated;
MealFragmentState createState() => new MealFragmentState();
}
class MealFragmentState extends State<MealFragment> {
InputValue _description = InputValue.empty;
void _handleSave() {
config.onCreated(new Meal(when: new DateTime.now(), description: _description.text));
Navigator.pop(context);
}
Widget buildToolBar() {
return new ToolBar(
left: new IconButton(
icon: "navigation/close",
onPressed: () => Navigator.pop(context)
),
center: new Text('New Meal'),
right: <Widget>[
// TODO(abarth): Should this be a FlatButton?
new InkWell(
onTap: _handleSave,
child: new Text('SAVE')
)
]
);
}
void _handleDescriptionChanged(InputValue description) {
setState(() {
_description = description;
});
}
static final GlobalKey descriptionKey = new GlobalKey();
Widget buildBody() {
Meal meal = new Meal(when: new DateTime.now());
return new Block(children: <Widget>[
new Text(meal.displayDate),
new Input(
key: descriptionKey,
autofocus: true,
hintText: 'Describe meal',
onChanged: _handleDescriptionChanged
),
],
padding: const EdgeDims.all(20.0)
);
}
Widget build(BuildContext context) {
return new Scaffold(
toolBar: buildToolBar(),
body: buildBody()
);
}
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
part of fitness;
class Measurement extends FitnessItem {
Measurement({ DateTime when, this.weight }) : super(when: when);
Measurement.fromJson(Map json) : weight = json['weight'], super.fromJson(json);
final double weight;
// TODO(jackson): Internationalize
String get displayWeight => "${weight.toStringAsFixed(1)} lbs";
@override
Map toJson() {
Map json = super.toJson();
json['weight'] = weight;
json['type'] = runtimeType.toString();
return json;
}
FitnessItemRow toRow({ FitnessItemHandler onDismissed }) {
return new MeasurementRow(measurement: this, onDismissed: onDismissed);
}
}
class MeasurementRow extends FitnessItemRow {
MeasurementRow({ Measurement measurement, FitnessItemHandler onDismissed })
: super(item: measurement, onDismissed: onDismissed);
Widget buildContent(BuildContext context) {
Measurement measurement = item;
List<Widget> children = <Widget>[
new Flexible(
child: new Text(
measurement.displayWeight,
style: Theme.of(context).text.subhead
)
),
new Flexible(
child: new Text(
measurement.displayDate,
style: Theme.of(context).text.caption.copyWith(textAlign: TextAlign.right)
)
)
];
return new Row(
children: children,
alignItems: FlexAlignItems.baseline,
textBaseline: DefaultTextStyle.of(context).textBaseline
);
}
}
class MeasurementFragment extends StatefulComponent {
MeasurementFragment({ this.onCreated });
final FitnessItemHandler onCreated;
MeasurementFragmentState createState() => new MeasurementFragmentState();
}
class MeasurementFragmentState extends State<MeasurementFragment> {
InputValue _weight = InputValue.empty;
DateTime _when = new DateTime.now();
void _handleSave() {
double parsedWeight;
try {
parsedWeight = double.parse(_weight.text);
} on FormatException catch(e) {
print("Exception $e");
Scaffold.of(context).showSnackBar(new SnackBar(
content: new Text('Save failed')
));
}
config.onCreated(new Measurement(when: _when, weight: parsedWeight));
Navigator.pop(context);
}
Widget buildToolBar() {
return new ToolBar(
left: new IconButton(
icon: "navigation/close",
onPressed: () => Navigator.pop(context)
),
center: new Text('New Measurement'),
right: <Widget>[
// TODO(abarth): Should this be a FlatButton?
new InkWell(
onTap: _handleSave,
child: new Text('SAVE')
)
]
);
}
void _handleWeightChanged(InputValue weight) {
setState(() {
_weight = weight;
});
}
static final GlobalKey weightKey = new GlobalKey();
Future _handleDatePressed() async {
DateTime value = await showDatePicker(
context: context,
initialDate: _when,
firstDate: new DateTime(2015, 8),
lastDate: new DateTime(2101)
);
if (value != _when) {
setState(() {
_when = value;
});
}
}
Widget buildBody(BuildContext context) {
Measurement measurement = new Measurement(when: _when);
// TODO(jackson): Revisit the layout of this pane to be more maintainable
return new Container(
padding: const EdgeDims.all(20.0),
child: new Column(
children: <Widget>[
new GestureDetector(
onTap: _handleDatePressed,
child: new Container(
height: 50.0,
child: new Column(
children: <Widget>[
new Text('Measurement Date'),
new Text(measurement.displayDate, style: Theme.of(context).text.caption),
],
alignItems: FlexAlignItems.start
)
)
),
new Input(
key: weightKey,
autofocus: true,
hintText: 'Enter weight',
keyboardType: KeyboardType.number,
onChanged: _handleWeightChanged
),
],
alignItems: FlexAlignItems.stretch
)
);
}
Widget build(BuildContext context) {
return new Scaffold(
toolBar: buildToolBar(),
body: buildBody(context)
);
}
}
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
part of fitness;
class _SettingsDialog extends StatefulComponent {
_SettingsDialogState createState() => new _SettingsDialogState();
}
class _SettingsDialogState extends State<_SettingsDialog> {
final GlobalKey weightGoalKey = new GlobalKey();
InputValue _goalWeight = InputValue.empty;
void _handleGoalWeightChanged(InputValue goalWeight) {
setState(() {
_goalWeight = goalWeight;
});
}
void _handleGoalWeightSubmitted(InputValue goalWeight) {
_goalWeight = goalWeight;
_handleSavePressed();
}
void _handleSavePressed() {
double goalWeight;
try {
goalWeight = double.parse(_goalWeight.text);
} on FormatException {
goalWeight = 0.0;
}
Navigator.pop(context, goalWeight);
}
Widget build(BuildContext context) {
return new Dialog(
title: new Text("Goal Weight"),
content: new Input(
key: weightGoalKey,
value: _goalWeight,
autofocus: true,
hintText: 'Goal weight in lbs',
keyboardType: KeyboardType.number,
onChanged: _handleGoalWeightChanged,
onSubmitted: _handleGoalWeightSubmitted
),
actions: <Widget>[
new FlatButton(
child: new Text('CANCEL'),
onPressed: () {
Navigator.pop(context);
}
),
new FlatButton(
child: new Text('SAVE'),
onPressed: _handleSavePressed
),
]
);
}
}
typedef void SettingsUpdater({
BackupMode backup,
double goalWeight
});
class SettingsFragment extends StatefulComponent {
SettingsFragment({ this.userData, this.updater });
final UserData userData;
final SettingsUpdater updater;
SettingsFragmentState createState() => new SettingsFragmentState();
}
class SettingsFragmentState extends State<SettingsFragment> {
void _handleBackupChanged(bool value) {
assert(config.updater != null);
config.updater(backup: value ? BackupMode.enabled : BackupMode.disabled);
}
Widget buildToolBar() {
return new ToolBar(
center: new Text('Settings')
);
}
String get goalWeightText {
if (config.userData.goalWeight == null || config.userData.goalWeight == 0.0)
return "None";
return "${config.userData.goalWeight}";
}
Future _handleGoalWeightPressed() async {
double goalWeight = await showDialog(
context: context,
child: new _SettingsDialog()
);
config.updater(goalWeight: goalWeight);
}
Widget buildSettingsPane(BuildContext context) {
return new Block(children: <Widget>[
new DrawerItem(
onPressed: () { _handleBackupChanged(!(config.userData.backupMode == BackupMode.enabled)); },
child: new Row(
children: <Widget>[
new Flexible(child: new Text('Back up data to the cloud')),
new Switch(value: config.userData.backupMode == BackupMode.enabled, onChanged: _handleBackupChanged),
]
)
),
new DrawerItem(
onPressed: () => _handleGoalWeightPressed(),
child: new Column(
children: <Widget>[
new Text('Goal Weight'),
new Text(goalWeightText, style: Theme.of(context).text.caption),
],
alignItems: FlexAlignItems.start
)
),
],
padding: const EdgeDims.symmetric(vertical: 20.0)
);
}
Widget build(BuildContext context) {
return new Scaffold(
toolBar: buildToolBar(),
body: buildSettingsPane(context)
);
}
}
// Copyright 2015 The Chromium Authors. 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:convert';
import 'dart:io';
import 'dart:async';
import 'package:path/path.dart' as path;
import 'main.dart';
import 'package:flutter/services.dart';
String cachedDataFilePath = null;
Future<String> dataFilePath() async {
if (cachedDataFilePath == null) {
String dataDir = await getFilesDir();
cachedDataFilePath = path.join(dataDir, 'data.json');
}
return cachedDataFilePath;
}
Future<UserData> loadFitnessData() async {
String dataPath = await dataFilePath();
print("Loading from $dataPath");
JsonDecoder decoder = new JsonDecoder();
Map data = await decoder.convert(await new File(dataPath).readAsString());
return new UserDataImpl.fromJson(data);
}
// Intentionally synchronous for execution just before shutdown.
Future saveFitnessData(UserDataImpl data) async {
String dataPath = await dataFilePath();
print("Saving to $dataPath");
JsonEncoder encoder = new JsonEncoder();
String contents = await encoder.convert(data);
File dataFile = await new File(dataPath).writeAsString(contents);
print("Success! $dataFile");
}
name: fitness
dependencies:
path: ^1.3.6
flutter:
path: ../../packages/flutter
playfair:
path: ../../packages/playfair
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment