Commit 8b1c01ec authored by Eric Seidel's avatar Eric Seidel

Merge pull request #2185 from eseidelGoogle/delete_fitness

Remove examples/fitness
parents ba7c9928 5e7c5045
.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