From 27d93e53ff5c104b6fe13cb8cf12e7953f984dc2 Mon Sep 17 00:00:00 2001 From: Aaron Fischer Date: Sat, 6 Jul 2019 00:44:20 +0200 Subject: [PATCH] Fetch all data directly from github --- mobile-app/lib/github_reader.dart | 115 ++++++++++++++ mobile-app/lib/main.dart | 145 ++++++++---------- ...rbandRoaster.dart => warband_roaster.dart} | 82 ++++++---- ...dRoaster.g.dart => warband_roaster.g.dart} | 5 +- 4 files changed, 240 insertions(+), 107 deletions(-) create mode 100644 mobile-app/lib/github_reader.dart rename mobile-app/lib/{WarbandRoaster.dart => warband_roaster.dart} (68%) rename mobile-app/lib/{WarbandRoaster.g.dart => warband_roaster.g.dart} (93%) diff --git a/mobile-app/lib/github_reader.dart b/mobile-app/lib/github_reader.dart new file mode 100644 index 0000000..5ffa78d --- /dev/null +++ b/mobile-app/lib/github_reader.dart @@ -0,0 +1,115 @@ +import 'dart:collection'; +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:toolheim/warband_roaster.dart'; +import 'package:yaml/yaml.dart'; + +enum SyncState { + unknown, + running, + success, + error, +} + +class GitHubAdapter { + // TODO: Store this in the SharedPreferences + final String repository; + final String path; + + static SyncState syncState = SyncState.unknown; + List syncErrors = new List(); + static DateTime lastSync; + + GitHubAdapter(this.repository, this.path); + + /// Search for warband files in the GitHub repository + /// + /// This method will search for matching files and check their content in a + /// subfolder (see fields [repository] and [path]). If the file + /// contain errors or can't read, a sync error message will be written into + /// the [syncErrors] list. + Future> search() async { + syncErrors.clear(); + + Stream> roasterStream() async* { + // Get all files which could be potential warband files (end with + // mordheim.yml and contain the word "heros"). + http.Response response = await http.get( + "https://api.github.com/search/code?q=heros+repo:" + + repository + + "+filename:mordheim.yml+path:\"" + + path + + "\""); + RegExp fileRegex = new RegExp(r"[a-zA-Z]+\.mordheim\.ya?ml"); + var resp = jsonDecode(response.body); + + for (var searchResult in resp['items']) { + if (fileRegex.hasMatch(searchResult['name'])) { + String completePath = searchResult['path']; + String playerName = + completePath.substring(path.length + 1).split('/').first; + + if (playerName == '') { + playerName = 'Lonely Recluse'; + } + + // Fetch last change and some metainformation of the file + http.Response response = await http.get( + "https://api.github.com/repos/" + + repository + + "/commits?path=" + + completePath); + var resp = jsonDecode(response.body); + var lastCommit = resp.first; + + // TODO: Add some error handling + + yield { + 'filePath': completePath.toString(), + 'shaHash': lastCommit['sha'], + 'player': playerName.toString(), + 'author': lastCommit['commit']['author']['name'], + 'date': lastCommit['commit']['committer']['date'], + 'message': lastCommit['commit']['message'] + }; + } + } + } + + List roasters = new List(); + // TODO: Read params from share preferences + await for (Map player in roasterStream()) { + http.Response response = await http.get( + "https://raw.githubusercontent.com/" + + repository + + '/master/' + + player['filePath']); + + try { + YamlMap yamlObject = loadYaml(response.body); + WarbandRoaster roaster = WarbandRoaster.fromJson(yamlObject); + roaster.playerName = player['player']; + roaster.author = player['author']; + roaster.commitDate = player['date']; + roaster.commitMessage = player['message']; + roaster.commitHash = player['shaHash'] + + roasters.add(roaster); + } catch (e) { + syncErrors.add(e.toString()); + } + } + + return roasters; + } + + Future update() async { + // TODO: Search for warband yml files + // TODO: Check if it is in the right format + // TODO: Store it into the database if valid + lastSync = DateTime.now(); + + return SyncState.success; + } +} diff --git a/mobile-app/lib/main.dart b/mobile-app/lib/main.dart index 5e7f576..8b0dcb0 100644 --- a/mobile-app/lib/main.dart +++ b/mobile-app/lib/main.dart @@ -1,8 +1,8 @@ import 'dart:collection'; import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'package:toolheim/WarbandRoaster.dart'; +import 'package:toolheim/github_reader.dart'; +import 'package:toolheim/warband_roaster.dart'; import 'package:yaml/yaml.dart'; void main() => runApp(Toolheim()); @@ -27,37 +27,26 @@ class RoasterWidget extends StatefulWidget { } class _RoasterWidgetState extends State { - Future roaster; + // TODO: Read this from SharedPreferences + String activePlayer = 'Aaron'; + Future> roasters; + GitHubAdapter github = GitHubAdapter( + 'Labernator/Mordheim', 'Mordheim-BorderTownBurning/Warband Rosters'); - static Future fetchWarband(urlPath) async { - http.Response response = await http.get(urlPath); - YamlMap yamlObject = loadYaml(response.body); - return WarbandRoaster.fromJson(yamlObject); - } - - void initState() { + Future initState() { super.initState(); - roaster = fetchWarband(players()['Aaron']); + roasters = github.search(); } - HashMap players() { - HashMap players = new HashMap(); - players['Aaron'] = 'https://raw.githubusercontent.com/Labernator/Mordheim/master/Mordheim-BorderTownBurning/Warband%20Rosters/Aaron/aaron.mordheim.yml'; - players['Kai'] = 'https://raw.githubusercontent.com/Labernator/Mordheim/master/Mordheim-BorderTownBurning/Warband%20Rosters/kai/kai.mordheim_post5.yml'; - players['Fabian'] = 'https://raw.githubusercontent.com/Labernator/Mordheim/master/Mordheim-BorderTownBurning/Warband%20Rosters/Fabian/fabian.mordheim.yml'; - players['Marius'] = 'https://raw.githubusercontent.com/Labernator/Mordheim/master/Mordheim-BorderTownBurning/Warband%20Rosters/Marius/Marius_Post_5.mordheim.yml'; - players['Stefan'] = 'https://raw.githubusercontent.com/Labernator/Mordheim/master/Mordheim-BorderTownBurning/Warband%20Rosters/Stefan/Pit%20Fighter.yml'; - - return players; - } - - List playerList() { + List playerList(List roasters) { List tiles = new List(); + WarbandRoaster myWarband = roasters.first; + // Show some stats for the own warband tiles.add(UserAccountsDrawerHeader( - accountName: Text('The Revolting Dwarfs'), - accountEmail: Text('Dwarf Rangers'), + accountName: Text(myWarband.name), + accountEmail: Text(myWarband.race), currentAccountPicture: CircleAvatar( child: Text('Aa'), ), @@ -65,16 +54,15 @@ class _RoasterWidgetState extends State { // TODO: Order Players on CP or rating - players().forEach((player, url) { + roasters.forEach((roaster) { tiles.add(ListTile( - title: Text(player.toString()), - leading: CircleAvatar( - child: Text('WB'), - ), - trailing: Chip( - label: Text('326 XP') - ) - )); + title: Text(roaster.name + '(' + roaster.playerName + ')'), + subtitle: Text(roaster.commitMessage), + isThreeLine: true, + trailing: Chip( + label: Text( + '326 XP', + )))); }); tiles.add(Divider()); @@ -85,7 +73,7 @@ class _RoasterWidgetState extends State { @override Widget build(BuildContext context) { return FutureBuilder( - future: roaster, + future: github.search(), builder: (BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: @@ -93,53 +81,54 @@ class _RoasterWidgetState extends State { case ConnectionState.active: return Center(child: CircularProgressIndicator()); case ConnectionState.done: - WarbandRoaster roaster = snapshot.data; + List roasters = snapshot.data; + + // TODO: Replace with router + WarbandRoaster warband = roasters.first; + return Scaffold( - appBar: AppBar( - title: Text(roaster.name), - ), - drawer: Drawer( - child: Column( - children: this.playerList() + appBar: AppBar( + title: Text(warband.name), ), - ), - body: ListView.builder( - itemCount: roaster.heros.length + roaster.henchmenGroups.length, - itemBuilder: (BuildContext context, int index) { - // TODO: Sort by initiative - if (index < roaster.heros.length) { - var hero = roaster.heros[index]; + drawer: Drawer( + child: Column(children: playerList(roasters)), + ), + body: ListView.builder( + itemCount: + warband.heros.length + warband.henchmenGroups.length, + itemBuilder: (BuildContext context, int index) { + // TODO: Sort by initiative + if (index < warband.heros.length) { + var hero = warband.heros[index]; - return ListTile( - title: Text(hero.name), - leading: CircleAvatar( - child: Text(hero.experience.toString()), - backgroundColor: Colors.green, - foregroundColor: Colors.greenAccent, - ), - subtitle: Text(hero.type), - ); - } else { - var henchmenGroup = roaster.henchmenGroups[index-roaster.heros.length]; + return ListTile( + title: Text(hero.name), + leading: CircleAvatar( + child: Text(hero.experience.toString()), + backgroundColor: Colors.green, + foregroundColor: Colors.greenAccent, + ), + subtitle: Text(hero.type), + ); + } else { + var henchmenGroup = warband + .henchmenGroups[index - warband.heros.length]; - return ListTile( - title: Text(henchmenGroup.name), - trailing: Chip( - label: Text(henchmenGroup.number.toString() + 'x') - ), - leading: CircleAvatar( - child: Text(henchmenGroup.experience.toString()), - backgroundColor: Colors.orange, - foregroundColor: Colors.white, - ), - subtitle: Text(henchmenGroup.type), - ); - } - } - ) - ); + return ListTile( + title: Text(henchmenGroup.name), + trailing: Chip( + label: Text( + henchmenGroup.number.toString() + 'x')), + leading: CircleAvatar( + child: Text(henchmenGroup.experience.toString()), + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + ), + subtitle: Text(henchmenGroup.type), + ); + } + })); } - } - ); + }); } } diff --git a/mobile-app/lib/WarbandRoaster.dart b/mobile-app/lib/warband_roaster.dart similarity index 68% rename from mobile-app/lib/WarbandRoaster.dart rename to mobile-app/lib/warband_roaster.dart index b07f75d..19ade68 100644 --- a/mobile-app/lib/WarbandRoaster.dart +++ b/mobile-app/lib/warband_roaster.dart @@ -2,7 +2,7 @@ import 'dart:collection'; import 'package:json_annotation/json_annotation.dart'; -part 'WarbandRoaster.g.dart'; +part 'warband_roaster.g.dart'; // flutter packages pub run build_runner build --delete-conflicting-outputs @@ -15,20 +15,20 @@ 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*"); + 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; return Stats( - int.tryParse(matches.group(1)) ?? 0, - int.tryParse(matches.group(2)) ?? 0, - int.tryParse(matches.group(3)) ?? 0, - int.tryParse(matches.group(4)) ?? 0, - int.tryParse(matches.group(5)) ?? 0, - int.tryParse(matches.group(6)) ?? 0, - int.tryParse(matches.group(7)) ?? 0, - int.tryParse(matches.group(8)) ?? 0, - int.tryParse(matches.group(9)) ?? 0, - int.tryParse(matches.group(10)) ?? 0 - ); + int.tryParse(matches.group(1)) ?? 0, + int.tryParse(matches.group(2)) ?? 0, + int.tryParse(matches.group(3)) ?? 0, + int.tryParse(matches.group(4)) ?? 0, + int.tryParse(matches.group(5)) ?? 0, + int.tryParse(matches.group(6)) ?? 0, + int.tryParse(matches.group(7)) ?? 0, + int.tryParse(matches.group(8)) ?? 0, + int.tryParse(matches.group(9)) ?? 0, + int.tryParse(matches.group(10)) ?? 0); } } @@ -63,7 +63,8 @@ class HenchmenGroup extends Unit { static HashMap _henchmenHeaderFromJson(String header) { HashMap h = new HashMap(); - RegExp re = new RegExp(r"([^\(]+)\(([0-9]+)x?\s+([^\)]+)\)\s*\[([0-9]+)XP\]\s*"); + RegExp re = + new RegExp(r"([^\(]+)\(([0-9]+)x?\s+([^\)]+)\)\s*\[([0-9]+)XP\]\s*"); var matches = re.allMatches(header).toList().first; h['name'] = matches.group(1); @@ -74,9 +75,7 @@ class HenchmenGroup extends Unit { return h; } - factory HenchmenGroup.fromJson(yaml) => - _$HenchmenGroupFromJson(yaml); - + factory HenchmenGroup.fromJson(yaml) => _$HenchmenGroupFromJson(yaml); } @JsonSerializable(nullable: true, anyMap: true, createToJson: false) @@ -107,18 +106,18 @@ class Hero extends Unit { final int warbandaddition; - Hero(this.stats, this.skilllists, this.weapons, this.amour, this.rules, this.warbandaddition, this.header) { + Hero(this.stats, this.skilllists, this.weapons, this.amour, this.rules, + this.warbandaddition, this.header) { this.name = this.header['name']; this.type = this.header['type']; this.experience = int.tryParse(this.header['experience']) ?? 0; } - - factory Hero.fromJson(yaml) => - _$HeroFromJson(yaml); + + factory Hero.fromJson(yaml) => _$HeroFromJson(yaml); static HashMap _heroHeaderFromJson(String header) { HashMap 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; h['name'] = matches.group(1); @@ -141,11 +140,23 @@ class Stats { final int leadership; final int save; - Stats(this.movement, this.weaponSkill, this.ballisticSkill, this.strength, this.toughtness, this.wounds, this.initiative, this.attacks, this.leadership, this.save); + Stats( + this.movement, + this.weaponSkill, + this.ballisticSkill, + this.strength, + this.toughtness, + this.wounds, + this.initiative, + this.attacks, + this.leadership, + this.save); } @JsonSerializable(nullable: true, anyMap: true, createToJson: false) class WarbandRoaster { + /// Store the complete string of name and race. This will split up into the + /// fields name and race. @JsonKey(name: 'warband', fromJson: _warbandNameAndRace) final HashMap nameAndRace; @JsonKey(ignore: true) @@ -167,7 +178,25 @@ class WarbandRoaster { @JsonKey(name: 'henchmen') final List henchmenGroups; - WarbandRoaster(this.nameAndRace, this.campaignPoints, this.objective, this.alignment, this.gc, this.shards, this.equipment, this.achievments, this.heros, this.henchmenGroups) { + /// The players name is not defined in the yml file. This will be added later + /// from the GitHubAdapter. + String playerName = ''; + String author = ''; + String commitDate = ''; + String commitMessage = ''; + String commitHash = ''; + + WarbandRoaster( + this.nameAndRace, + this.campaignPoints, + this.objective, + this.alignment, + this.gc, + this.shards, + this.equipment, + this.achievments, + this.heros, + this.henchmenGroups) { this.name = this.nameAndRace['name']; this.race = this.nameAndRace['race']; } @@ -183,6 +212,5 @@ class WarbandRoaster { return nr; } - factory WarbandRoaster.fromJson(yaml) => - _$WarbandRoasterFromJson(yaml); -} \ No newline at end of file + factory WarbandRoaster.fromJson(yaml) => _$WarbandRoasterFromJson(yaml); +} diff --git a/mobile-app/lib/WarbandRoaster.g.dart b/mobile-app/lib/warband_roaster.g.dart similarity index 93% rename from mobile-app/lib/WarbandRoaster.g.dart rename to mobile-app/lib/warband_roaster.g.dart index 69d262d..5bbc177 100644 --- a/mobile-app/lib/WarbandRoaster.g.dart +++ b/mobile-app/lib/warband_roaster.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'WarbandRoaster.dart'; +part of 'warband_roaster.dart'; // ************************************************************************** // JsonSerializableGenerator @@ -40,5 +40,6 @@ WarbandRoaster _$WarbandRoasterFromJson(Map json) { ?.toList(), (json['henchmen'] as List) ?.map((e) => e == null ? null : HenchmenGroup.fromJson(e)) - ?.toList()); + ?.toList()) + ..playerName = json['playerName'] as String; }