[ermine] Add datetime and timezone quick settings.
- Fix weather setting to use hardcoded station for now.
- Update existing settings based on quickui.fidl changes in
http://fxr/335169
- Refactor status.dart to separate widgets for quickui spec types.
- Add ability to display detail view for timezone setting.
Testing: Includes tests for datetime and timezone.
Change-Id: I0cf8b842acaf0da8e5305c207aa7ae9712035355
diff --git a/lib/quickui/test/quickui_test.dart b/lib/quickui/test/quickui_test.dart
index 9bf75ae..c84d97d 100644
--- a/lib/quickui/test/quickui_test.dart
+++ b/lib/quickui/test/quickui_test.dart
@@ -44,6 +44,7 @@
@override
void update(Value value) {
spec = Spec(
+ title: '',
groups: <Group>[
Group(title: 'Bar', values: [value]),
],
@@ -54,6 +55,6 @@
void dispose() {}
static Spec _build() {
- return Spec(groups: <Group>[Group(title: 'Foo', values: [])]);
+ return Spec(title: '', groups: <Group>[Group(title: 'Foo', values: [])]);
}
}
diff --git a/lib/quickui/test/uistream_test.dart b/lib/quickui/test/uistream_test.dart
index e20cc46..fe406cf 100644
--- a/lib/quickui/test/uistream_test.dart
+++ b/lib/quickui/test/uistream_test.dart
@@ -50,6 +50,7 @@
@override
void update(Value value) {
spec = Spec(
+ title: '',
groups: <Group>[
Group(title: 'Bar', values: [value]),
],
@@ -60,6 +61,6 @@
void dispose() {}
static Spec _build() {
- return Spec(groups: <Group>[Group(title: 'Foo', values: [])]);
+ return Spec(title: '', groups: <Group>[Group(title: 'Foo', values: [])]);
}
}
diff --git a/session_shells/ermine/internationalization/lib/strings.dart b/session_shells/ermine/internationalization/lib/strings.dart
index fa3d215..487168a 100644
--- a/session_shells/ermine/internationalization/lib/strings.dart
+++ b/session_shells/ermine/internationalization/lib/strings.dart
@@ -164,12 +164,30 @@
desc: 'The short name for the "Weather" label',
);
+ static String get unit => Intl.message(
+ 'Unit',
+ name: 'unit',
+ desc: 'The short name for the unit of measurement',
+ );
+
static String get date => Intl.message(
'Date',
name: 'date',
desc: 'The short name for the "date" label',
);
+ static String get dateTime => Intl.message(
+ 'Date & Time',
+ name: 'dateTime',
+ desc: 'The short name for the "date & time" label',
+ );
+
+ static String get timezone => Intl.message(
+ 'Timezone',
+ name: 'timezone',
+ desc: 'The short name for the "timezone" label',
+ );
+
static String get network => Intl.message(
'Network',
name: 'network',
diff --git a/session_shells/ermine/settings/BUILD.gn b/session_shells/ermine/settings/BUILD.gn
index d88c8d0..3a49b1b 100644
--- a/session_shells/ermine/settings/BUILD.gn
+++ b/session_shells/ermine/settings/BUILD.gn
@@ -13,7 +13,9 @@
"src/battery.dart",
"src/bluetooth.dart",
"src/brightness.dart",
+ "src/datetime.dart",
"src/memory.dart",
+ "src/timezone.dart",
"src/volume.dart",
"src/weather.dart",
]
@@ -23,6 +25,7 @@
"//sdk/fidl/fuchsia.media",
"//sdk/fidl/fuchsia.memory",
"//sdk/fidl/fuchsia.power",
+ "//sdk/fidl/fuchsia.timezone",
"//sdk/fidl/fuchsia.ui.brightness",
"//src/experiences/lib/quickui",
"//src/experiences/session_shells/ermine/internationalization",
@@ -34,7 +37,9 @@
flutter_test("ermine_settings_unittests") {
sources = [
"brightness_test.dart",
+ "datetime_test.dart",
"memory_test.dart",
+ "timezone_test.dart",
]
deps = [
diff --git a/session_shells/ermine/settings/lib/settings.dart b/session_shells/ermine/settings/lib/settings.dart
index 9fe40a8..6b9ddbe 100644
--- a/session_shells/ermine/settings/lib/settings.dart
+++ b/session_shells/ermine/settings/lib/settings.dart
@@ -5,6 +5,8 @@
export 'src/battery.dart';
export 'src/bluetooth.dart';
export 'src/brightness.dart';
+export 'src/datetime.dart';
export 'src/memory.dart';
+export 'src/timezone.dart';
export 'src/volume.dart';
export 'src/weather.dart';
diff --git a/session_shells/ermine/settings/lib/src/battery.dart b/session_shells/ermine/settings/lib/src/battery.dart
index 82212f7..2458e4c 100644
--- a/session_shells/ermine/settings/lib/src/battery.dart
+++ b/session_shells/ermine/settings/lib/src/battery.dart
@@ -51,14 +51,14 @@
return null;
}
if (value == 100) {
- return Spec(groups: [
+ return Spec(title: _title, groups: [
Group(title: _title, values: [
Value.withIcon(IconValue(codePoint: Icons.battery_full.codePoint)),
Value.withText(TextValue(text: batteryText)),
]),
]);
} else if (charging) {
- return Spec(groups: [
+ return Spec(title: _title, groups: [
Group(title: _title, values: [
Value.withIcon(
IconValue(codePoint: Icons.battery_charging_full.codePoint)),
@@ -66,14 +66,14 @@
]),
]);
} else if (value <= 10) {
- return Spec(groups: [
+ return Spec(title: _title, groups: [
Group(title: _title, values: [
Value.withIcon(IconValue(codePoint: Icons.battery_alert.codePoint)),
Value.withText(TextValue(text: batteryText)),
]),
]);
} else {
- return Spec(groups: [
+ return Spec(title: _title, groups: [
Group(title: _title, values: [
Value.withText(TextValue(text: batteryText)),
]),
@@ -96,7 +96,8 @@
}) : _binding = binding ?? BatteryInfoWatcherBinding() {
monitor
..watch(_binding.wrap(_BatteryInfoWatcherImpl(this)))
- ..getBatteryInfo().then(_updateBattery);
+ ..getBatteryInfo().then(_updateBattery)
+ ..ctrl.close();
}
void dispose() {
diff --git a/session_shells/ermine/settings/lib/src/bluetooth.dart b/session_shells/ermine/settings/lib/src/bluetooth.dart
index c9ab07e..dc9aa3c 100644
--- a/session_shells/ermine/settings/lib/src/bluetooth.dart
+++ b/session_shells/ermine/settings/lib/src/bluetooth.dart
@@ -45,7 +45,7 @@
}
static Spec _specForBluetooth(List<TextValue> values) {
- return Spec(groups: [
+ return Spec(title: _title, groups: [
Group(title: _title, values: [
Value.withIcon(IconValue(codePoint: Icons.bluetooth.codePoint)),
Value.withGrid(GridValue(columns: 1, values: values))
diff --git a/session_shells/ermine/settings/lib/src/brightness.dart b/session_shells/ermine/settings/lib/src/brightness.dart
index 234bd30..273eced 100644
--- a/session_shells/ermine/settings/lib/src/brightness.dart
+++ b/session_shells/ermine/settings/lib/src/brightness.dart
@@ -68,7 +68,7 @@
}
static Spec _specForAutoBrightness(double value) {
- return Spec(groups: [
+ return Spec(title: _title, groups: [
Group(title: _title, values: [
Value.withIcon(IconValue(codePoint: Icons.brightness_auto.codePoint)),
Value.withProgress(ProgressValue(value: value, action: progressAction)),
@@ -78,7 +78,7 @@
}
static Spec _specForManualBrightness(double value) {
- return Spec(groups: [
+ return Spec(title: _title, groups: [
Group(title: _title, values: [
Value.withIcon(IconValue(codePoint: Icons.brightness_low.codePoint)),
Value.withProgress(ProgressValue(value: value, action: progressAction)),
diff --git a/session_shells/ermine/settings/lib/src/datetime.dart b/session_shells/ermine/settings/lib/src/datetime.dart
new file mode 100644
index 0000000..cc3b47b
--- /dev/null
+++ b/session_shells/ermine/settings/lib/src/datetime.dart
@@ -0,0 +1,46 @@
+// Copyright 2019 The Fuchsia 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:async';
+
+import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
+import 'package:intl/intl.dart';
+import 'package:internationalization/strings.dart';
+import 'package:quickui/quickui.dart';
+
+/// Defines a [UiSpec] for displaying date and time.
+class Datetime extends UiSpec {
+ // Localized strings.
+ static String get _title => Strings.dateTime;
+ static const Duration refreshDuration = Duration(seconds: 1);
+
+ // Action to change timezone.
+ static int changeAction = QuickAction.details.$value;
+
+ Timer _timer;
+
+ Datetime() {
+ _timer = Timer.periodic(refreshDuration, (_) => _onChange());
+ _onChange();
+ }
+
+ void _onChange() async {
+ spec = _specForDateTime();
+ }
+
+ @override
+ void update(Value value) async {}
+
+ @override
+ void dispose() {
+ _timer?.cancel();
+ }
+
+ static Spec _specForDateTime([int action = 0]) {
+ String dateTime = DateFormat().add_E().add_jm().format(DateTime.now());
+ return Spec(title: _title, groups: [
+ Group(title: _title, values: [Value.withText(TextValue(text: dateTime))]),
+ ]);
+ }
+}
diff --git a/session_shells/ermine/settings/lib/src/memory.dart b/session_shells/ermine/settings/lib/src/memory.dart
index 54a9533..4432266 100644
--- a/session_shells/ermine/settings/lib/src/memory.dart
+++ b/session_shells/ermine/settings/lib/src/memory.dart
@@ -48,7 +48,7 @@
static Spec _specForMemory(double value, double used, double total) {
String usedString = (used).toStringAsPrecision(3);
String totalString = (total).toStringAsPrecision(3);
- return Spec(groups: [
+ return Spec(title: _memory, groups: [
Group(title: _memory, values: [
Value.withProgress(ProgressValue(value: value)),
Value.withText(TextValue(text: '${usedString}GB / ${totalString}GB')),
diff --git a/session_shells/ermine/settings/lib/src/timezone.dart b/session_shells/ermine/settings/lib/src/timezone.dart
new file mode 100644
index 0000000..be742b1
--- /dev/null
+++ b/session_shells/ermine/settings/lib/src/timezone.dart
@@ -0,0 +1,723 @@
+// Copyright 2019 The Fuchsia 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:async';
+import 'package:flutter/material.dart';
+
+import 'package:fidl_fuchsia_timezone/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
+import 'package:fuchsia_logger/logger.dart';
+import 'package:fuchsia_services/services.dart' show StartupContext;
+import 'package:internationalization/strings.dart';
+import 'package:quickui/quickui.dart';
+
+/// Defines a [UiSpec] for displaying and changing timezone.
+class TimeZone extends UiSpec {
+ // Localized strings.
+ static String get _title => Strings.timezone;
+
+ // Action to change timezone.
+ static int changeAction = QuickAction.details.$value;
+
+ _TimeZoneModel model;
+
+ TimeZone({TimezoneProxy timezone, TimezoneWatcherBinding binding}) {
+ model = _TimeZoneModel(
+ timezone: timezone,
+ binding: binding,
+ onChange: _onChange,
+ );
+ }
+
+ factory TimeZone.fromStartupContext(StartupContext startupContext) {
+ // Connect to timeservice and update the system time.
+ final timeService = TimeServiceProxy();
+ startupContext.incoming.connectToService(timeService);
+ timeService.update(3 /* num_retries */).then((success) {
+ if (!success) {
+ log.warning('Failed to update system time from the network.');
+ }
+ timeService.ctrl.close();
+ });
+
+ // Connect to timezone service.
+ final timezoneService = TimezoneProxy();
+ startupContext.incoming.connectToService(timezoneService);
+
+ final timezone = TimeZone(timezone: timezoneService);
+ return timezone;
+ }
+
+ void _onChange() async {
+ spec = _specForTimeZone(model);
+ }
+
+ @override
+ void update(Value value) async {
+ if (value.$tag == ValueTag.button &&
+ value.button.action == QuickAction.cancel.$value) {
+ spec = _specForTimeZone(model);
+ } else if (value.$tag == ValueTag.text && value.text.action > 0) {
+ if (value.text.action == changeAction) {
+ spec = _specForTimeZone(model, changeAction);
+ } else {
+ final index = value.text.action ^ QuickAction.submit.$value;
+ model.timezoneId = _kTimeZones[index].zoneId;
+ spec = _specForTimeZone(model);
+ }
+ }
+ }
+
+ @override
+ void dispose() {
+ model.dispose();
+ }
+
+ static Spec _specForTimeZone(_TimeZoneModel model, [int action = 0]) {
+ if (action == 0 || action & QuickAction.cancel.$value > 0) {
+ return Spec(title: _title, groups: [
+ Group(title: _title, values: [
+ Value.withText(TextValue(
+ text: model.timezoneId,
+ action: changeAction,
+ )),
+ ]),
+ ]);
+ } else if (action == changeAction) {
+ final values = List<TextValue>.generate(
+ _kTimeZones.length,
+ (index) => TextValue(
+ text: _kTimeZones[index].zoneId,
+ action: QuickAction.submit.$value | index,
+ ));
+ return Spec(title: _title, groups: [
+ Group(title: 'Select Timezone', values: [
+ Value.withGrid(GridValue(
+ columns: 1,
+ values: values,
+ )),
+ Value.withButton(ButtonValue(
+ label: 'close',
+ action: QuickAction.cancel.$value,
+ )),
+ ]),
+ ]);
+ } else {
+ return null;
+ }
+ }
+}
+
+class _TimeZoneModel {
+ final TimezoneProxy timezone;
+ final TimezoneWatcherBinding _binding;
+ final VoidCallback onChange;
+
+ String _timezoneId;
+
+ _TimeZoneModel({this.timezone, TimezoneWatcherBinding binding, this.onChange})
+ : _binding = binding ?? TimezoneWatcherBinding() {
+ // Get current timezone and watch it for changes.
+ timezone
+ ..getTimezoneId().then((tz) {
+ _timezoneId = tz;
+ onChange();
+ })
+ ..watch(_binding.wrap(_TimezoneWatcherImpl(this)));
+ }
+
+ void dispose() {
+ timezone.ctrl.close();
+ _binding.close();
+ }
+
+ String get timezoneId => _timezoneId;
+ set timezoneId(String value) {
+ _timezoneId = value;
+ timezone.setTimezone(value);
+ }
+}
+
+class _TimezoneWatcherImpl extends TimezoneWatcher {
+ final _TimeZoneModel model;
+ _TimezoneWatcherImpl(this.model);
+ @override
+ Future<void> onTimezoneOffsetChange(String timezoneId) async {
+ model._timezoneId = timezoneId;
+ model.onChange();
+ }
+}
+
+class _Timezone {
+ /// The ICU standard zone ID.
+ final String zoneId;
+
+ const _Timezone({this.zoneId});
+}
+
+// Note: these timezones were generated from a script using ICU data.
+// These should ideally be loaded ad hoc or stored somewhere.
+const List<_Timezone> _kTimeZones = <_Timezone>[
+ _Timezone(zoneId: 'US/Eastern'),
+ _Timezone(zoneId: 'US/Pacific'),
+ _Timezone(zoneId: 'Europe/Paris'),
+ _Timezone(zoneId: 'Africa/Abidjan'),
+ _Timezone(zoneId: 'Africa/Accra'),
+ _Timezone(zoneId: 'Africa/Addis_Ababa'),
+ _Timezone(zoneId: 'Africa/Algiers'),
+ _Timezone(zoneId: 'Africa/Asmara'),
+ _Timezone(zoneId: 'Africa/Asmera'),
+ _Timezone(zoneId: 'Africa/Bamako'),
+ _Timezone(zoneId: 'Africa/Bangui'),
+ _Timezone(zoneId: 'Africa/Banjul'),
+ _Timezone(zoneId: 'Africa/Bissau'),
+ _Timezone(zoneId: 'Africa/Blantyre'),
+ _Timezone(zoneId: 'Africa/Brazzaville'),
+ _Timezone(zoneId: 'Africa/Bujumbura'),
+ _Timezone(zoneId: 'Africa/Cairo'),
+ _Timezone(zoneId: 'Africa/Casablanca'),
+ _Timezone(zoneId: 'Africa/Ceuta'),
+ _Timezone(zoneId: 'Africa/Conakry'),
+ _Timezone(zoneId: 'Africa/Dakar'),
+ _Timezone(zoneId: 'Africa/Dar_es_Salaam'),
+ _Timezone(zoneId: 'Africa/Djibouti'),
+ _Timezone(zoneId: 'Africa/Douala'),
+ _Timezone(zoneId: 'Africa/El_Aaiun'),
+ _Timezone(zoneId: 'Africa/Freetown'),
+ _Timezone(zoneId: 'Africa/Gaborone'),
+ _Timezone(zoneId: 'Africa/Harare'),
+ _Timezone(zoneId: 'Africa/Johannesburg'),
+ _Timezone(zoneId: 'Africa/Kampala'),
+ _Timezone(zoneId: 'Africa/Khartoum'),
+ _Timezone(zoneId: 'Africa/Kigali'),
+ _Timezone(zoneId: 'Africa/Kinshasa'),
+ _Timezone(zoneId: 'Africa/Lagos'),
+ _Timezone(zoneId: 'Africa/Libreville'),
+ _Timezone(zoneId: 'Africa/Lome'),
+ _Timezone(zoneId: 'Africa/Luanda'),
+ _Timezone(zoneId: 'Africa/Lubumbashi'),
+ _Timezone(zoneId: 'Africa/Lusaka'),
+ _Timezone(zoneId: 'Africa/Malabo'),
+ _Timezone(zoneId: 'Africa/Maputo'),
+ _Timezone(zoneId: 'Africa/Maseru'),
+ _Timezone(zoneId: 'Africa/Mbabane'),
+ _Timezone(zoneId: 'Africa/Mogadishu'),
+ _Timezone(zoneId: 'Africa/Monrovia'),
+ _Timezone(zoneId: 'Africa/Nairobi'),
+ _Timezone(zoneId: 'Africa/Ndjamena'),
+ _Timezone(zoneId: 'Africa/Niamey'),
+ _Timezone(zoneId: 'Africa/Nouakchott'),
+ _Timezone(zoneId: 'Africa/Ouagadougou'),
+ _Timezone(zoneId: 'Africa/Porto-Novo'),
+ _Timezone(zoneId: 'Africa/Sao_Tome'),
+ _Timezone(zoneId: 'Africa/Timbuktu'),
+ _Timezone(zoneId: 'Africa/Tripoli'),
+ _Timezone(zoneId: 'Africa/Tunis'),
+ _Timezone(zoneId: 'Africa/Windhoek'),
+ _Timezone(zoneId: 'America/Adak'),
+ _Timezone(zoneId: 'America/Anchorage'),
+ _Timezone(zoneId: 'America/Anguilla'),
+ _Timezone(zoneId: 'America/Antigua'),
+ _Timezone(zoneId: 'America/Araguaina'),
+ _Timezone(zoneId: 'America/Argentina/Buenos_Aires'),
+ _Timezone(zoneId: 'America/Argentina/Catamarca'),
+ _Timezone(zoneId: 'America/Argentina/ComodRivadavia'),
+ _Timezone(zoneId: 'America/Argentina/Cordoba'),
+ _Timezone(zoneId: 'America/Argentina/Jujuy'),
+ _Timezone(zoneId: 'America/Argentina/La_Rioja'),
+ _Timezone(zoneId: 'America/Argentina/Mendoza'),
+ _Timezone(zoneId: 'America/Argentina/Rio_Gallegos'),
+ _Timezone(zoneId: 'America/Argentina/San_Juan'),
+ _Timezone(zoneId: 'America/Argentina/Tucuman'),
+ _Timezone(zoneId: 'America/Argentina/Ushuaia'),
+ _Timezone(zoneId: 'America/Aruba'),
+ _Timezone(zoneId: 'America/Asuncion'),
+ _Timezone(zoneId: 'America/Atikokan'),
+ _Timezone(zoneId: 'America/Atka'),
+ _Timezone(zoneId: 'America/Bahia'),
+ _Timezone(zoneId: 'America/Barbados'),
+ _Timezone(zoneId: 'America/Belem'),
+ _Timezone(zoneId: 'America/Belize'),
+ _Timezone(zoneId: 'America/Blanc-Sablon'),
+ _Timezone(zoneId: 'America/Boa_Vista'),
+ _Timezone(zoneId: 'America/Bogota'),
+ _Timezone(zoneId: 'America/Boise'),
+ _Timezone(zoneId: 'America/Buenos_Aires'),
+ _Timezone(zoneId: 'America/Cambridge_Bay'),
+ _Timezone(zoneId: 'America/Campo_Grande'),
+ _Timezone(zoneId: 'America/Cancun'),
+ _Timezone(zoneId: 'America/Caracas'),
+ _Timezone(zoneId: 'America/Catamarca'),
+ _Timezone(zoneId: 'America/Cayenne'),
+ _Timezone(zoneId: 'America/Cayman'),
+ _Timezone(zoneId: 'America/Chicago'),
+ _Timezone(zoneId: 'America/Chihuahua'),
+ _Timezone(zoneId: 'America/Coral_Harbour'),
+ _Timezone(zoneId: 'America/Cordoba'),
+ _Timezone(zoneId: 'America/Costa_Rica'),
+ _Timezone(zoneId: 'America/Cuiaba'),
+ _Timezone(zoneId: 'America/Curacao'),
+ _Timezone(zoneId: 'America/Danmarkshavn'),
+ _Timezone(zoneId: 'America/Dawson'),
+ _Timezone(zoneId: 'America/Dawson_Creek'),
+ _Timezone(zoneId: 'America/Denver'),
+ _Timezone(zoneId: 'America/Detroit'),
+ _Timezone(zoneId: 'America/Dominica'),
+ _Timezone(zoneId: 'America/Edmonton'),
+ _Timezone(zoneId: 'America/Eirunepe'),
+ _Timezone(zoneId: 'America/El_Salvador'),
+ _Timezone(zoneId: 'America/Ensenada'),
+ _Timezone(zoneId: 'America/Fort_Wayne'),
+ _Timezone(zoneId: 'America/Fortaleza'),
+ _Timezone(zoneId: 'America/Glace_Bay'),
+ _Timezone(zoneId: 'America/Godthab'),
+ _Timezone(zoneId: 'America/Goose_Bay'),
+ _Timezone(zoneId: 'America/Grand_Turk'),
+ _Timezone(zoneId: 'America/Grenada'),
+ _Timezone(zoneId: 'America/Guadeloupe'),
+ _Timezone(zoneId: 'America/Guatemala'),
+ _Timezone(zoneId: 'America/Guayaquil'),
+ _Timezone(zoneId: 'America/Guyana'),
+ _Timezone(zoneId: 'America/Halifax'),
+ _Timezone(zoneId: 'America/Havana'),
+ _Timezone(zoneId: 'America/Hermosillo'),
+ _Timezone(zoneId: 'America/Indiana/Indianapolis'),
+ _Timezone(zoneId: 'America/Indiana/Knox'),
+ _Timezone(zoneId: 'America/Indiana/Marengo'),
+ _Timezone(zoneId: 'America/Indiana/Petersburg'),
+ _Timezone(zoneId: 'America/Indiana/Tell_City'),
+ _Timezone(zoneId: 'America/Indiana/Vevay'),
+ _Timezone(zoneId: 'America/Indiana/Vincennes'),
+ _Timezone(zoneId: 'America/Indiana/Winamac'),
+ _Timezone(zoneId: 'America/Indianapolis'),
+ _Timezone(zoneId: 'America/Inuvik'),
+ _Timezone(zoneId: 'America/Iqaluit'),
+ _Timezone(zoneId: 'America/Jamaica'),
+ _Timezone(zoneId: 'America/Jujuy'),
+ _Timezone(zoneId: 'America/Juneau'),
+ _Timezone(zoneId: 'America/Kentucky/Louisville'),
+ _Timezone(zoneId: 'America/Kentucky/Monticello'),
+ _Timezone(zoneId: 'America/Knox_IN'),
+ _Timezone(zoneId: 'America/La_Paz'),
+ _Timezone(zoneId: 'America/Lima'),
+ _Timezone(zoneId: 'America/Los_Angeles'),
+ _Timezone(zoneId: 'America/Louisville'),
+ _Timezone(zoneId: 'America/Maceio'),
+ _Timezone(zoneId: 'America/Managua'),
+ _Timezone(zoneId: 'America/Manaus'),
+ _Timezone(zoneId: 'America/Marigot'),
+ _Timezone(zoneId: 'America/Martinique'),
+ _Timezone(zoneId: 'America/Mazatlan'),
+ _Timezone(zoneId: 'America/Mendoza'),
+ _Timezone(zoneId: 'America/Menominee'),
+ _Timezone(zoneId: 'America/Merida'),
+ _Timezone(zoneId: 'America/Mexico_City'),
+ _Timezone(zoneId: 'America/Miquelon'),
+ _Timezone(zoneId: 'America/Moncton'),
+ _Timezone(zoneId: 'America/Monterrey'),
+ _Timezone(zoneId: 'America/Montevideo'),
+ _Timezone(zoneId: 'America/Montreal'),
+ _Timezone(zoneId: 'America/Montserrat'),
+ _Timezone(zoneId: 'America/Nassau'),
+ _Timezone(zoneId: 'America/New_York'),
+ _Timezone(zoneId: 'America/Nipigon'),
+ _Timezone(zoneId: 'America/Nome'),
+ _Timezone(zoneId: 'America/Noronha'),
+ _Timezone(zoneId: 'America/North_Dakota/Center'),
+ _Timezone(zoneId: 'America/North_Dakota/New_Salem'),
+ _Timezone(zoneId: 'America/Panama'),
+ _Timezone(zoneId: 'America/Pangnirtung'),
+ _Timezone(zoneId: 'America/Paramaribo'),
+ _Timezone(zoneId: 'America/Phoenix'),
+ _Timezone(zoneId: 'America/Port-au-Prince'),
+ _Timezone(zoneId: 'America/Port_of_Spain'),
+ _Timezone(zoneId: 'America/Porto_Acre'),
+ _Timezone(zoneId: 'America/Porto_Velho'),
+ _Timezone(zoneId: 'America/Puerto_Rico'),
+ _Timezone(zoneId: 'America/Rainy_River'),
+ _Timezone(zoneId: 'America/Rankin_Inlet'),
+ _Timezone(zoneId: 'America/Recife'),
+ _Timezone(zoneId: 'America/Regina'),
+ _Timezone(zoneId: 'America/Resolute'),
+ _Timezone(zoneId: 'America/Rio_Branco'),
+ _Timezone(zoneId: 'America/Rosario'),
+ _Timezone(zoneId: 'America/Santiago'),
+ _Timezone(zoneId: 'America/Santo_Domingo'),
+ _Timezone(zoneId: 'America/Sao_Paulo'),
+ _Timezone(zoneId: 'America/Scoresbysund'),
+ _Timezone(zoneId: 'America/Shiprock'),
+ _Timezone(zoneId: 'America/St_Barthelemy'),
+ _Timezone(zoneId: 'America/St_Johns'),
+ _Timezone(zoneId: 'America/St_Kitts'),
+ _Timezone(zoneId: 'America/St_Lucia'),
+ _Timezone(zoneId: 'America/St_Thomas'),
+ _Timezone(zoneId: 'America/St_Vincent'),
+ _Timezone(zoneId: 'America/Swift_Current'),
+ _Timezone(zoneId: 'America/Tegucigalpa'),
+ _Timezone(zoneId: 'America/Thule'),
+ _Timezone(zoneId: 'America/Thunder_Bay'),
+ _Timezone(zoneId: 'America/Tijuana'),
+ _Timezone(zoneId: 'America/Toronto'),
+ _Timezone(zoneId: 'America/Tortola'),
+ _Timezone(zoneId: 'America/Vancouver'),
+ _Timezone(zoneId: 'America/Virgin'),
+ _Timezone(zoneId: 'America/Whitehorse'),
+ _Timezone(zoneId: 'America/Winnipeg'),
+ _Timezone(zoneId: 'America/Yakutat'),
+ _Timezone(zoneId: 'America/Yellowknife'),
+ _Timezone(zoneId: 'Antarctica/Casey'),
+ _Timezone(zoneId: 'Antarctica/Davis'),
+ _Timezone(zoneId: 'Antarctica/DumontDUrville'),
+ _Timezone(zoneId: 'Antarctica/Mawson'),
+ _Timezone(zoneId: 'Antarctica/McMurdo'),
+ _Timezone(zoneId: 'Antarctica/Palmer'),
+ _Timezone(zoneId: 'Antarctica/Rothera'),
+ _Timezone(zoneId: 'Antarctica/South_Pole'),
+ _Timezone(zoneId: 'Antarctica/Syowa'),
+ _Timezone(zoneId: 'Antarctica/Vostok'),
+ _Timezone(zoneId: 'Arctic/Longyearbyen'),
+ _Timezone(zoneId: 'Asia/Aden'),
+ _Timezone(zoneId: 'Asia/Almaty'),
+ _Timezone(zoneId: 'Asia/Amman'),
+ _Timezone(zoneId: 'Asia/Anadyr'),
+ _Timezone(zoneId: 'Asia/Aqtau'),
+ _Timezone(zoneId: 'Asia/Aqtobe'),
+ _Timezone(zoneId: 'Asia/Ashgabat'),
+ _Timezone(zoneId: 'Asia/Ashkhabad'),
+ _Timezone(zoneId: 'Asia/Baghdad'),
+ _Timezone(zoneId: 'Asia/Bahrain'),
+ _Timezone(zoneId: 'Asia/Baku'),
+ _Timezone(zoneId: 'Asia/Bangkok'),
+ _Timezone(zoneId: 'Asia/Beirut'),
+ _Timezone(zoneId: 'Asia/Bishkek'),
+ _Timezone(zoneId: 'Asia/Brunei'),
+ _Timezone(zoneId: 'Asia/Calcutta'),
+ _Timezone(zoneId: 'Asia/Choibalsan'),
+ _Timezone(zoneId: 'Asia/Chongqing'),
+ _Timezone(zoneId: 'Asia/Chungking'),
+ _Timezone(zoneId: 'Asia/Colombo'),
+ _Timezone(zoneId: 'Asia/Dacca'),
+ _Timezone(zoneId: 'Asia/Damascus'),
+ _Timezone(zoneId: 'Asia/Dhaka'),
+ _Timezone(zoneId: 'Asia/Dili'),
+ _Timezone(zoneId: 'Asia/Dubai'),
+ _Timezone(zoneId: 'Asia/Dushanbe'),
+ _Timezone(zoneId: 'Asia/Gaza'),
+ _Timezone(zoneId: 'Asia/Harbin'),
+ _Timezone(zoneId: 'Asia/Hong_Kong'),
+ _Timezone(zoneId: 'Asia/Hovd'),
+ _Timezone(zoneId: 'Asia/Irkutsk'),
+ _Timezone(zoneId: 'Asia/Istanbul'),
+ _Timezone(zoneId: 'Asia/Jakarta'),
+ _Timezone(zoneId: 'Asia/Jayapura'),
+ _Timezone(zoneId: 'Asia/Jerusalem'),
+ _Timezone(zoneId: 'Asia/Kabul'),
+ _Timezone(zoneId: 'Asia/Kamchatka'),
+ _Timezone(zoneId: 'Asia/Karachi'),
+ _Timezone(zoneId: 'Asia/Kashgar'),
+ _Timezone(zoneId: 'Asia/Katmandu'),
+ _Timezone(zoneId: 'Asia/Krasnoyarsk'),
+ _Timezone(zoneId: 'Asia/Kuala_Lumpur'),
+ _Timezone(zoneId: 'Asia/Kuching'),
+ _Timezone(zoneId: 'Asia/Kuwait'),
+ _Timezone(zoneId: 'Asia/Macao'),
+ _Timezone(zoneId: 'Asia/Macau'),
+ _Timezone(zoneId: 'Asia/Magadan'),
+ _Timezone(zoneId: 'Asia/Makassar'),
+ _Timezone(zoneId: 'Asia/Manila'),
+ _Timezone(zoneId: 'Asia/Muscat'),
+ _Timezone(zoneId: 'Asia/Nicosia'),
+ _Timezone(zoneId: 'Asia/Novosibirsk'),
+ _Timezone(zoneId: 'Asia/Omsk'),
+ _Timezone(zoneId: 'Asia/Oral'),
+ _Timezone(zoneId: 'Asia/Phnom_Penh'),
+ _Timezone(zoneId: 'Asia/Pontianak'),
+ _Timezone(zoneId: 'Asia/Pyongyang'),
+ _Timezone(zoneId: 'Asia/Qatar'),
+ _Timezone(zoneId: 'Asia/Qyzylorda'),
+ _Timezone(zoneId: 'Asia/Rangoon'),
+ _Timezone(zoneId: 'Asia/Riyadh'),
+ _Timezone(zoneId: 'Asia/Riyadh87'),
+ _Timezone(zoneId: 'Asia/Riyadh88'),
+ _Timezone(zoneId: 'Asia/Riyadh89'),
+ _Timezone(zoneId: 'Asia/Saigon'),
+ _Timezone(zoneId: 'Asia/Sakhalin'),
+ _Timezone(zoneId: 'Asia/Samarkand'),
+ _Timezone(zoneId: 'Asia/Seoul'),
+ _Timezone(zoneId: 'Asia/Shanghai'),
+ _Timezone(zoneId: 'Asia/Singapore'),
+ _Timezone(zoneId: 'Asia/Taipei'),
+ _Timezone(zoneId: 'Asia/Tashkent'),
+ _Timezone(zoneId: 'Asia/Tbilisi'),
+ _Timezone(zoneId: 'Asia/Tehran'),
+ _Timezone(zoneId: 'Asia/Tel_Aviv'),
+ _Timezone(zoneId: 'Asia/Thimbu'),
+ _Timezone(zoneId: 'Asia/Thimphu'),
+ _Timezone(zoneId: 'Asia/Tokyo'),
+ _Timezone(zoneId: 'Asia/Ujung_Pandang'),
+ _Timezone(zoneId: 'Asia/Ulaanbaatar'),
+ _Timezone(zoneId: 'Asia/Ulan_Bator'),
+ _Timezone(zoneId: 'Asia/Urumqi'),
+ _Timezone(zoneId: 'Asia/Vientiane'),
+ _Timezone(zoneId: 'Asia/Vladivostok'),
+ _Timezone(zoneId: 'Asia/Yakutsk'),
+ _Timezone(zoneId: 'Asia/Yekaterinburg'),
+ _Timezone(zoneId: 'Asia/Yerevan'),
+ _Timezone(zoneId: 'Atlantic/Azores'),
+ _Timezone(zoneId: 'Atlantic/Bermuda'),
+ _Timezone(zoneId: 'Atlantic/Canary'),
+ _Timezone(zoneId: 'Atlantic/Cape_Verde'),
+ _Timezone(zoneId: 'Atlantic/Faeroe'),
+ _Timezone(zoneId: 'Atlantic/Faroe'),
+ _Timezone(zoneId: 'Atlantic/Jan_Mayen'),
+ _Timezone(zoneId: 'Atlantic/Madeira'),
+ _Timezone(zoneId: 'Atlantic/Reykjavik'),
+ _Timezone(zoneId: 'Atlantic/South_Georgia'),
+ _Timezone(zoneId: 'Atlantic/St_Helena'),
+ _Timezone(zoneId: 'Atlantic/Stanley'),
+ _Timezone(zoneId: 'Australia/ACT'),
+ _Timezone(zoneId: 'Australia/Adelaide'),
+ _Timezone(zoneId: 'Australia/Brisbane'),
+ _Timezone(zoneId: 'Australia/Broken_Hill'),
+ _Timezone(zoneId: 'Australia/Canberra'),
+ _Timezone(zoneId: 'Australia/Currie'),
+ _Timezone(zoneId: 'Australia/Darwin'),
+ _Timezone(zoneId: 'Australia/Eucla'),
+ _Timezone(zoneId: 'Australia/Hobart'),
+ _Timezone(zoneId: 'Australia/LHI'),
+ _Timezone(zoneId: 'Australia/Lindeman'),
+ _Timezone(zoneId: 'Australia/Lord_Howe'),
+ _Timezone(zoneId: 'Australia/Melbourne'),
+ _Timezone(zoneId: 'Australia/NSW'),
+ _Timezone(zoneId: 'Australia/North'),
+ _Timezone(zoneId: 'Australia/Perth'),
+ _Timezone(zoneId: 'Australia/Queensland'),
+ _Timezone(zoneId: 'Australia/South'),
+ _Timezone(zoneId: 'Australia/Sydney'),
+ _Timezone(zoneId: 'Australia/Tasmania'),
+ _Timezone(zoneId: 'Australia/Victoria'),
+ _Timezone(zoneId: 'Australia/West'),
+ _Timezone(zoneId: 'Australia/Yancowinna'),
+ _Timezone(zoneId: 'Brazil/Acre'),
+ _Timezone(zoneId: 'Brazil/DeNoronha'),
+ _Timezone(zoneId: 'Brazil/East'),
+ _Timezone(zoneId: 'Brazil/West'),
+ _Timezone(zoneId: 'CET'),
+ _Timezone(zoneId: 'CST6CDT'),
+ _Timezone(zoneId: 'Canada/Atlantic'),
+ _Timezone(zoneId: 'Canada/Central'),
+ _Timezone(zoneId: 'Canada/East-Saskatchewan'),
+ _Timezone(zoneId: 'Canada/Eastern'),
+ _Timezone(zoneId: 'Canada/Mountain'),
+ _Timezone(zoneId: 'Canada/Newfoundland'),
+ _Timezone(zoneId: 'Canada/Pacific'),
+ _Timezone(zoneId: 'Canada/Saskatchewan'),
+ _Timezone(zoneId: 'Canada/Yukon'),
+ _Timezone(zoneId: 'Chile/Continental'),
+ _Timezone(zoneId: 'Chile/EasterIsland'),
+ _Timezone(zoneId: 'Cuba'),
+ _Timezone(zoneId: 'EET'),
+ _Timezone(zoneId: 'EST'),
+ _Timezone(zoneId: 'EST5EDT'),
+ _Timezone(zoneId: 'Egypt'),
+ _Timezone(zoneId: 'Eire'),
+ _Timezone(zoneId: 'Etc/GMT'),
+ _Timezone(zoneId: 'Etc/GMT+0'),
+ _Timezone(zoneId: 'Etc/GMT+1'),
+ _Timezone(zoneId: 'Etc/GMT+10'),
+ _Timezone(zoneId: 'Etc/GMT+11'),
+ _Timezone(zoneId: 'Etc/GMT+12'),
+ _Timezone(zoneId: 'Etc/GMT+2'),
+ _Timezone(zoneId: 'Etc/GMT+3'),
+ _Timezone(zoneId: 'Etc/GMT+4'),
+ _Timezone(zoneId: 'Etc/GMT+5'),
+ _Timezone(zoneId: 'Etc/GMT+6'),
+ _Timezone(zoneId: 'Etc/GMT+7'),
+ _Timezone(zoneId: 'Etc/GMT+8'),
+ _Timezone(zoneId: 'Etc/GMT+9'),
+ _Timezone(zoneId: 'Etc/GMT-0'),
+ _Timezone(zoneId: 'Etc/GMT-1'),
+ _Timezone(zoneId: 'Etc/GMT-10'),
+ _Timezone(zoneId: 'Etc/GMT-11'),
+ _Timezone(zoneId: 'Etc/GMT-12'),
+ _Timezone(zoneId: 'Etc/GMT-13'),
+ _Timezone(zoneId: 'Etc/GMT-14'),
+ _Timezone(zoneId: 'Etc/GMT-2'),
+ _Timezone(zoneId: 'Etc/GMT-3'),
+ _Timezone(zoneId: 'Etc/GMT-4'),
+ _Timezone(zoneId: 'Etc/GMT-5'),
+ _Timezone(zoneId: 'Etc/GMT-6'),
+ _Timezone(zoneId: 'Etc/GMT-7'),
+ _Timezone(zoneId: 'Etc/GMT-8'),
+ _Timezone(zoneId: 'Etc/GMT-9'),
+ _Timezone(zoneId: 'Etc/GMT0'),
+ _Timezone(zoneId: 'Etc/Greenwich'),
+ _Timezone(zoneId: 'Etc/UCT'),
+ _Timezone(zoneId: 'Etc/UTC'),
+ _Timezone(zoneId: 'Etc/Universal'),
+ _Timezone(zoneId: 'Etc/Zulu'),
+ _Timezone(zoneId: 'Europe/Amsterdam'),
+ _Timezone(zoneId: 'Europe/Andorra'),
+ _Timezone(zoneId: 'Europe/Athens'),
+ _Timezone(zoneId: 'Europe/Belfast'),
+ _Timezone(zoneId: 'Europe/Belgrade'),
+ _Timezone(zoneId: 'Europe/Berlin'),
+ _Timezone(zoneId: 'Europe/Bratislava'),
+ _Timezone(zoneId: 'Europe/Brussels'),
+ _Timezone(zoneId: 'Europe/Bucharest'),
+ _Timezone(zoneId: 'Europe/Budapest'),
+ _Timezone(zoneId: 'Europe/Chisinau'),
+ _Timezone(zoneId: 'Europe/Copenhagen'),
+ _Timezone(zoneId: 'Europe/Dublin'),
+ _Timezone(zoneId: 'Europe/Gibraltar'),
+ _Timezone(zoneId: 'Europe/Guernsey'),
+ _Timezone(zoneId: 'Europe/Helsinki'),
+ _Timezone(zoneId: 'Europe/Isle_of_Man'),
+ _Timezone(zoneId: 'Europe/Istanbul'),
+ _Timezone(zoneId: 'Europe/Jersey'),
+ _Timezone(zoneId: 'Europe/Kaliningrad'),
+ _Timezone(zoneId: 'Europe/Kiev'),
+ _Timezone(zoneId: 'Europe/Lisbon'),
+ _Timezone(zoneId: 'Europe/Ljubljana'),
+ _Timezone(zoneId: 'Europe/London'),
+ _Timezone(zoneId: 'Europe/Luxembourg'),
+ _Timezone(zoneId: 'Europe/Madrid'),
+ _Timezone(zoneId: 'Europe/Malta'),
+ _Timezone(zoneId: 'Europe/Mariehamn'),
+ _Timezone(zoneId: 'Europe/Minsk'),
+ _Timezone(zoneId: 'Europe/Monaco'),
+ _Timezone(zoneId: 'Europe/Moscow'),
+ _Timezone(zoneId: 'Europe/Nicosia'),
+ _Timezone(zoneId: 'Europe/Oslo'),
+ _Timezone(zoneId: 'Europe/Podgorica'),
+ _Timezone(zoneId: 'Europe/Prague'),
+ _Timezone(zoneId: 'Europe/Riga'),
+ _Timezone(zoneId: 'Europe/Rome'),
+ _Timezone(zoneId: 'Europe/Samara'),
+ _Timezone(zoneId: 'Europe/San_Marino'),
+ _Timezone(zoneId: 'Europe/Sarajevo'),
+ _Timezone(zoneId: 'Europe/Simferopol'),
+ _Timezone(zoneId: 'Europe/Skopje'),
+ _Timezone(zoneId: 'Europe/Sofia'),
+ _Timezone(zoneId: 'Europe/Stockholm'),
+ _Timezone(zoneId: 'Europe/Tallinn'),
+ _Timezone(zoneId: 'Europe/Tirane'),
+ _Timezone(zoneId: 'Europe/Tiraspol'),
+ _Timezone(zoneId: 'Europe/Uzhgorod'),
+ _Timezone(zoneId: 'Europe/Vaduz'),
+ _Timezone(zoneId: 'Europe/Vatican'),
+ _Timezone(zoneId: 'Europe/Vienna'),
+ _Timezone(zoneId: 'Europe/Vilnius'),
+ _Timezone(zoneId: 'Europe/Volgograd'),
+ _Timezone(zoneId: 'Europe/Warsaw'),
+ _Timezone(zoneId: 'Europe/Zagreb'),
+ _Timezone(zoneId: 'Europe/Zaporozhye'),
+ _Timezone(zoneId: 'Europe/Zurich'),
+ _Timezone(zoneId: 'Factory'),
+ _Timezone(zoneId: 'GB'),
+ _Timezone(zoneId: 'GB-Eire'),
+ _Timezone(zoneId: 'GMT'),
+ _Timezone(zoneId: 'GMT+0'),
+ _Timezone(zoneId: 'GMT-0'),
+ _Timezone(zoneId: 'GMT0'),
+ _Timezone(zoneId: 'Greenwich'),
+ _Timezone(zoneId: 'HST'),
+ _Timezone(zoneId: 'Hongkong'),
+ _Timezone(zoneId: 'Iceland'),
+ _Timezone(zoneId: 'Indian/Antananarivo'),
+ _Timezone(zoneId: 'Indian/Chagos'),
+ _Timezone(zoneId: 'Indian/Christmas'),
+ _Timezone(zoneId: 'Indian/Cocos'),
+ _Timezone(zoneId: 'Indian/Comoro'),
+ _Timezone(zoneId: 'Indian/Kerguelen'),
+ _Timezone(zoneId: 'Indian/Mahe'),
+ _Timezone(zoneId: 'Indian/Maldives'),
+ _Timezone(zoneId: 'Indian/Mauritius'),
+ _Timezone(zoneId: 'Indian/Mayotte'),
+ _Timezone(zoneId: 'Indian/Reunion'),
+ _Timezone(zoneId: 'Iran'),
+ _Timezone(zoneId: 'Israel'),
+ _Timezone(zoneId: 'Jamaica'),
+ _Timezone(zoneId: 'Japan'),
+ _Timezone(zoneId: 'Kwajalein'),
+ _Timezone(zoneId: 'Libya'),
+ _Timezone(zoneId: 'MET'),
+ _Timezone(zoneId: 'MST'),
+ _Timezone(zoneId: 'MST7MDT'),
+ _Timezone(zoneId: 'Mexico/BajaNorte'),
+ _Timezone(zoneId: 'Mexico/BajaSur'),
+ _Timezone(zoneId: 'Mexico/General'),
+ _Timezone(zoneId: 'Mideast/Riyadh87'),
+ _Timezone(zoneId: 'Mideast/Riyadh88'),
+ _Timezone(zoneId: 'Mideast/Riyadh89'),
+ _Timezone(zoneId: 'NZ'),
+ _Timezone(zoneId: 'NZ-CHAT'),
+ _Timezone(zoneId: 'Navajo'),
+ _Timezone(zoneId: 'PRC'),
+ _Timezone(zoneId: 'PST8PDT'),
+ _Timezone(zoneId: 'Pacific/Apia'),
+ _Timezone(zoneId: 'Pacific/Auckland'),
+ _Timezone(zoneId: 'Pacific/Chatham'),
+ _Timezone(zoneId: 'Pacific/Easter'),
+ _Timezone(zoneId: 'Pacific/Efate'),
+ _Timezone(zoneId: 'Pacific/Enderbury'),
+ _Timezone(zoneId: 'Pacific/Fakaofo'),
+ _Timezone(zoneId: 'Pacific/Fiji'),
+ _Timezone(zoneId: 'Pacific/Funafuti'),
+ _Timezone(zoneId: 'Pacific/Galapagos'),
+ _Timezone(zoneId: 'Pacific/Gambier'),
+ _Timezone(zoneId: 'Pacific/Guadalcanal'),
+ _Timezone(zoneId: 'Pacific/Guam'),
+ _Timezone(zoneId: 'Pacific/Honolulu'),
+ _Timezone(zoneId: 'Pacific/Johnston'),
+ _Timezone(zoneId: 'Pacific/Kiritimati'),
+ _Timezone(zoneId: 'Pacific/Kosrae'),
+ _Timezone(zoneId: 'Pacific/Kwajalein'),
+ _Timezone(zoneId: 'Pacific/Majuro'),
+ _Timezone(zoneId: 'Pacific/Marquesas'),
+ _Timezone(zoneId: 'Pacific/Midway'),
+ _Timezone(zoneId: 'Pacific/Nauru'),
+ _Timezone(zoneId: 'Pacific/Niue'),
+ _Timezone(zoneId: 'Pacific/Norfolk'),
+ _Timezone(zoneId: 'Pacific/Noumea'),
+ _Timezone(zoneId: 'Pacific/Pago_Pago'),
+ _Timezone(zoneId: 'Pacific/Palau'),
+ _Timezone(zoneId: 'Pacific/Pitcairn'),
+ _Timezone(zoneId: 'Pacific/Ponape'),
+ _Timezone(zoneId: 'Pacific/Port_Moresby'),
+ _Timezone(zoneId: 'Pacific/Rarotonga'),
+ _Timezone(zoneId: 'Pacific/Saipan'),
+ _Timezone(zoneId: 'Pacific/Samoa'),
+ _Timezone(zoneId: 'Pacific/Tahiti'),
+ _Timezone(zoneId: 'Pacific/Tarawa'),
+ _Timezone(zoneId: 'Pacific/Tongatapu'),
+ _Timezone(zoneId: 'Pacific/Truk'),
+ _Timezone(zoneId: 'Pacific/Wake'),
+ _Timezone(zoneId: 'Pacific/Wallis'),
+ _Timezone(zoneId: 'Pacific/Yap'),
+ _Timezone(zoneId: 'Poland'),
+ _Timezone(zoneId: 'Portugal'),
+ _Timezone(zoneId: 'ROC'),
+ _Timezone(zoneId: 'ROK'),
+ _Timezone(zoneId: 'Singapore'),
+ _Timezone(zoneId: 'Turkey'),
+ _Timezone(zoneId: 'UCT'),
+ _Timezone(zoneId: 'US/Alaska'),
+ _Timezone(zoneId: 'US/Aleutian'),
+ _Timezone(zoneId: 'US/Arizona'),
+ _Timezone(zoneId: 'US/Central'),
+ _Timezone(zoneId: 'US/East-Indiana'),
+ _Timezone(zoneId: 'US/Hawaii'),
+ _Timezone(zoneId: 'US/Indiana-Starke'),
+ _Timezone(zoneId: 'US/Michigan'),
+ _Timezone(zoneId: 'US/Mountain'),
+ _Timezone(zoneId: 'US/Pacific'),
+ _Timezone(zoneId: 'US/Pacific-New'),
+ _Timezone(zoneId: 'US/Samoa'),
+ _Timezone(zoneId: 'UTC'),
+ _Timezone(zoneId: 'Universal'),
+ _Timezone(zoneId: 'W-SU'),
+ _Timezone(zoneId: 'WET'),
+ _Timezone(zoneId: 'Zulu'),
+];
diff --git a/session_shells/ermine/settings/lib/src/volume.dart b/session_shells/ermine/settings/lib/src/volume.dart
index 6d27563..d29ee64 100644
--- a/session_shells/ermine/settings/lib/src/volume.dart
+++ b/session_shells/ermine/settings/lib/src/volume.dart
@@ -59,7 +59,7 @@
static Spec _specForVolume(double value) {
String roundedVolume = (value * 100).round().toString();
- return Spec(groups: [
+ return Spec(title: _title, groups: [
Group(title: _title, values: [
Value.withText(TextValue(text: roundedVolume)),
Value.withProgress(
diff --git a/session_shells/ermine/settings/lib/src/weather.dart b/session_shells/ermine/settings/lib/src/weather.dart
index f5e7c39..9b26dff 100644
--- a/session_shells/ermine/settings/lib/src/weather.dart
+++ b/session_shells/ermine/settings/lib/src/weather.dart
@@ -21,8 +21,8 @@
// TODO(sanjayc): Replace hardcoded locations with user specified ones.
static final List<_Location> _locations = <_Location>[
- _Location('Mountain View', '37.386051,-122.083855'),
- _Location('San Francisco', '37.61961,-122.365579'),
+ _Location('Mountain View', station: 'KSJC'),
+ _Location('San Francisco', station: 'KSFO'),
];
// Weather refresh duration.
@@ -64,7 +64,7 @@
if (locations.isEmpty) {
return null;
}
- return Spec(groups: [
+ return Spec(title: _title, groups: [
Group(title: _title, values: [
if (tempInFahrenheit)
Value.withButton(
@@ -78,7 +78,7 @@
final location = locations[index ~/ 2];
final temp =
tempInFahrenheit ? location.fahrenheit : location.degrees;
- final weather = '$temp ${location.observation}';
+ final weather = '$temp / ${location.observation}';
return TextValue(text: index.isEven ? location.name : weather);
}))),
]),
@@ -86,29 +86,7 @@
}
static Future<void> _refresh() async {
- await Future.forEach(_locations, (location) async {
- // Load the station if it is not loaded yet.
- if (location.station == null) {
- await _loadStation(location);
- }
- // Now load the current weather data for the station.
- await _loadCurrentCondition(location);
- });
- }
-
- // Get the first weather station for [_Location] point using:
- // https://www.weather.gov/documentation/services-web-api#/default/get_stations__stationId__observations_latest
- static Future<void> _loadStation(_Location location) async {
- // Get the
- final stationsUrl = '$_weatherBaseUrl/points/${location.point}/stations';
- var request = await HttpClient().getUrl(Uri.parse(stationsUrl));
- var response = await request.close();
- var result = await _readResponse(response);
- var data = json.decode(result);
- List features = data['features'];
- if (features.isNotEmpty) {
- location.station = features[0]['properties']['stationIdentifier'];
- }
+ await Future.forEach(_locations, _loadCurrentCondition);
}
// Get the latest observation for weather station in [_Location] using:
@@ -145,7 +123,7 @@
String station;
double tempInDegrees;
- _Location(this.name, this.point, [this.observation]);
+ _Location(this.name, {this.point, this.station, this.observation});
String get degrees => '${tempInDegrees.toInt()}°C';
diff --git a/session_shells/ermine/settings/test/datetime_test.dart b/session_shells/ermine/settings/test/datetime_test.dart
new file mode 100644
index 0000000..b1aa3b3
--- /dev/null
+++ b/session_shells/ermine/settings/test/datetime_test.dart
@@ -0,0 +1,23 @@
+// Copyright 2019 The Fuchsia 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 'package:flutter_test/flutter_test.dart';
+import 'package:settings/settings.dart';
+
+void main() {
+ test('Datetime', () async {
+ final stopWatch = Stopwatch()..start();
+ final datetime = Datetime();
+ var spec = await datetime.getSpec();
+
+ expect(spec.title, isNotNull);
+ expect(spec.groups.first.values.first.text.text, isNotNull);
+
+ // Make sure the next update is received after [Datetime.refreshDuration].
+ spec = await datetime.getSpec();
+ stopWatch.stop();
+
+ expect(stopWatch.elapsed >= Datetime.refreshDuration, true);
+ });
+}
diff --git a/session_shells/ermine/settings/test/timezone_test.dart b/session_shells/ermine/settings/test/timezone_test.dart
new file mode 100644
index 0000000..8364e12
--- /dev/null
+++ b/session_shells/ermine/settings/test/timezone_test.dart
@@ -0,0 +1,46 @@
+// Copyright 2019 The Fuchsia 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 'package:fidl_fuchsia_timezone/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+import 'package:settings/settings.dart';
+
+void main() {
+ test('Timezone', () async {
+ final timezoneProxy = MockTimezoneProxy();
+ final binding = MockBinding();
+
+ when(timezoneProxy.getTimezoneId())
+ .thenAnswer((_) => Future<String>.value('Foo'));
+
+ TimeZone timezone = TimeZone(timezone: timezoneProxy, binding: binding);
+ final spec = await timezone.getSpec();
+ expect(spec.groups.first.values.first.text.text == 'Foo', true);
+ });
+
+ test('Change Timezone', () async {
+ final timezoneProxy = MockTimezoneProxy();
+ final binding = MockBinding();
+
+ when(timezoneProxy.getTimezoneId())
+ .thenAnswer((_) => Future<String>.value('Foo'));
+
+ TimeZone timezone = TimeZone(timezone: timezoneProxy, binding: binding);
+ await timezone.getSpec();
+
+ final spec = await timezone.getSpec(Value.withText(TextValue(
+ text: 'US/Eastern',
+ action: QuickAction.submit.$value,
+ )));
+
+ expect(spec.groups.first.values.first.text.text == 'US/Eastern', true);
+ });
+}
+
+// Mock classes.
+class MockTimezoneProxy extends Mock implements TimezoneProxy {}
+
+class MockBinding extends Mock implements TimezoneWatcherBinding {}
diff --git a/session_shells/ermine/shell/BUILD.gn b/session_shells/ermine/shell/BUILD.gn
index 6a56100..001acee 100644
--- a/session_shells/ermine/shell/BUILD.gn
+++ b/session_shells/ermine/shell/BUILD.gn
@@ -69,8 +69,12 @@
"src/widgets/ask/ask_container.dart",
"src/widgets/ask/ask_suggestion_list.dart",
"src/widgets/ask/ask_text_field.dart",
+ "src/widgets/status/detail_status_entry.dart",
+ "src/widgets/status/spec_builder.dart",
"src/widgets/status/status.dart",
+ "src/widgets/status/status_button.dart",
"src/widgets/status/status_container.dart",
+ "src/widgets/status/status_entry.dart",
"src/widgets/status/status_graph.dart",
"src/widgets/status/status_progress.dart",
"src/widgets/story/cluster.dart",
diff --git a/session_shells/ermine/shell/lib/src/models/app_model.dart b/session_shells/ermine/shell/lib/src/models/app_model.dart
index fe57a19..e7adecd 100644
--- a/session_shells/ermine/shell/lib/src/models/app_model.dart
+++ b/session_shells/ermine/shell/lib/src/models/app_model.dart
@@ -205,6 +205,7 @@
/// Called when tapped behind Ask bar, quick settings, notifications or the
/// Escape key was pressed.
void onCancel() {
+ status.reset();
askVisibility.value = false;
statusVisibility.value = false;
helpVisibility.value = false;
diff --git a/session_shells/ermine/shell/lib/src/models/status_model.dart b/session_shells/ermine/shell/lib/src/models/status_model.dart
index 3e58666..8f11680 100644
--- a/session_shells/ermine/shell/lib/src/models/status_model.dart
+++ b/session_shells/ermine/shell/lib/src/models/status_model.dart
@@ -6,6 +6,7 @@
import 'package:fidl_fuchsia_modular/fidl_async.dart' as modular;
import 'package:fidl_fuchsia_device_manager/fidl_async.dart';
+import 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
import 'package:fuchsia_inspect/inspect.dart';
import 'package:fuchsia_services/services.dart';
import 'package:quickui/uistream.dart';
@@ -25,17 +26,22 @@
UiStream weather;
UiStream volume;
UiStream bluetooth;
+ UiStream datetime;
+ UiStream timezone;
final StartupContext startupContext;
final modular.PuppetMasterProxy puppetMaster;
final AdministratorProxy deviceManager;
+ final ValueNotifier<UiStream> detailNotifier = ValueNotifier<UiStream>(null);
StatusModel({this.startupContext, this.puppetMaster, this.deviceManager}) {
+ datetime = UiStream(Datetime());
+ timezone = UiStream(TimeZone.fromStartupContext(startupContext));
brightness = UiStream(Brightness.fromStartupContext(startupContext));
memory = UiStream(Memory.fromStartupContext(startupContext));
battery = UiStream(Battery.fromStartupContext(startupContext));
- weather = UiStream(Weather());
volume = UiStream(Volume.fromStartupContext(startupContext));
bluetooth = UiStream(Bluetooth.fromStartupContext(startupContext));
+ weather = UiStream(Weather());
}
factory StatusModel.fromStartupContext(StartupContext startupContext) {
@@ -61,6 +67,19 @@
weather.dispose();
volume.dispose();
battery.dispose();
+ datetime.dispose();
+ timezone.dispose();
+ }
+
+ UiStream get detailStream => detailNotifier.value;
+
+ void reset() {
+ // Send [QuickAction.cancel] to the detail stream if on detail view.
+ detailNotifier.value?.update(Value.withButton(ButtonValue(
+ label: '',
+ action: QuickAction.cancel.$value,
+ )));
+ detailNotifier.value = null;
}
/// Launch settings mod.
diff --git a/session_shells/ermine/shell/lib/src/widgets/status/detail_status_entry.dart b/session_shells/ermine/shell/lib/src/widgets/status/detail_status_entry.dart
new file mode 100644
index 0000000..818187b
--- /dev/null
+++ b/session_shells/ermine/shell/lib/src/widgets/status/detail_status_entry.dart
@@ -0,0 +1,84 @@
+// Copyright 2019 The Fuchsia 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 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
+import 'package:flutter/material.dart';
+
+import '../../models/status_model.dart';
+import '../../utils/styles.dart';
+import 'spec_builder.dart';
+
+/// Defines a widget that displays a status entry in the detail view of
+/// status shellement.
+class DetailStatusEntry extends StatelessWidget {
+ final StatusModel model;
+ final ValueChanged<Value> onChange;
+ final _lastSpec = ValueNotifier<Spec>(null);
+
+ DetailStatusEntry({this.model, this.onChange});
+
+ @override
+ Widget build(BuildContext context) {
+ return AnimatedBuilder(
+ animation: model.detailNotifier,
+ builder: (context, _) {
+ // Show Offstage if detail stream or last spec is not available.
+ if (_lastSpec.value == null && model.detailStream == null) {
+ return Offstage();
+ }
+
+ final uiStream = model.detailStream;
+ final spec = _lastSpec.value ?? uiStream.spec;
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: <Widget>[
+ Container(
+ decoration: BoxDecoration(
+ border: Border(
+ bottom: BorderSide(
+ color: ErmineStyle.kOverlayBorderColor,
+ width: ErmineStyle.kOverlayBorderWidth,
+ ),
+ ),
+ ),
+ child: Row(
+ children: <Widget>[
+ IconButton(
+ icon: Icon(Icons.arrow_back),
+ onPressed: () => onChange(Value.withButton(ButtonValue(
+ label: '',
+ action: QuickAction.cancel.$value,
+ ))),
+ ),
+ Padding(padding: EdgeInsets.only(left: 8)),
+ Expanded(
+ child: Text(spec.title.toUpperCase()),
+ ),
+ ],
+ ),
+ ),
+ Padding(padding: EdgeInsets.only(bottom: 8)),
+ Flexible(
+ child: SingleChildScrollView(
+ child: model.detailStream == null
+ ? buildFromSpec(spec, onChange)
+ : StreamBuilder<Spec>(
+ stream: uiStream.stream,
+ initialData: uiStream.spec,
+ builder: (context, snapshot) {
+ if (!snapshot.hasData) {
+ return Offstage();
+ }
+ _lastSpec.value = snapshot.data;
+ return buildFromSpec(_lastSpec.value, onChange);
+ },
+ ),
+ ),
+ ),
+ ],
+ );
+ },
+ );
+ }
+}
diff --git a/session_shells/ermine/shell/lib/src/widgets/status/spec_builder.dart b/session_shells/ermine/shell/lib/src/widgets/status/spec_builder.dart
new file mode 100644
index 0000000..53f5321
--- /dev/null
+++ b/session_shells/ermine/shell/lib/src/widgets/status/spec_builder.dart
@@ -0,0 +1,170 @@
+// Copyright 2019 The Fuchsia 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 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
+import 'package:flutter/material.dart';
+
+import 'status.dart';
+import 'status_button.dart';
+import 'status_graph.dart';
+import 'status_progress.dart';
+
+/// Returns a [Widget] built from the given [Spec].
+///
+/// The first row would include title [Text] and may be followed by widgets
+/// associated with the [Value] type. A [GridValue] widget is returned in its
+/// own row.
+Widget buildFromSpec(Spec spec, void Function(Value) update) {
+ // Split the values into lists separated by [GridValue].
+ List<Widget> result = <Widget>[];
+ for (final group in spec.groups) {
+ // Handle group with no values, but has a title.
+ if (group.values.isEmpty && group.title.isNotEmpty) {
+ result.add(_buildTitleRow(group.title, []));
+ continue;
+ }
+
+ // Split values in group by GridValue. Grid is on a row by itself.
+ final List<List<Value>> values = [];
+ for (final value in group.values) {
+ if (values.isEmpty ||
+ value.$tag == ValueTag.grid ||
+ values.last.last.$tag == ValueTag.grid) {
+ values.add([value]);
+ } else {
+ values.last.add(value);
+ }
+ }
+
+ for (final groupedValues in values) {
+ // Convert [Value] to [Widget]s.
+ final widgets =
+ groupedValues.map((value) => _buildFromValue(value, update)).toList();
+ // Create a title row for first set of values and value row for the rest.
+ if (groupedValues == values.first && group.title.isNotEmpty) {
+ // For grid, show title and grid in separate rows.
+ if (groupedValues.first.$tag == ValueTag.grid) {
+ result
+ ..add(_buildTitleRow(group.title, []))
+ ..add(_buildValueRow(widgets));
+ } else {
+ result.add(_buildTitleRow(group.title, widgets));
+ }
+ } else {
+ result.add(_buildValueRow(widgets));
+ }
+ }
+ }
+
+ return Container(
+ constraints: BoxConstraints(minHeight: kRowHeight),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: result,
+ ),
+ );
+}
+
+Widget _buildFromValue(Value value, void Function(Value) update) {
+ if (value.$tag == ValueTag.button) {
+ return StatusButton(value.button.label, () => update(value));
+ }
+ if (value.$tag == ValueTag.text) {
+ final text = Text(value.text.text.toUpperCase());
+ return value.text.action > 0
+ ? GestureDetector(
+ onTap: () => update(value),
+ child: text,
+ )
+ : text;
+ }
+ if (value.$tag == ValueTag.progress) {
+ return SizedBox(
+ height: kItemHeight,
+ width: kProgressBarWidth,
+ child: ProgressBar(
+ value: value.progress.value,
+ onChange: (v) => update(Value.withProgress(
+ ProgressValue(value: v, action: value.progress.action))),
+ ),
+ );
+ }
+
+ if (value.$tag == ValueTag.grid) {
+ int columns = value.grid.columns;
+ int rows = value.grid.values.length ~/ columns;
+ return Container(
+ padding: EdgeInsets.only(left: 8),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: List<Widget>.generate(rows, (row) {
+ return Padding(
+ padding: EdgeInsets.symmetric(vertical: 4),
+ child: Row(
+ // mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: List<Widget>.generate(value.grid.columns, (column) {
+ final index = row * value.grid.columns + column;
+ Value textValue = Value.withText(value.grid.values[index]);
+ final text = Text(
+ textValue.text.text,
+ textAlign: column == 0 ? TextAlign.start : TextAlign.end,
+ );
+ return Expanded(
+ flex: column == 0 ? 3 : 2,
+ child: textValue.text.action > 0
+ ? GestureDetector(
+ onTap: () => update(textValue),
+ child: text,
+ )
+ : text,
+ );
+ }),
+ ),
+ );
+ }),
+ ),
+ );
+ }
+ if (value.$tag == ValueTag.icon) {
+ return GestureDetector(
+ child: Icon(
+ IconData(
+ value.icon.codePoint,
+ fontFamily: value.icon.fontFamily ?? 'MaterialIcons',
+ ),
+ size: kIconHeight,
+ ),
+ onTap: () => update(value),
+ );
+ }
+ if (value.$tag == ValueTag.graph) {
+ return QuickGraph(value: value.graph.value, step: value.graph.step);
+ }
+ return Offstage();
+}
+
+Widget _buildValueRow(List<Widget> children) {
+ return Wrap(
+ children: children,
+ alignment: WrapAlignment.end,
+ spacing: kPadding,
+ runSpacing: kPadding,
+ );
+}
+
+Widget _buildTitleRow(String title, List<Widget> children) {
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ textBaseline: TextBaseline.alphabetic,
+ children: <Widget>[
+ Container(
+ padding: EdgeInsets.only(right: 32),
+ child: Text(title.toUpperCase()),
+ ),
+ Expanded(
+ child: _buildValueRow(children),
+ ),
+ ],
+ );
+}
diff --git a/session_shells/ermine/shell/lib/src/widgets/status/status.dart b/session_shells/ermine/shell/lib/src/widgets/status/status.dart
index 35115f1..3144a3d 100644
--- a/session_shells/ermine/shell/lib/src/widgets/status/status.dart
+++ b/session_shells/ermine/shell/lib/src/widgets/status/status.dart
@@ -10,11 +10,12 @@
import 'package:quickui/uistream.dart';
import '../../models/status_model.dart';
-import 'status_graph.dart';
-import 'status_progress.dart';
+import '../../utils/styles.dart';
+import 'detail_status_entry.dart';
+import 'status_button.dart';
+import 'status_entry.dart';
const kPadding = 12.0;
-const kTitleWidth = 100.0;
const kRowHeight = 28.0;
const kItemHeight = 16.0;
const kIconHeight = 18.0;
@@ -36,195 +37,74 @@
@override
Widget build(BuildContext context) {
- return SingleChildScrollView(
- padding: EdgeInsets.all(kPadding),
- child: Column(
- children: <Widget>[
- _ManualStatusEntry(model),
- _StatusEntry(model.brightness),
- _StatusEntry(model.volume),
- _StatusEntry(model.battery),
- _StatusEntry(model.memory),
- _StatusEntry(model.weather),
- _StatusEntry(model.bluetooth),
- ],
- ),
- );
- }
-}
+ // The [PageController] used to switch between main and detail views.
+ final pageController = PageController();
-class _StatusEntry extends StatelessWidget {
- final UiStream uiStream;
+ // Returns the callback to handle [QuickAction] from buttons in spec
+ // available from the provided [uiStream].
+ ValueChanged<Value> _onChange(UiStream uiStream) {
+ return (Value value) {
+ // Check if a button with [QuickAction] was clicked.
+ final action = _actionFromValue(value);
+ if (action & QuickAction.details.$value > 0) {
+ model.detailNotifier.value = uiStream;
- _StatusEntry(this.uiStream) {
- uiStream.listen();
- }
-
- @override
- Widget build(BuildContext context) {
- return StreamBuilder<Spec>(
- stream: uiStream.stream,
- initialData: uiStream.spec,
- builder: (context, snapshot) {
- if (!snapshot.hasData) {
- return Offstage();
- }
- final spec = snapshot.data;
- final widgets = _buildFromSpec(spec, uiStream.update);
- return Container(
- constraints: BoxConstraints(minHeight: kRowHeight),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.end,
- children: widgets,
- ),
- );
- },
- );
- }
-
-// Returns a list of [Row] widgets from the given [Spec].
-// The first row would include title [Text] and may be followed by widgets
-// associated with the [Value] type. A [GridValue] widget is returned in its
-// own row.
- List<Widget> _buildFromSpec(Spec spec, void Function(Value) update) {
- // Split the values into lists separated by [GridValue].
- List<Widget> result = <Widget>[];
- List<Widget> widgets = <Widget>[];
-
- for (final group in spec.groups) {
- for (final value in group.values) {
- final widget = _buildFromValue(value, update);
- if (value.$tag == ValueTag.grid) {
- if (result.isEmpty) {
- result.add(_buildTitleRow(group.title, widgets.toList()));
- } else {
- result.add(_buildValueRow(widgets.toList()));
- }
- widgets.clear();
- }
- widgets.add(widget);
- }
-
- if (widgets.isNotEmpty) {
- if (result.isEmpty) {
- result.add(_buildTitleRow(group.title, widgets.toList()));
+ pageController.nextPage(
+ duration: ErmineStyle.kScreenAnimationDuration,
+ curve: ErmineStyle.kScreenAnimationCurve,
+ );
+ uiStream.update(value);
+ } else if (action & QuickAction.cancel.$value > 0 ||
+ action & QuickAction.submit.$value > 0) {
+ model.detailStream?.update(value);
+ pageController.previousPage(
+ duration: ErmineStyle.kScreenAnimationDuration,
+ curve: ErmineStyle.kScreenAnimationCurve,
+ );
+ model.detailNotifier.value = null;
} else {
- result.add(_buildValueRow(widgets.toList()));
+ uiStream?.update(value);
}
- }
- }
- return result;
- }
-
- Widget _buildFromValue(Value value, void Function(Value) update) {
- if (value.$tag == ValueTag.button) {
- return _buildButton(value.button.label, () => update(value));
- }
- if (value.$tag == ValueTag.text) {
- return Text(value.text.text.toUpperCase());
- }
- if (value.$tag == ValueTag.progress) {
- return SizedBox(
- height: kItemHeight,
- width: kProgressBarWidth,
- child: ProgressBar(
- value: value.progress.value,
- onChange: (v) => update(Value.withProgress(
- ProgressValue(value: v, action: value.progress.action))),
- ),
- );
+ };
}
- if (value.$tag == ValueTag.grid) {
- int columns = value.grid.columns;
- int rows = value.grid.values.length ~/ columns;
- return Container(
- padding: EdgeInsets.only(left: 8),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: List<Widget>.generate(rows, (row) {
- return Padding(
- padding: EdgeInsets.symmetric(vertical: 4),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: List<Widget>.generate(value.grid.columns, (column) {
- final index = row * value.grid.columns + column;
- return Expanded(
- flex: column == 0 ? 2 : 1,
- child: Text(
- value.grid.values[index].text,
- textAlign: column == 0 ? TextAlign.start : TextAlign.end,
- ),
- );
- }),
- ),
- );
- }),
- ),
- );
- }
- if (value.$tag == ValueTag.icon) {
- return GestureDetector(
- child: Icon(
- IconData(
- value.icon.codePoint,
- fontFamily: value.icon.fontFamily ?? 'MaterialIcons',
+ return PageView(
+ controller: pageController,
+ physics: NeverScrollableScrollPhysics(),
+ children: <Widget>[
+ SingleChildScrollView(
+ padding: EdgeInsets.all(kPadding),
+ child: Column(
+ children: <Widget>[
+ _ManualStatusEntry(model),
+ for (final uiStream in [
+ model.datetime,
+ model.timezone,
+ model.volume,
+ model.brightness,
+ model.battery,
+ model.memory,
+ model.bluetooth,
+ model.weather,
+ ])
+ StatusEntry(
+ uiStream: uiStream,
+ onChange: _onChange(uiStream),
+ detailNotifier: model.detailNotifier,
+ // getSpec: spec,
+ ),
+ ],
),
- size: kIconHeight,
),
- onTap: () => update(value),
- );
- }
- if (value.$tag == ValueTag.graph) {
- return QuickGraph(value: value.graph.value, step: value.graph.step);
- }
- return Offstage();
+ DetailStatusEntry(
+ model: model,
+ onChange: _onChange(null),
+ )
+ ],
+ );
}
}
-Widget _buildValueRow(List<Widget> children) {
- return Wrap(
- children: children,
- alignment: WrapAlignment.end,
- spacing: kPadding,
- runSpacing: kPadding,
- );
-}
-
-Widget _buildTitleRow(String title, List<Widget> children) {
- return Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- textBaseline: TextBaseline.alphabetic,
- children: <Widget>[
- Container(
- width: kTitleWidth,
- child: Text(title.toUpperCase()),
- ),
- Expanded(
- child: _buildValueRow(children),
- ),
- ],
- );
-}
-
-Widget _buildButton(String label, void Function() onTap) {
- return GestureDetector(
- onTap: onTap,
- child: Container(
- height: kItemHeight,
- color: Colors.white,
- padding: EdgeInsets.symmetric(vertical: 0, horizontal: 2),
- child: Text(
- label.toUpperCase(),
- style: TextStyle(
- color: Colors.black,
- fontWeight: FontWeight.w400,
- ),
- ),
- ),
- );
-}
-
class _ManualStatusEntry extends StatelessWidget {
final StatusModel model;
@@ -237,13 +117,37 @@
height: kRowHeight,
child: Row(
children: <Widget>[
- _buildButton(Strings.restart, model.restartDevice),
+ StatusButton(Strings.restart, model.restartDevice),
Padding(padding: EdgeInsets.only(right: kPadding)),
- _buildButton(Strings.shutdown, model.shutdownDevice),
+ StatusButton(Strings.shutdown, model.shutdownDevice),
Spacer(),
- _buildButton(Strings.settings, model.launchSettings),
+ StatusButton(Strings.settings, model.launchSettings),
],
),
);
}
}
+
+int _actionFromValue(Value value) {
+ switch (value.$tag) {
+ case ValueTag.button:
+ return value.button.action;
+ case ValueTag.number:
+ return value.number.action;
+ case ValueTag.text:
+ return value.text.action;
+ case ValueTag.progress:
+ return value.progress.action;
+ case ValueTag.input:
+ return value.input.action;
+ case ValueTag.icon:
+ return value.input.action;
+ case ValueTag.grid:
+ return 0;
+ case ValueTag.graph:
+ return value.graph.action;
+ case ValueTag.list:
+ return 0;
+ }
+ return 0;
+}
diff --git a/session_shells/ermine/shell/lib/src/widgets/status/status_button.dart b/session_shells/ermine/shell/lib/src/widgets/status/status_button.dart
new file mode 100644
index 0000000..8a6f50b
--- /dev/null
+++ b/session_shells/ermine/shell/lib/src/widgets/status/status_button.dart
@@ -0,0 +1,33 @@
+// Copyright 2019 The Fuchsia 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 'package:flutter/material.dart';
+import 'status.dart';
+
+/// Defines a widget to render a Button for a status entry.
+class StatusButton extends StatelessWidget {
+ final String label;
+ final VoidCallback onTap;
+
+ const StatusButton(this.label, this.onTap);
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: onTap,
+ child: Container(
+ height: kItemHeight,
+ color: Colors.white,
+ padding: EdgeInsets.symmetric(vertical: 0, horizontal: 2),
+ child: Text(
+ label.toUpperCase(),
+ style: TextStyle(
+ color: Colors.black,
+ fontWeight: FontWeight.w400,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/session_shells/ermine/shell/lib/src/widgets/status/status_entry.dart b/session_shells/ermine/shell/lib/src/widgets/status/status_entry.dart
new file mode 100644
index 0000000..4738fe1
--- /dev/null
+++ b/session_shells/ermine/shell/lib/src/widgets/status/status_entry.dart
@@ -0,0 +1,45 @@
+// Copyright 2019 The Fuchsia 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 'package:fidl_fuchsia_ui_remotewidgets/fidl_async.dart';
+import 'package:flutter/material.dart';
+import 'package:quickui/uistream.dart';
+
+import 'spec_builder.dart';
+
+/// Defines a widget to represent a status entry in the Status shellement.
+class StatusEntry extends StatelessWidget {
+ final UiStream uiStream;
+ final ValueChanged<Value> onChange;
+ final ValueNotifier<UiStream> detailNotifier;
+ final _lastSpec = ValueNotifier<Spec>(null);
+
+ StatusEntry({this.uiStream, this.onChange, this.detailNotifier}) {
+ uiStream.listen();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return AnimatedBuilder(
+ animation: detailNotifier,
+ builder: (context, child) {
+ // If a [DetailStatusEntry] is displaying for this stream, freeze the
+ // UI for this stream until its shown.
+ return detailNotifier.value == uiStream
+ ? buildFromSpec(_lastSpec.value, onChange)
+ : StreamBuilder<Spec>(
+ stream: uiStream.stream,
+ initialData: uiStream.spec,
+ builder: (context, snapshot) {
+ if (!snapshot.hasData) {
+ return Offstage();
+ }
+ _lastSpec.value = snapshot.data;
+ return buildFromSpec(_lastSpec.value, onChange);
+ },
+ );
+ },
+ );
+ }
+}
diff --git a/session_shells/ermine/shell/meta/ermine.cmx b/session_shells/ermine/shell/meta/ermine.cmx
index a93b6e8..b3f234b 100644
--- a/session_shells/ermine/shell/meta/ermine.cmx
+++ b/session_shells/ermine/shell/meta/ermine.cmx
@@ -29,6 +29,8 @@
"fuchsia.power.BatteryManager",
"fuchsia.sys.Environment",
"fuchsia.sys.Launcher",
+ "fuchsia.timezone.TimeService",
+ "fuchsia.timezone.Timezone",
"fuchsia.ui.brightness.Control",
"fuchsia.ui.input.ImeService",
"fuchsia.ui.policy.Presenter",