More error handling

This commit is contained in:
Aaron Fischer 2019-08-01 00:05:42 +02:00
parent 82682b3d16
commit d34a0d4b32
7 changed files with 149 additions and 64 deletions

View file

@ -1,4 +1,3 @@
import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -59,6 +58,9 @@ class GitHubAdapter extends ChangeNotifier {
Stream<String> warbandFileStream() async* { Stream<String> warbandFileStream() async* {
// Get all files which could be potential warband files (end with // Get all files which could be potential warband files (end with
// mordheim.yml and contain the word "heros"). // mordheim.yml and contain the word "heros").
// TODO: Check rate limit
// TODO: Extract github access to separate class
http.Response response = await http.get( http.Response response = await http.get(
"https://api.github.com/search/code?q=heros+repo:" + "https://api.github.com/search/code?q=heros+repo:" +
repository + repository +
@ -101,11 +103,14 @@ class GitHubAdapter extends ChangeNotifier {
WarbandRoster roster = await fetchWarband(filePath); WarbandRoster roster = await fetchWarband(filePath);
Version latestVersion = await getLatestVersion(filePath); Version latestVersion = await getLatestVersion(filePath);
if (roster != null && latestVersion != null) {
roster.playerName = getPlayerNameFromFilePath(filePath); roster.playerName = getPlayerNameFromFilePath(filePath);
roster.version = latestVersion; roster.version = latestVersion;
roster.filePath = filePath; roster.filePath = filePath;
_rosters.add(roster); _rosters.add(roster);
notifyListeners();
}
//https://github.com/lesnitsky/flutter_localstorage/blob/master/example/lib/main.dart //https://github.com/lesnitsky/flutter_localstorage/blob/master/example/lib/main.dart
// FIXME: store it correctly // FIXME: store it correctly
@ -114,7 +119,6 @@ class GitHubAdapter extends ChangeNotifier {
// player['player'] + '-current-version', roster.currentVersion); // player['player'] + '-current-version', roster.currentVersion);
//storage.setItem( //storage.setItem(
// player['player'] + '-last-sync-version', roster.lastSyncVersion); // player['player'] + '-last-sync-version', roster.lastSyncVersion);
notifyListeners();
} }
} }
@ -127,7 +131,7 @@ class GitHubAdapter extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void update() async { Future<void> update() async {
_syncinProgress = true; _syncinProgress = true;
_syncErrors.clear(); _syncErrors.clear();
@ -136,10 +140,13 @@ class GitHubAdapter extends ChangeNotifier {
for (var i = 0; i < rosters.length; i++) { for (var i = 0; i < rosters.length; i++) {
Version newVersion = await getLatestVersion(rosters[i].filePath); Version newVersion = await getLatestVersion(rosters[i].filePath);
// File does not exist any more, we remove the roster
if (newVersion == null) { if (newVersion == null) {
rosters.removeAt(i);
continue; continue;
} }
// New version found, so we fetch the updated roster
if (newVersion.gitHash != rosters[i].version.gitHash) { if (newVersion.gitHash != rosters[i].version.gitHash) {
WarbandRoster newRoster = await fetchWarband(rosters[i].filePath); WarbandRoster newRoster = await fetchWarband(rosters[i].filePath);
@ -159,10 +166,6 @@ class GitHubAdapter extends ChangeNotifier {
_lastSync = DateTime.now(); _lastSync = DateTime.now();
_syncinProgress = false; _syncinProgress = false;
if (_syncErrors.length != 0) {
// TODO: Show sync errors.
}
notifyListeners(); notifyListeners();
} }
@ -187,7 +190,8 @@ class GitHubAdapter extends ChangeNotifier {
filePath); filePath);
if (response.statusCode != 200) { if (response.statusCode != 200) {
_syncErrors.add('Could not load the warband metadata from GitHub.'); _syncErrors
.add(filePath + ': Could not load the warband metadata from GitHub.');
return null; return null;
} }
@ -196,7 +200,8 @@ class GitHubAdapter extends ChangeNotifier {
try { try {
commits = jsonDecode(response.body); commits = jsonDecode(response.body);
} on FormatException catch (e) { } on FormatException catch (e) {
_syncErrors.add('Could not parse GitHub response. ' + e.toString()); _syncErrors
.add(filePath + ': Could not parse GitHub response. ' + e.toString());
return null; return null;
} }
@ -225,13 +230,15 @@ class GitHubAdapter extends ChangeNotifier {
// later (see below). // later (see below).
} }
if (response == null) {
return null;
}
try { try {
if (response != null) {
YamlMap yamlObject = loadYaml(response.body); YamlMap yamlObject = loadYaml(response.body);
return WarbandRoster.fromJson(yamlObject); return WarbandRoster.fromJson(yamlObject);
}
} catch (e) { } catch (e) {
_syncErrors.add(e.toString()); _syncErrors.add(filePath + ': ' + e.message);
} }
return null; return null;

View file

@ -10,7 +10,7 @@ part 'warband_roster.g.dart';
abstract class Unit { abstract class Unit {
static List<String> _splitListFromJson(String list) { static List<String> _splitListFromJson(String list) {
if (list == null) { if (list == null) {
return new List(); return [];
} }
return list.split(new RegExp(r" *, *")); return list.split(new RegExp(r" *, *"));
} }
@ -18,7 +18,14 @@ abstract class Unit {
static Stats _statsFromJson(String stats) { static Stats _statsFromJson(String stats) {
RegExp re = new RegExp( RegExp re = new RegExp(
r"\s*M([0-9]+[dD]*[6]*)\s*,\s*WS([0-9]+)\s*,\s*BS([0-9]+)\s*,\s*S([0-9]+)\s*,\s*T([0-9]+)\s*,\s*W([0-9]+)\s*,\s*I([0-9]+)\s*,\s*A([0-9]+)\s*,\s*Ld([0-9]+)\s*,\s*Sv([0-9\-]+)\s*"); r"\s*M([0-9]+[dD]*[6]*)\s*,\s*WS([0-9]+)\s*,\s*BS([0-9]+)\s*,\s*S([0-9]+)\s*,\s*T([0-9]+)\s*,\s*W([0-9]+)\s*,\s*I([0-9]+)\s*,\s*A([0-9]+)\s*,\s*Ld([0-9]+)\s*,\s*Sv([0-9\-]+)\s*");
var matches = re.allMatches(stats).toList().first; var matchList = re.allMatches(stats).toList();
if (matchList.isEmpty) {
throw FormatException(
'The stats "' + stats + '" are not in the right format.');
}
var matches = matchList.first;
return Stats( return Stats(
int.tryParse(matches.group(1)) ?? 0, int.tryParse(matches.group(1)) ?? 0,
int.tryParse(matches.group(2)) ?? 0, int.tryParse(matches.group(2)) ?? 0,
@ -35,7 +42,7 @@ abstract class Unit {
@JsonSerializable(nullable: true, anyMap: true, createToJson: false) @JsonSerializable(nullable: true, anyMap: true, createToJson: false)
class HenchmenGroup extends Unit { class HenchmenGroup extends Unit {
@JsonKey(name: 'group', fromJson: _henchmenHeaderFromJson) @JsonKey(name: 'group', fromJson: _henchmenHeaderFromJson, required: true)
final HashMap<String, String> header; final HashMap<String, String> header;
@JsonKey(ignore: true) @JsonKey(ignore: true)
String name; String name;
@ -46,16 +53,16 @@ class HenchmenGroup extends Unit {
@JsonKey(ignore: true) @JsonKey(ignore: true)
int experience; int experience;
@JsonKey(fromJson: Unit._statsFromJson) @JsonKey(fromJson: Unit._statsFromJson, required: true)
final Stats stats; final Stats stats;
@JsonKey(fromJson: Unit._splitListFromJson) @JsonKey(fromJson: Unit._splitListFromJson)
final List<String> weapons; final List<String> weapons;
@JsonKey(fromJson: Unit._splitListFromJson) @JsonKey(fromJson: Unit._splitListFromJson)
final List<String> amour; final List<String> armour;
HenchmenGroup(this.header, this.stats, this.weapons, this.amour) { HenchmenGroup(this.header, this.stats, this.weapons, this.armour) {
this.name = this.header['name']; this.name = this.header['name'];
this.type = this.header['type']; this.type = this.header['type'];
this.number = int.tryParse(this.header['number']) ?? 1; this.number = int.tryParse(this.header['number']) ?? 1;
@ -66,7 +73,15 @@ class HenchmenGroup extends Unit {
HashMap<String, String> h = new HashMap(); HashMap<String, String> h = new HashMap();
RegExp re = RegExp re =
new RegExp(r"([^\(]+)\(([0-9]+)x?\s+([^\)]+)\)\s*\[([0-9]+)XP\]\s*"); new RegExp(r"([^\(]+)\(([0-9]+)x?\s+([^\)]+)\)\s*\[([0-9]+)XP\]\s*");
var matches = re.allMatches(header).toList().first; var matchList = re.allMatches(header).toList();
if (matchList.isEmpty) {
throw FormatException('The henchmen group header "' +
header +
'" is not in the right format.');
}
var matches = matchList.first;
h['name'] = matches.group(1); h['name'] = matches.group(1);
h['number'] = matches.group(2); h['number'] = matches.group(2);
@ -81,7 +96,7 @@ class HenchmenGroup extends Unit {
@JsonSerializable(nullable: true, anyMap: true, createToJson: false) @JsonSerializable(nullable: true, anyMap: true, createToJson: false)
class Hero extends Unit { class Hero extends Unit {
@JsonKey(name: 'hero', fromJson: _heroHeaderFromJson) @JsonKey(name: 'hero', fromJson: _heroHeaderFromJson, required: true)
final HashMap<String, String> header; final HashMap<String, String> header;
@JsonKey(ignore: true) @JsonKey(ignore: true)
String name; String name;
@ -90,7 +105,7 @@ class Hero extends Unit {
@JsonKey(ignore: true) @JsonKey(ignore: true)
int experience; int experience;
@JsonKey(fromJson: Unit._statsFromJson) @JsonKey(fromJson: Unit._statsFromJson, required: true)
final Stats stats; final Stats stats;
@JsonKey(fromJson: Unit._splitListFromJson) @JsonKey(fromJson: Unit._splitListFromJson)
@ -100,14 +115,15 @@ class Hero extends Unit {
final List<String> weapons; final List<String> weapons;
@JsonKey(fromJson: Unit._splitListFromJson) @JsonKey(fromJson: Unit._splitListFromJson)
final List<String> amour; final List<String> armour;
@JsonKey(fromJson: Unit._splitListFromJson) @JsonKey(fromJson: Unit._splitListFromJson)
final List<String> rules; final List<String> rules;
@JsonKey(defaultValue: 0)
final int warbandaddition; final int warbandaddition;
Hero(this.stats, this.skilllists, this.weapons, this.amour, this.rules, Hero(this.stats, this.skilllists, this.weapons, this.armour, this.rules,
this.warbandaddition, this.header) { this.warbandaddition, this.header) {
this.name = this.header['name']; this.name = this.header['name'];
this.type = this.header['type']; this.type = this.header['type'];
@ -119,7 +135,14 @@ class Hero extends Unit {
static HashMap<String, String> _heroHeaderFromJson(String header) { static HashMap<String, String> _heroHeaderFromJson(String header) {
HashMap<String, String> h = new HashMap(); HashMap<String, String> h = new HashMap();
RegExp re = new RegExp(r"([^\(]+)\(([^\)]+)\)\s*\[([0-9]+)XP\]\s*"); RegExp re = new RegExp(r"([^\(]+)\(([^\)]+)\)\s*\[([0-9]+)XP\]\s*");
var matches = re.allMatches(header).toList().first; var matchList = re.allMatches(header).toList();
if (matchList.isEmpty) {
throw FormatException(
'The hero header "' + header + '" is not in the right format.');
}
var matches = matchList.first;
h['name'] = matches.group(1); h['name'] = matches.group(1);
h['type'] = matches.group(2); h['type'] = matches.group(2);
@ -169,7 +192,7 @@ class Version {
class WarbandRoster { class WarbandRoster {
/// Store the complete string of name and race. This will split up into the /// Store the complete string of name and race. This will split up into the
/// fields name and race. /// fields name and race.
@JsonKey(name: 'warband', fromJson: _warbandNameAndRace) @JsonKey(name: 'warband', fromJson: _warbandNameAndRace, required: true)
final HashMap<String, String> nameAndRace; final HashMap<String, String> nameAndRace;
@JsonKey(ignore: true) @JsonKey(ignore: true)
String name; String name;
@ -182,8 +205,10 @@ class WarbandRoster {
@JsonKey(name: 'campaign', defaultValue: 0) @JsonKey(name: 'campaign', defaultValue: 0)
final int campaignPoints; final int campaignPoints;
@JsonKey(required: true)
final String objective; final String objective;
@JsonKey(required: true)
final String alignment; final String alignment;
@JsonKey(defaultValue: '') @JsonKey(defaultValue: '')
@ -198,9 +223,10 @@ class WarbandRoster {
@JsonKey(defaultValue: '') @JsonKey(defaultValue: '')
final String equipment; final String equipment;
@JsonKey(required: true)
final List<Hero> heros; final List<Hero> heros;
@JsonKey(name: 'henchmen') @JsonKey(name: 'henchmen', required: true)
final List<HenchmenGroup> henchmenGroups; final List<HenchmenGroup> henchmenGroups;
/// The players name is not defined in the yml file. This will be added later /// The players name is not defined in the yml file. This will be added later
@ -241,9 +267,16 @@ class WarbandRoster {
HashMap<String, String> nr = new HashMap(); HashMap<String, String> nr = new HashMap();
RegExp re = new RegExp(r"(.*) \((.*)\)"); RegExp re = new RegExp(r"(.*) \((.*)\)");
var matches = re.allMatches(nameAndRace); var matchList = re.allMatches(nameAndRace).toList();
nr['name'] = matches.toList().first.group(1).toString();
nr['race'] = matches.toList().first.group(2).toString(); if (matchList.isEmpty) {
throw FormatException(
'Name and race "' + nameAndRace + '" are not in the right format.');
}
var matches = matchList.first;
nr['name'] = matches.group(1).toString();
nr['race'] = matches.group(2).toString();
return nr; return nr;
} }

View file

@ -7,27 +7,36 @@ part of 'warband_roster.dart';
// ************************************************************************** // **************************************************************************
HenchmenGroup _$HenchmenGroupFromJson(Map json) { HenchmenGroup _$HenchmenGroupFromJson(Map json) {
$checkKeys(json, requiredKeys: const ['group', 'stats']);
return HenchmenGroup( return HenchmenGroup(
HenchmenGroup._henchmenHeaderFromJson(json['group'] as String), HenchmenGroup._henchmenHeaderFromJson(json['group'] as String),
Unit._statsFromJson(json['stats'] as String), Unit._statsFromJson(json['stats'] as String),
Unit._splitListFromJson(json['weapons'] as String), Unit._splitListFromJson(json['weapons'] as String),
Unit._splitListFromJson(json['amour'] as String), Unit._splitListFromJson(json['armour'] as String),
); );
} }
Hero _$HeroFromJson(Map json) { Hero _$HeroFromJson(Map json) {
$checkKeys(json, requiredKeys: const ['hero', 'stats']);
return Hero( return Hero(
Unit._statsFromJson(json['stats'] as String), Unit._statsFromJson(json['stats'] as String),
Unit._splitListFromJson(json['skilllists'] as String), Unit._splitListFromJson(json['skilllists'] as String),
Unit._splitListFromJson(json['weapons'] as String), Unit._splitListFromJson(json['weapons'] as String),
Unit._splitListFromJson(json['amour'] as String), Unit._splitListFromJson(json['armour'] as String),
Unit._splitListFromJson(json['rules'] as String), Unit._splitListFromJson(json['rules'] as String),
json['warbandaddition'] as int, json['warbandaddition'] as int ?? 0,
Hero._heroHeaderFromJson(json['hero'] as String), Hero._heroHeaderFromJson(json['hero'] as String),
); );
} }
WarbandRoster _$WarbandRosterFromJson(Map json) { WarbandRoster _$WarbandRosterFromJson(Map json) {
$checkKeys(json, requiredKeys: const [
'warband',
'objective',
'alignment',
'heros',
'henchmen'
]);
return WarbandRoster( return WarbandRoster(
WarbandRoster._warbandNameAndRace(json['warband'] as String), WarbandRoster._warbandNameAndRace(json['warband'] as String),
json['campaign'] as int ?? 0, json['campaign'] as int ?? 0,

View file

@ -3,8 +3,6 @@ import 'package:preferences/preferences.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:toolheim/data/github_adapter.dart'; import 'package:toolheim/data/github_adapter.dart';
// TODO: Display possible errors here
class SettingsScreen extends StatelessWidget { class SettingsScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -73,7 +71,7 @@ class SettingsScreen extends StatelessWidget {
//String _path = 'Mordheim-BorderTownBurning/Warband Rosters'; //String _path = 'Mordheim-BorderTownBurning/Warband Rosters';
} }
Widget buildSyncErrors(BuildContext context) { static Widget buildSyncErrors(BuildContext context) {
List<Widget> syncErrors = new List(); List<Widget> syncErrors = new List();
GitHubAdapter github = Provider.of<GitHubAdapter>(context); GitHubAdapter github = Provider.of<GitHubAdapter>(context);

View file

@ -3,6 +3,8 @@ import 'package:provider/provider.dart';
import 'package:badges/badges.dart'; import 'package:badges/badges.dart';
import 'package:toolheim/data/github_adapter.dart'; import 'package:toolheim/data/github_adapter.dart';
import 'package:toolheim/data/warband_roster.dart'; import 'package:toolheim/data/warband_roster.dart';
import 'package:toolheim/screens/settings_screen.dart';
import 'package:url_launcher/url_launcher.dart';
class WarbandDrawerWidget extends StatelessWidget { class WarbandDrawerWidget extends StatelessWidget {
@override @override
@ -10,12 +12,20 @@ class WarbandDrawerWidget extends StatelessWidget {
GitHubAdapter github = Provider.of<GitHubAdapter>(context); GitHubAdapter github = Provider.of<GitHubAdapter>(context);
// No settings at all // No settings at all
if (github.repository == null) { String settingsText =
'There is no repository set up. Please open the settings and provide a valid GitHub repository.';
// Never fetched any data
if (github.activeRoster == null) {
settingsText =
'The repository is set, but no warbands are found. Try to search for warbands on the settings screen.';
}
if (github.repository == null || github.activeRoster == null) {
return Padding( return Padding(
padding: const EdgeInsets.only(top: 100, left: 30, right: 30), padding: const EdgeInsets.only(top: 100, left: 30, right: 30),
child: Column(children: <Widget>[ child: Column(children: <Widget>[
Text( Text(settingsText),
'There is no repository set up. Please open the settings and provide a valid GitHub repository.'),
FlatButton( FlatButton(
onPressed: () { onPressed: () {
Navigator.popAndPushNamed(context, '/settings'); Navigator.popAndPushNamed(context, '/settings');
@ -25,23 +35,17 @@ class WarbandDrawerWidget extends StatelessWidget {
style: TextStyle(color: Colors.blue), style: TextStyle(color: Colors.blue),
), ),
), ),
]),
);
}
// Never fetched any data
if (github.activeRoster == null) {
return Padding(
padding: const EdgeInsets.only(top: 100, left: 30, right: 30),
child: Column(children: <Widget>[
Text( Text(
'The repository is set, but no warbands are found. Try to search for warbands on the settings screen.'), 'If you have no clue what this app is all about, open the help screen and read the introduction.'),
FlatButton( FlatButton(
onPressed: () { onPressed: () async {
Navigator.popAndPushNamed(context, '/settings'); const url = '';
if (await canLaunch(url)) {
await launch(url);
}
}, },
child: Text( child: Text(
'Open Settings', 'Help',
style: TextStyle(color: Colors.blue), style: TextStyle(color: Colors.blue),
), ),
), ),
@ -57,7 +61,7 @@ class WarbandDrawerWidget extends StatelessWidget {
WarbandRoster activeroster = github.activeRoster; WarbandRoster activeroster = github.activeRoster;
List<WarbandRoster> rosters = github.rosters; List<WarbandRoster> rosters = github.rosters;
List<Widget> tiles = new List(); List<Widget> tiles = [];
// Show some stats for the own warband // Show some stats for the own warband
tiles.add(UserAccountsDrawerHeader( tiles.add(UserAccountsDrawerHeader(
@ -68,8 +72,26 @@ class WarbandDrawerWidget extends StatelessWidget {
color: Colors.white, color: Colors.white,
highlightColor: Colors.brown, highlightColor: Colors.brown,
tooltip: 'Refresh warbands', tooltip: 'Refresh warbands',
onPressed: github.update, onPressed: () async {
), await github.update();
if (github.syncErrors.length > 0) {
List<Widget> errors = github.syncErrors.map((error) {
return Text(error);
}).toList();
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('We have some errors while updating.'),
content: SettingsScreen.buildSyncErrors(context),
actions: <Widget>[
FlatButton(child: Text('Ok'), onPressed: () {})
]);
});
}
}),
IconButton( IconButton(
icon: Icon(Icons.settings), icon: Icon(Icons.settings),
color: Colors.white, color: Colors.white,
@ -78,6 +100,14 @@ class WarbandDrawerWidget extends StatelessWidget {
Navigator.popAndPushNamed(context, '/settings'); Navigator.popAndPushNamed(context, '/settings');
}, },
), ),
IconButton(
icon: Icon(Icons.help),
color: Colors.white,
highlightColor: Colors.brown,
onPressed: () {
Navigator.popAndPushNamed(context, '/help');
},
),
], ],
accountName: Text(activeroster.name), accountName: Text(activeroster.name),
accountEmail: Text(activeroster.race), accountEmail: Text(activeroster.race),

View file

@ -464,6 +464,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.6" version: "1.1.6"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

View file

@ -27,6 +27,7 @@ dependencies:
shared_preferences: shared_preferences:
preferences: preferences:
localstorage: localstorage:
url_launcher:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.