diff --git a/examples/fitness/lib/date_utils.dart b/examples/fitness/lib/date_utils.dart new file mode 100644 index 00000000000..570203e648c --- /dev/null +++ b/examples/fitness/lib/date_utils.dart @@ -0,0 +1,81 @@ +// 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; + + /** 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 { + // TODO(jmesserly): locale specific date format + String twoDigits(int n) { + if (n >= 10) return "${n}"; + return "0${n}"; + } + String twoDigitMonth = twoDigits(then.month); + String twoDigitDay = twoDigits(then.day); + return "${then.year}-${twoDigitMonth}-${twoDigitDay}"; + } + } +} diff --git a/examples/fitness/lib/fitness_item.dart b/examples/fitness/lib/fitness_item.dart index 34e71c6e0bb..b4c952c4b73 100644 --- a/examples/fitness/lib/fitness_item.dart +++ b/examples/fitness/lib/fitness_item.dart @@ -6,7 +6,9 @@ part of fitness; typedef void FitnessItemHandler(FitnessItem item); -const double kFitnessItemHeight = 79.0; +// 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']); @@ -19,7 +21,7 @@ abstract class FitnessItem { Map toJson() => { 'when' : when.toIso8601String() }; // TODO(jackson): Internationalize - String get displayDate => "${when.year.toString()}-${when.month.toString().padLeft(2,'0')}-${when.day.toString().padLeft(2,'0')}"; + String get displayDate => DateUtils.toRecentTimeString(when); FitnessItemRow toRow({ FitnessItemHandler onDismissed }); } @@ -40,12 +42,19 @@ abstract class FitnessItemRow extends Component { Widget build() { return new Dismissable( onDismissed: () => onDismissed(item), - child: new Card( - child: new Container( - height: kFitnessItemHeight, - padding: const EdgeDims.all(8.0), - child: buildContent() - ) + 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(this).dividerColor) + ) + ), + child: buildContent() ) ); } diff --git a/examples/fitness/lib/main.dart b/examples/fitness/lib/main.dart index debf33ef31c..e30787c5200 100644 --- a/examples/fitness/lib/main.dart +++ b/examples/fitness/lib/main.dart @@ -9,6 +9,8 @@ import 'package:sky/painting/text_style.dart'; import 'package:sky/theme/colors.dart' as colors; import 'package:sky/widgets.dart'; import 'user_data.dart'; +import 'date_utils.dart'; +import 'dart:async'; part 'feed.dart'; part 'fitness_item.dart'; @@ -17,23 +19,47 @@ part 'meal.dart'; part 'measurement.dart'; part 'settings.dart'; -class FitnessApp extends App { +class UserData { + List _items = []; + List get items => _items; + void set items(List newItems) { + _items = []; + _items.addAll(newItems); + sort(); + } + + void sort() { + _items.sort((a, b) => -a.when.compareTo(b.when)); + } + + void addAndSave(FitnessItem item) { + _items.add(item); + sort(); + save(); + } + + void removeAndSave(FitnessItem item) { + _items.remove(item); + save(); + } + + Future save() => saveFitnessData(_items); +} + +class FitnessApp extends App { NavigationState _navigationState; - final List _userData = []; + final UserData _userData = new UserData(); void didMount() { super.didMount(); loadFitnessData().then((List list) { - setState(() { - _userData.addAll(list); - }); + setState(() => _userData.items = list); }).catchError((e) => print("Failed to load data: $e")); } void save() { - saveFitnessData(_userData) - .catchError((e) => print("Failed to load data: $e")); + _userData.save().catchError((e) => print("Failed to load data: $e")); } void initState() { @@ -42,7 +68,7 @@ class FitnessApp extends App { name: '/', builder: (navigator, route) => new FeedFragment( navigator: navigator, - userData: _userData, + userData: _userData.items, onItemCreated: _handleItemCreated, onItemDeleted: _handleItemDeleted ) @@ -71,27 +97,18 @@ class FitnessApp extends App { void onBack() { if (_navigationState.hasPrevious()) { - setState(() { - _navigationState.pop(); - }); + setState(() => _navigationState.pop()); } else { super.onBack(); } } void _handleItemCreated(FitnessItem item) { - setState(() { - _userData.add(item); - _userData.sort((a, b) => a.when.compareTo(b.when)); - save(); - }); + setState(() => _userData.addAndSave(item)); } void _handleItemDeleted(FitnessItem item) { - setState(() { - _userData.remove(item); - saveFitnessData(_userData); - }); + setState(() => _userData.removeAndSave(item)); } BackupMode backupSetting = BackupMode.disabled; diff --git a/examples/fitness/lib/measurement.dart b/examples/fitness/lib/measurement.dart index a84d4fc9b16..19d9bef9123 100644 --- a/examples/fitness/lib/measurement.dart +++ b/examples/fitness/lib/measurement.dart @@ -35,7 +35,7 @@ class MeasurementRow extends FitnessItemRow { new Flexible( child: new Text( measurement.displayWeight, - style: const TextStyle(textAlign: TextAlign.right) + style: Theme.of(this).text.subhead ) ), new Flexible(