diff --git a/mobile-app/lib/data/github_adapter.dart b/mobile-app/lib/data/github_adapter.dart index 217cc0b..191c4f7 100644 --- a/mobile-app/lib/data/github_adapter.dart +++ b/mobile-app/lib/data/github_adapter.dart @@ -1,4 +1,3 @@ -import 'dart:collection'; import 'dart:convert'; import 'package:flutter/material.dart'; @@ -59,6 +58,9 @@ class GitHubAdapter extends ChangeNotifier { Stream warbandFileStream() async* { // Get all files which could be potential warband files (end with // mordheim.yml and contain the word "heros"). + + // TODO: Check rate limit + // TODO: Extract github access to separate class http.Response response = await http.get( "https://api.github.com/search/code?q=heros+repo:" + repository + @@ -101,11 +103,14 @@ class GitHubAdapter extends ChangeNotifier { WarbandRoster roster = await fetchWarband(filePath); Version latestVersion = await getLatestVersion(filePath); - roster.playerName = getPlayerNameFromFilePath(filePath); - roster.version = latestVersion; - roster.filePath = filePath; + if (roster != null && latestVersion != null) { + roster.playerName = getPlayerNameFromFilePath(filePath); + roster.version = latestVersion; + roster.filePath = filePath; - _rosters.add(roster); + _rosters.add(roster); + notifyListeners(); + } //https://github.com/lesnitsky/flutter_localstorage/blob/master/example/lib/main.dart // FIXME: store it correctly @@ -114,7 +119,6 @@ class GitHubAdapter extends ChangeNotifier { // player['player'] + '-current-version', roster.currentVersion); //storage.setItem( // player['player'] + '-last-sync-version', roster.lastSyncVersion); - notifyListeners(); } } @@ -127,7 +131,7 @@ class GitHubAdapter extends ChangeNotifier { notifyListeners(); } - void update() async { + Future update() async { _syncinProgress = true; _syncErrors.clear(); @@ -136,10 +140,13 @@ class GitHubAdapter extends ChangeNotifier { for (var i = 0; i < rosters.length; i++) { Version newVersion = await getLatestVersion(rosters[i].filePath); + // File does not exist any more, we remove the roster if (newVersion == null) { + rosters.removeAt(i); continue; } + // New version found, so we fetch the updated roster if (newVersion.gitHash != rosters[i].version.gitHash) { WarbandRoster newRoster = await fetchWarband(rosters[i].filePath); @@ -159,10 +166,6 @@ class GitHubAdapter extends ChangeNotifier { _lastSync = DateTime.now(); _syncinProgress = false; - if (_syncErrors.length != 0) { - // TODO: Show sync errors. - } - notifyListeners(); } @@ -187,7 +190,8 @@ class GitHubAdapter extends ChangeNotifier { filePath); 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; } @@ -196,7 +200,8 @@ class GitHubAdapter extends ChangeNotifier { try { commits = jsonDecode(response.body); } 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; } @@ -225,13 +230,15 @@ class GitHubAdapter extends ChangeNotifier { // later (see below). } + if (response == null) { + return null; + } + try { - if (response != null) { - YamlMap yamlObject = loadYaml(response.body); - return WarbandRoster.fromJson(yamlObject); - } + YamlMap yamlObject = loadYaml(response.body); + return WarbandRoster.fromJson(yamlObject); } catch (e) { - _syncErrors.add(e.toString()); + _syncErrors.add(filePath + ': ' + e.message); } return null; diff --git a/mobile-app/lib/data/warband_roster.dart b/mobile-app/lib/data/warband_roster.dart index 19b9156..013d421 100644 --- a/mobile-app/lib/data/warband_roster.dart +++ b/mobile-app/lib/data/warband_roster.dart @@ -10,7 +10,7 @@ part 'warband_roster.g.dart'; abstract class Unit { static List _splitListFromJson(String list) { if (list == null) { - return new List(); + return []; } return list.split(new RegExp(r" *, *")); } @@ -18,7 +18,14 @@ abstract class Unit { static Stats _statsFromJson(String stats) { 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*"); - 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( int.tryParse(matches.group(1)) ?? 0, int.tryParse(matches.group(2)) ?? 0, @@ -35,7 +42,7 @@ abstract class Unit { @JsonSerializable(nullable: true, anyMap: true, createToJson: false) class HenchmenGroup extends Unit { - @JsonKey(name: 'group', fromJson: _henchmenHeaderFromJson) + @JsonKey(name: 'group', fromJson: _henchmenHeaderFromJson, required: true) final HashMap header; @JsonKey(ignore: true) String name; @@ -46,16 +53,16 @@ class HenchmenGroup extends Unit { @JsonKey(ignore: true) int experience; - @JsonKey(fromJson: Unit._statsFromJson) + @JsonKey(fromJson: Unit._statsFromJson, required: true) final Stats stats; @JsonKey(fromJson: Unit._splitListFromJson) final List weapons; @JsonKey(fromJson: Unit._splitListFromJson) - final List amour; + final List 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.type = this.header['type']; this.number = int.tryParse(this.header['number']) ?? 1; @@ -66,7 +73,15 @@ class HenchmenGroup extends Unit { HashMap h = new HashMap(); RegExp re = 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['number'] = matches.group(2); @@ -81,7 +96,7 @@ class HenchmenGroup extends Unit { @JsonSerializable(nullable: true, anyMap: true, createToJson: false) class Hero extends Unit { - @JsonKey(name: 'hero', fromJson: _heroHeaderFromJson) + @JsonKey(name: 'hero', fromJson: _heroHeaderFromJson, required: true) final HashMap header; @JsonKey(ignore: true) String name; @@ -90,7 +105,7 @@ class Hero extends Unit { @JsonKey(ignore: true) int experience; - @JsonKey(fromJson: Unit._statsFromJson) + @JsonKey(fromJson: Unit._statsFromJson, required: true) final Stats stats; @JsonKey(fromJson: Unit._splitListFromJson) @@ -100,14 +115,15 @@ class Hero extends Unit { final List weapons; @JsonKey(fromJson: Unit._splitListFromJson) - final List amour; + final List armour; @JsonKey(fromJson: Unit._splitListFromJson) final List rules; + @JsonKey(defaultValue: 0) 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.name = this.header['name']; this.type = this.header['type']; @@ -119,7 +135,14 @@ class Hero extends Unit { static HashMap _heroHeaderFromJson(String header) { HashMap h = new HashMap(); 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['type'] = matches.group(2); @@ -169,7 +192,7 @@ class Version { class WarbandRoster { /// Store the complete string of name and race. This will split up into the /// fields name and race. - @JsonKey(name: 'warband', fromJson: _warbandNameAndRace) + @JsonKey(name: 'warband', fromJson: _warbandNameAndRace, required: true) final HashMap nameAndRace; @JsonKey(ignore: true) String name; @@ -182,8 +205,10 @@ class WarbandRoster { @JsonKey(name: 'campaign', defaultValue: 0) final int campaignPoints; + @JsonKey(required: true) final String objective; + @JsonKey(required: true) final String alignment; @JsonKey(defaultValue: '') @@ -198,9 +223,10 @@ class WarbandRoster { @JsonKey(defaultValue: '') final String equipment; + @JsonKey(required: true) final List heros; - @JsonKey(name: 'henchmen') + @JsonKey(name: 'henchmen', required: true) final List henchmenGroups; /// The players name is not defined in the yml file. This will be added later @@ -241,9 +267,16 @@ class WarbandRoster { HashMap nr = new HashMap(); RegExp re = new RegExp(r"(.*) \((.*)\)"); - var matches = re.allMatches(nameAndRace); - nr['name'] = matches.toList().first.group(1).toString(); - nr['race'] = matches.toList().first.group(2).toString(); + var matchList = re.allMatches(nameAndRace).toList(); + + 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; } diff --git a/mobile-app/lib/data/warband_roster.g.dart b/mobile-app/lib/data/warband_roster.g.dart index 4340256..cee83e3 100644 --- a/mobile-app/lib/data/warband_roster.g.dart +++ b/mobile-app/lib/data/warband_roster.g.dart @@ -7,27 +7,36 @@ part of 'warband_roster.dart'; // ************************************************************************** HenchmenGroup _$HenchmenGroupFromJson(Map json) { + $checkKeys(json, requiredKeys: const ['group', 'stats']); return HenchmenGroup( HenchmenGroup._henchmenHeaderFromJson(json['group'] as String), Unit._statsFromJson(json['stats'] as String), Unit._splitListFromJson(json['weapons'] as String), - Unit._splitListFromJson(json['amour'] as String), + Unit._splitListFromJson(json['armour'] as String), ); } Hero _$HeroFromJson(Map json) { + $checkKeys(json, requiredKeys: const ['hero', 'stats']); return Hero( Unit._statsFromJson(json['stats'] as String), Unit._splitListFromJson(json['skilllists'] 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), - json['warbandaddition'] as int, + json['warbandaddition'] as int ?? 0, Hero._heroHeaderFromJson(json['hero'] as String), ); } WarbandRoster _$WarbandRosterFromJson(Map json) { + $checkKeys(json, requiredKeys: const [ + 'warband', + 'objective', + 'alignment', + 'heros', + 'henchmen' + ]); return WarbandRoster( WarbandRoster._warbandNameAndRace(json['warband'] as String), json['campaign'] as int ?? 0, diff --git a/mobile-app/lib/screens/settings_screen.dart b/mobile-app/lib/screens/settings_screen.dart index 99aa7fe..94d7429 100644 --- a/mobile-app/lib/screens/settings_screen.dart +++ b/mobile-app/lib/screens/settings_screen.dart @@ -3,8 +3,6 @@ import 'package:preferences/preferences.dart'; import 'package:provider/provider.dart'; import 'package:toolheim/data/github_adapter.dart'; -// TODO: Display possible errors here - class SettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { @@ -73,7 +71,7 @@ class SettingsScreen extends StatelessWidget { //String _path = 'Mordheim-BorderTownBurning/Warband Rosters'; } - Widget buildSyncErrors(BuildContext context) { + static Widget buildSyncErrors(BuildContext context) { List syncErrors = new List(); GitHubAdapter github = Provider.of(context); diff --git a/mobile-app/lib/widgets/warband_drawer_widget.dart b/mobile-app/lib/widgets/warband_drawer_widget.dart index 45b51f6..946179f 100644 --- a/mobile-app/lib/widgets/warband_drawer_widget.dart +++ b/mobile-app/lib/widgets/warband_drawer_widget.dart @@ -3,6 +3,8 @@ import 'package:provider/provider.dart'; import 'package:badges/badges.dart'; import 'package:toolheim/data/github_adapter.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 { @override @@ -10,12 +12,20 @@ class WarbandDrawerWidget extends StatelessWidget { GitHubAdapter github = Provider.of(context); // 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( padding: const EdgeInsets.only(top: 100, left: 30, right: 30), child: Column(children: [ - Text( - 'There is no repository set up. Please open the settings and provide a valid GitHub repository.'), + Text(settingsText), FlatButton( onPressed: () { Navigator.popAndPushNamed(context, '/settings'); @@ -25,23 +35,17 @@ class WarbandDrawerWidget extends StatelessWidget { 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: [ 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( - onPressed: () { - Navigator.popAndPushNamed(context, '/settings'); + onPressed: () async { + const url = ''; + if (await canLaunch(url)) { + await launch(url); + } }, child: Text( - 'Open Settings', + 'Help', style: TextStyle(color: Colors.blue), ), ), @@ -57,19 +61,37 @@ class WarbandDrawerWidget extends StatelessWidget { WarbandRoster activeroster = github.activeRoster; List rosters = github.rosters; - List tiles = new List(); + List tiles = []; // Show some stats for the own warband tiles.add(UserAccountsDrawerHeader( margin: const EdgeInsets.all(0), otherAccountsPictures: [ IconButton( - icon: Icon(Icons.refresh), - color: Colors.white, - highlightColor: Colors.brown, - tooltip: 'Refresh warbands', - onPressed: github.update, - ), + icon: Icon(Icons.refresh), + color: Colors.white, + highlightColor: Colors.brown, + tooltip: 'Refresh warbands', + onPressed: () async { + await github.update(); + + if (github.syncErrors.length > 0) { + List 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: [ + FlatButton(child: Text('Ok'), onPressed: () {}) + ]); + }); + } + }), IconButton( icon: Icon(Icons.settings), color: Colors.white, @@ -78,6 +100,14 @@ class WarbandDrawerWidget extends StatelessWidget { Navigator.popAndPushNamed(context, '/settings'); }, ), + IconButton( + icon: Icon(Icons.help), + color: Colors.white, + highlightColor: Colors.brown, + onPressed: () { + Navigator.popAndPushNamed(context, '/help'); + }, + ), ], accountName: Text(activeroster.name), accountEmail: Text(activeroster.race), diff --git a/mobile-app/pubspec.lock b/mobile-app/pubspec.lock index a0094e8..f7471a9 100644 --- a/mobile-app/pubspec.lock +++ b/mobile-app/pubspec.lock @@ -464,6 +464,13 @@ packages: url: "https://pub.dartlang.org" source: hosted 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: dependency: transitive description: diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 518950c..5e30b2f 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: shared_preferences: preferences: localstorage: + url_launcher: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons.