Fetch all data directly from github

This commit is contained in:
Aaron Fischer 2019-07-06 00:44:20 +02:00
parent 35276cf974
commit 27d93e53ff
4 changed files with 240 additions and 107 deletions

View file

@ -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<String> syncErrors = new List<String>();
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<List<WarbandRoaster>> search() async {
syncErrors.clear();
Stream<Map<String, String>> 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<WarbandRoaster> roasters = new List<WarbandRoaster>();
// TODO: Read params from share preferences
await for (Map<String, String> 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<SyncState> 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;
}
}

View file

@ -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<RoasterWidget> {
Future<WarbandRoaster> roaster;
// TODO: Read this from SharedPreferences
String activePlayer = 'Aaron';
Future<List<WarbandRoaster>> roasters;
GitHubAdapter github = GitHubAdapter(
'Labernator/Mordheim', 'Mordheim-BorderTownBurning/Warband Rosters');
static Future<WarbandRoaster> 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<Widget> playerList() {
List<Widget> playerList(List<WarbandRoaster> roasters) {
List<Widget> 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<RoasterWidget> {
// 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<RoasterWidget> {
@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<RoasterWidget> {
case ConnectionState.active:
return Center(child: CircularProgressIndicator());
case ConnectionState.done:
WarbandRoaster roaster = snapshot.data;
List<WarbandRoaster> 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),
);
}
}));
}
}
);
});
}
}

View file

@ -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<String, String> _henchmenHeaderFromJson(String header) {
HashMap<String, String> 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<String, String> _heroHeaderFromJson(String header) {
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;
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<String, String> nameAndRace;
@JsonKey(ignore: true)
@ -167,7 +178,25 @@ class WarbandRoaster {
@JsonKey(name: 'henchmen')
final List<HenchmenGroup> 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);
}
factory WarbandRoaster.fromJson(yaml) => _$WarbandRoasterFromJson(yaml);
}

View file

@ -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;
}