Skip to content

Commit

Permalink
Add first use dialog, check for NFC (0.6.1)
Browse files Browse the repository at this point in the history
- Added dialog to cache data on first use
- Added option to re-fetch cached data, if
  dataset has changed since last cache
- Show "check card value" option, only if NFC
  support is present
- Added icon for "Search" quick action
- Fixed about dialog text color
  • Loading branch information
jeffsieu committed Jul 29, 2020
1 parent 50fbf35 commit d5a8b3a
Show file tree
Hide file tree
Showing 11 changed files with 207 additions and 119 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 54 additions & 0 deletions lib/routes/fetch_data_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';

import '../utils/bus_api.dart';

class FetchDataDialog extends StatefulWidget {
const FetchDataDialog({@required this.isSetup });
final bool isSetup;

@override
State<StatefulWidget> createState() {
return _FetchDataDialogState();
}
}


class _FetchDataDialogState extends State<FetchDataDialog> {
double progress = 0.25;

@override
void initState() {
super.initState();
_fetchData();
}

@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.isSetup ? 'Performing first time setup' : 'Re-fetching cached data'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
LinearProgressIndicator(value: progress),
Text('${(progress * 100).toInt()}%'),
],
),
);
}

Future<void> _fetchData() async {
await BusAPI().fetchAndStoreBusStops();
setState(() {
progress += 0.25;
});
await BusAPI().fetchAndStoreBusServices();
setState(() {
progress += 0.40;
});
await BusAPI().fetchAndStoreBusServiceRoutes();
setState(() {
progress += 0.10;
});
Navigator.pop(context);
}
}
65 changes: 45 additions & 20 deletions lib/routes/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'package:expandable/expandable.dart';
import 'package:flutter_nfc_kit/flutter_nfc_kit.dart';
import 'package:location/location.dart';
import 'package:quick_actions/quick_actions.dart';
import 'package:shimmer/shimmer.dart';
Expand All @@ -11,6 +12,7 @@ import '../models/bus.dart';
import '../models/bus_stop.dart';
import '../models/user_route.dart';
import '../routes/add_route_page.dart';
import '../routes/fetch_data_dialog.dart';
import '../routes/route_page.dart';
import '../routes/scan_card_page.dart';
import '../routes/settings_page.dart';
Expand Down Expand Up @@ -51,14 +53,15 @@ class _HomePageState extends BottomSheetPageState<HomePage> {
@override
void initState() {
super.initState();
showSetupDialog();
final QuickActions quickActions = QuickActions();
quickActions.initialize((String shortcutType) {
if (shortcutType == 'action_search') {
_pushSearchRoute();
}
});
quickActions.setShortcutItems(<ShortcutItem>[
const ShortcutItem(type: 'action_search', localizedTitle: 'Search', icon: 'icon_search'),
const ShortcutItem(type: 'action_search', localizedTitle: 'Search', icon: 'ic_shortcut_search'),
]);

_bottomNavIndex = 0;
Expand All @@ -68,6 +71,22 @@ class _HomePageState extends BottomSheetPageState<HomePage> {
canScroll = true;
}

Future<void> showSetupDialog() async {
final bool cachedBusStops = await areBusStopsCached();
final bool cachedBusServices = await areBusServicesCached();
final bool cachedBusServiceRoutes = await areBusServiceRoutesCached();
final bool isFullyCached = cachedBusStops && cachedBusServices && cachedBusServiceRoutes;
if (!isFullyCached) {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return const FetchDataDialog(isSetup: true);
},
);
}
}

@override
Widget build(BuildContext context) {
buildSheet(hasAppBar: false);
Expand Down Expand Up @@ -171,26 +190,32 @@ class _HomePageState extends BottomSheetPageState<HomePage> {
icon: Icon(Icons.map, color: Theme.of(context).hintColor),
onPressed: _pushSearchRouteWithMap,
),
PopupMenuButton<String>(
tooltip: 'More',
icon: Icon(Icons.more_vert, color: Theme.of(context).hintColor),
onSelected: (String item) {
if (item == 'Settings') {
_pushSettingsRoute();
} else if (item == 'Check card value') {
_pushScanCardRoute();
}
FutureBuilder<NFCAvailability>(
future: FlutterNfcKit.nfcAvailability,
builder: (BuildContext context, AsyncSnapshot<NFCAvailability> snapshot) {
return PopupMenuButton<String>(
tooltip: 'More',
icon: Icon(Icons.more_vert, color: Theme.of(context).hintColor),
onSelected: (String item) {
if (item == 'Settings') {
_pushSettingsRoute();
} else if (item == 'Check card value') {
_pushScanCardRoute();
}
},
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
if (snapshot.hasData && snapshot.data == NFCAvailability.available)
const PopupMenuItem<String>(
child: Text('Check card value'),
value: 'Check card value',
),
const PopupMenuItem<String>(
child: Text('Settings'),
value: 'Settings',
),
],
);
},
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
const PopupMenuItem<String>(
child: Text('Check card value'),
value: 'Check card value',
),
const PopupMenuItem<String>(
child: Text('Settings'),
value: 'Settings',
),
],
),
],
),
Expand Down
24 changes: 22 additions & 2 deletions lib/routes/settings_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:url_launcher/url_launcher.dart';

import '../main.dart';
import '../utils/database_utils.dart';
import 'fetch_data_dialog.dart';

class SettingsPage extends StatefulWidget {
static const String _kThemeLabelSystem = 'System';
Expand Down Expand Up @@ -40,6 +41,7 @@ class SettingsPageState extends State<SettingsPage> {
body: ListView(
children: <Widget>[
_buildThemeTile(),
_buildRefreshDataTile(),
_buildAboutTile(),
],
),
Expand Down Expand Up @@ -101,15 +103,16 @@ class SettingsPageState extends State<SettingsPage> {
height: IconTheme.of(context).size * 2,
),
applicationName: 'Stops',
applicationVersion: '0.6.0',
applicationVersion: '0.6.1',
children: <Widget>[
RichText(
text: TextSpan(
text: 'Made by ',
style: Theme.of(context).textTheme.bodyText2,
children: <TextSpan>[
TextSpan(
text: 'Jeff Sieu',
style: TextStyle(color: Theme.of(context).accentColor),
style: TextStyle(color: Theme.of(context).colorScheme.secondary),
recognizer: TapGestureRecognizer()..onTap = () async {
const String url = 'https://github.com/jeffsieu';
if (await canLaunch(url)) {
Expand All @@ -129,6 +132,23 @@ class SettingsPageState extends State<SettingsPage> {
);
}

Widget _buildRefreshDataTile() {
return ListTile(
title: const Text('Refresh cached data'),
subtitle: const Text('Select if there are missing stops/services'),
leading: const Icon(Icons.update),
onTap: () {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return const FetchDataDialog(isSetup: false);
},
);
},
);
}

String _getThemeLabel(ThemeMode themeMode) {
switch (themeMode) {
case ThemeMode.system:
Expand Down
119 changes: 57 additions & 62 deletions lib/utils/bus_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'dart:convert';
import 'dart:io';

import 'package:flutter/services.dart';
import 'package:sqflite/sqflite.dart';

import '../models/bus_service.dart';
import '../models/bus_stop.dart';
Expand Down Expand Up @@ -70,8 +69,6 @@ class BusAPI {

bool _areBusStopsLoaded = false;
bool _areBusServicesLoaded = false;
int _arrivalSkip = 0;
int _servicesSkip = 0;

/*
* Load LTA API key from secrets.json file in root directory
Expand All @@ -93,22 +90,7 @@ class BusAPI {
Stream<List<BusStop>> busStopsStream() {
StreamController<List<BusStop>> controller;
Future<void> onListen() async {
if (!await areBusStopsCached()) {
while (!_areBusStopsLoaded) {
final String result = await _fetchBusStopList(_arrivalSkip);
final List<dynamic> resultList = jsonDecode(result)['value'];
_busStops.addAll(resultList.map((dynamic busStopJson) =>
BusStop.fromJson(busStopJson)));

if (resultList.length != 500) {
_areBusStopsLoaded = true;
} else {
controller.add(_busStops);
_arrivalSkip += 500;
}
}
cacheBusStops(_busStops);
} else if (!_areBusStopsLoaded){
if (!_areBusStopsLoaded){
_busStops.addAll(await getCachedBusStops());
_areBusStopsLoaded = true;
}
Expand All @@ -121,23 +103,9 @@ class BusAPI {

Stream<List<BusService>> busServicesStream() {
StreamController<List<BusService>> controller;

Future<void> onListen() async {
if (!await areBusServicesCached()) {
while (!_areBusServicesLoaded) {
final String result = await _fetchBusServiceList(_servicesSkip);
final List<dynamic> resultList = jsonDecode(result)['value'];
_busServices.addAll(resultList.map((dynamic busServiceJson) =>
BusService.fromJson(busServiceJson)));

if (resultList.length != 500) {
_areBusServicesLoaded = true;
} else {
controller.add(_busServices);
_servicesSkip += 500;
}
}
cacheBusServices(_busServices);
} else if (!_areBusServicesLoaded){
if (!_areBusServicesLoaded){
_busServices.addAll(await getCachedBusServices());
_areBusServicesLoaded = true;
}
Expand Down Expand Up @@ -207,47 +175,74 @@ class BusAPI {
}
}

Future<String> _fetchAsString(String url, int skip, [String extraParams = '']) async {
final HttpClientRequest request = await HttpClient().getUrl(Uri.parse('$_kRootUrl$url?\$skip=$skip$extraParams'));
request.headers.set(_kApiTag, _kApiKey);
request.headers.set('Content-Type', 'application/json');
Future<String> _fetchAsString(String url, int skip, [String extraParams = '']) async {
final HttpClientRequest request = await HttpClient().getUrl(Uri.parse('$_kRootUrl$url?\$skip=$skip$extraParams'));
request.headers.set(_kApiTag, _kApiKey);
request.headers.set('Content-Type', 'application/json');

final HttpClientResponse response = await request.close();
final Future<String> content = utf8.decodeStream(response);
return content;
final HttpClientResponse response = await request.close();
final Future<String> content = utf8.decodeStream(response);
return content;
}

Future<String> _fetchBusStopList(int skip) async {
return _fetchAsString(_kGetBusStopsUrl, skip);
Future<List<T>> _fetchAsList<T>(String url, T function(dynamic json)) async {
int skip = 0;
const int concurrentCount = 6;
final List<T> resultList = <T>[];
bool atEndOfList = false;
while (!atEndOfList) {
final List<Future<String>> futures = <Future<String>>[];
for (int i = 0; i < concurrentCount; i++) {
futures.add(_fetchAsString(url, skip));
skip += 500;
}
final List<String> results = await Future.wait(futures);
for (String result in results) {
try {
final List<dynamic> rawList = jsonDecode(result)['value'];
if (rawList == null || rawList.isEmpty)
break;
resultList.addAll(rawList.map<T>(function));
if (rawList.length < 500) {
atEndOfList = true;
break;
}
} on FormatException {
continue;
}
}
}
return resultList;
}

Future<List<BusStop>> _fetchBusStopList() async {
return _fetchAsList(_kGetBusStopsUrl, BusStop.fromJson);
}

Future<String> _fetchBusStopArrivalList(String busStopCode) async {
return _fetchAsString(_kGetBusStopArrivalUrl, 0, '&BusStopCode=' + busStopCode);
}

Future<String> _fetchBusServiceList(int skip) async {
return _fetchAsString(_kGetBusServicesUrl, skip);
Future<List<BusService>> _fetchBusServiceList() async {
return _fetchAsList(_kGetBusServicesUrl, BusService.fromJson);
}

Future<String> _fetchBusServiceRouteList(int skip) async {
return _fetchAsString(_kGetBusRoutesUrl, skip);
Future<List<Map<String, dynamic>>> _fetchBusServiceRouteList() async {
return _fetchAsList(_kGetBusRoutesUrl, busServiceRouteStopToJson);
}

Future<void> fetchAndStoreBusServiceRoutes() async {
int skip = 0;
List<dynamic> resultList;
Future<void> fetchAndStoreBusStops() async {
final List<BusStop> busStops = await _fetchBusStopList();
cacheBusStops(busStops);
}

final Batch batch = await beginBatchTransaction();
Future<void> fetchAndStoreBusServices() async {
final List<BusService> busServices = await _fetchBusServiceList();
cacheBusServices(busServices);
}

do {
final String result = await _fetchBusServiceRouteList(skip);
resultList = jsonDecode(result)['value'];
for (dynamic busStop in resultList) {
cacheBusServiceRouteStop(busStop, batch);
}
skip += 500;
} while (resultList.isNotEmpty);
await batch.commit(noResult: true);
await setBusServiceRoutesCached();
Future<void> fetchAndStoreBusServiceRoutes() async {
final List<Map<String, dynamic>> busServiceRoutesRaw = await _fetchBusServiceRouteList();
cacheBusServiceRoutes(busServiceRoutesRaw);
}
}
Loading

0 comments on commit d5a8b3a

Please sign in to comment.