2019-07-06 00:44:20 +02:00
|
|
|
import 'dart:convert';
|
|
|
|
|
2019-07-10 14:16:13 +02:00
|
|
|
import 'package:flutter/material.dart';
|
2019-07-06 00:44:20 +02:00
|
|
|
import 'package:http/http.dart' as http;
|
2019-07-16 23:37:18 +02:00
|
|
|
import 'package:preferences/preferences.dart';
|
2019-07-25 13:37:31 +02:00
|
|
|
import 'package:localstorage/localstorage.dart';
|
2019-07-06 00:44:20 +02:00
|
|
|
import 'package:yaml/yaml.dart';
|
2019-07-25 13:37:31 +02:00
|
|
|
import 'package:toolheim/data/warband_roster.dart';
|
2019-07-06 00:44:20 +02:00
|
|
|
|
2019-07-10 14:16:13 +02:00
|
|
|
class GitHubAdapter extends ChangeNotifier {
|
2019-07-25 13:37:31 +02:00
|
|
|
final LocalStorage storage = new LocalStorage('rosters');
|
|
|
|
|
2019-07-10 14:16:13 +02:00
|
|
|
List<String> _syncErrors = new List<String>();
|
|
|
|
DateTime _lastSync;
|
|
|
|
|
2019-07-16 23:37:18 +02:00
|
|
|
List<WarbandRoster> _rosters = [];
|
2019-07-30 00:30:39 +02:00
|
|
|
String _activeRosterFilePath;
|
2019-07-06 00:44:20 +02:00
|
|
|
|
2019-07-16 23:37:18 +02:00
|
|
|
String get repository => PrefService.getString('repository');
|
|
|
|
String get path => PrefService.getString('path');
|
|
|
|
|
2019-08-02 00:56:04 +02:00
|
|
|
bool _syncInProgress = false;
|
|
|
|
bool get isSyncInProgress => _syncInProgress;
|
2019-07-06 00:44:20 +02:00
|
|
|
|
2019-07-10 14:16:13 +02:00
|
|
|
DateTime get lastSync => _lastSync;
|
2019-07-16 23:37:18 +02:00
|
|
|
List<String> get syncErrors => _syncErrors;
|
2019-07-10 14:16:13 +02:00
|
|
|
|
2019-07-30 00:30:39 +02:00
|
|
|
List<WarbandRoster> get rosters => _rosters;
|
2019-07-12 00:30:57 +02:00
|
|
|
|
2019-07-30 00:30:39 +02:00
|
|
|
WarbandRoster get activeRoster {
|
|
|
|
if (_rosters.length == 0) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_activeRosterFilePath == null) {
|
|
|
|
return _rosters.first;
|
|
|
|
}
|
|
|
|
return _rosters.firstWhere(
|
|
|
|
(roster) => roster.filePath == _activeRosterFilePath,
|
|
|
|
orElse: () => _rosters.first);
|
|
|
|
}
|
2019-07-12 00:30:57 +02:00
|
|
|
|
2019-07-27 00:09:51 +02:00
|
|
|
set activeRoster(WarbandRoster roster) {
|
2019-07-30 00:30:39 +02:00
|
|
|
_activeRosterFilePath = roster.filePath;
|
2019-07-10 14:16:13 +02:00
|
|
|
notifyListeners();
|
|
|
|
}
|
2019-07-06 00:44:20 +02:00
|
|
|
|
2019-08-08 20:15:06 +02:00
|
|
|
GitHubAdapter() {
|
|
|
|
load();
|
|
|
|
}
|
|
|
|
|
2019-07-06 00:44:20 +02:00
|
|
|
/// 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.
|
2019-08-02 00:56:04 +02:00
|
|
|
Future<void> search() async {
|
2019-07-27 00:09:51 +02:00
|
|
|
Stream<String> warbandFileStream() async* {
|
2019-07-06 00:44:20 +02:00
|
|
|
// Get all files which could be potential warband files (end with
|
|
|
|
// mordheim.yml and contain the word "heros").
|
2019-08-01 00:05:42 +02:00
|
|
|
|
|
|
|
// TODO: Check rate limit
|
|
|
|
// TODO: Extract github access to separate class
|
2019-07-06 00:44:20 +02:00
|
|
|
http.Response response = await http.get(
|
|
|
|
"https://api.github.com/search/code?q=heros+repo:" +
|
2019-07-16 23:37:18 +02:00
|
|
|
repository +
|
2019-07-06 00:44:20 +02:00
|
|
|
"+filename:mordheim.yml+path:\"" +
|
2019-07-16 23:37:18 +02:00
|
|
|
path +
|
2019-07-06 00:44:20 +02:00
|
|
|
"\"");
|
|
|
|
|
2019-07-07 22:31:06 +02:00
|
|
|
// GitHub is not reachable
|
|
|
|
if (response.statusCode != 200) {
|
2019-07-16 23:37:18 +02:00
|
|
|
_syncErrors.add('Could not find any warband roster files.');
|
2019-07-27 00:09:51 +02:00
|
|
|
yield null;
|
2019-07-07 22:31:06 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// No valid response from GitHub
|
|
|
|
dynamic searchResults;
|
|
|
|
try {
|
|
|
|
searchResults = jsonDecode(response.body);
|
|
|
|
} on FormatException catch (e) {
|
2019-07-16 23:37:18 +02:00
|
|
|
_syncErrors.add('Could not parse GitHub response. ' + e.toString());
|
2019-07-27 00:09:51 +02:00
|
|
|
yield null;
|
2019-07-07 22:31:06 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find suitable files for examination
|
|
|
|
RegExp fileRegex = new RegExp(r"[a-zA-Z]+\.mordheim\.ya?ml");
|
|
|
|
for (dynamic searchResult in searchResults['items']) {
|
2019-07-06 00:44:20 +02:00
|
|
|
if (fileRegex.hasMatch(searchResult['name'])) {
|
2019-07-27 00:09:51 +02:00
|
|
|
yield searchResult['path'].toString();
|
2019-07-06 00:44:20 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-02 00:56:04 +02:00
|
|
|
_syncErrors.clear();
|
2019-07-16 23:37:18 +02:00
|
|
|
_rosters.clear();
|
2019-08-02 00:56:04 +02:00
|
|
|
_syncInProgress = true;
|
2019-07-16 23:37:18 +02:00
|
|
|
|
2019-08-02 00:56:04 +02:00
|
|
|
notifyListeners();
|
2019-07-27 00:09:51 +02:00
|
|
|
|
2019-08-02 00:56:04 +02:00
|
|
|
await for (String filePath in warbandFileStream()) {
|
|
|
|
WarbandRoster roster = await fetchWarband(filePath);
|
|
|
|
Version latestVersion = await getLatestVersion(filePath);
|
2019-07-27 00:09:51 +02:00
|
|
|
|
2019-08-02 00:56:04 +02:00
|
|
|
if (roster != null && latestVersion != null) {
|
|
|
|
roster.playerName = getPlayerNameFromFilePath(filePath);
|
|
|
|
roster.version = latestVersion;
|
|
|
|
roster.filePath = filePath;
|
2019-07-27 00:09:51 +02:00
|
|
|
|
2019-08-02 00:56:04 +02:00
|
|
|
_rosters.add(roster);
|
|
|
|
notifyListeners();
|
2019-07-06 00:44:20 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-10 22:28:51 +02:00
|
|
|
// Sort by CP
|
2019-07-16 23:37:18 +02:00
|
|
|
_rosters.sort((a, b) => b.campaignPoints.compareTo(a.campaignPoints));
|
2019-07-10 22:28:51 +02:00
|
|
|
|
2019-07-10 14:16:13 +02:00
|
|
|
_lastSync = DateTime.now();
|
2019-08-02 00:56:04 +02:00
|
|
|
_syncInProgress = false;
|
|
|
|
|
2019-08-08 20:15:06 +02:00
|
|
|
await save();
|
2019-07-10 14:16:13 +02:00
|
|
|
notifyListeners();
|
2019-07-06 00:44:20 +02:00
|
|
|
}
|
|
|
|
|
2019-08-01 00:05:42 +02:00
|
|
|
Future<void> update() async {
|
2019-08-02 00:56:04 +02:00
|
|
|
_syncInProgress = true;
|
2019-07-30 00:30:39 +02:00
|
|
|
_syncErrors.clear();
|
|
|
|
|
|
|
|
notifyListeners();
|
|
|
|
|
|
|
|
for (var i = 0; i < rosters.length; i++) {
|
|
|
|
Version newVersion = await getLatestVersion(rosters[i].filePath);
|
2019-07-27 00:09:51 +02:00
|
|
|
|
2019-08-01 00:05:42 +02:00
|
|
|
// File does not exist any more, we remove the roster
|
2019-07-30 00:30:39 +02:00
|
|
|
if (newVersion == null) {
|
2019-08-01 00:05:42 +02:00
|
|
|
rosters.removeAt(i);
|
2019-08-02 00:56:04 +02:00
|
|
|
notifyListeners();
|
2019-07-30 00:30:39 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2019-08-01 00:05:42 +02:00
|
|
|
// New version found, so we fetch the updated roster
|
2019-07-30 00:30:39 +02:00
|
|
|
if (newVersion.gitHash != rosters[i].version.gitHash) {
|
|
|
|
WarbandRoster newRoster = await fetchWarband(rosters[i].filePath);
|
|
|
|
|
|
|
|
if (newRoster == null) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
newRoster.playerName = rosters[i].playerName;
|
|
|
|
newRoster.version = newVersion;
|
|
|
|
newRoster.filePath = rosters[i].filePath;
|
|
|
|
rosters[i] = newRoster;
|
2019-08-02 00:56:04 +02:00
|
|
|
notifyListeners();
|
2019-07-30 00:30:39 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_rosters.sort((a, b) => b.campaignPoints.compareTo(a.campaignPoints));
|
2019-07-06 00:44:20 +02:00
|
|
|
|
2019-07-10 14:16:13 +02:00
|
|
|
_lastSync = DateTime.now();
|
2019-08-02 00:56:04 +02:00
|
|
|
_syncInProgress = false;
|
2019-07-30 00:30:39 +02:00
|
|
|
|
2019-08-08 20:15:06 +02:00
|
|
|
await save();
|
2019-07-10 14:16:13 +02:00
|
|
|
notifyListeners();
|
2019-07-06 00:44:20 +02:00
|
|
|
}
|
2019-07-27 00:09:51 +02:00
|
|
|
|
|
|
|
String getPlayerNameFromFilePath(String filePath) {
|
|
|
|
// We try to get the name of the player from the name of the folder
|
|
|
|
// in which the file resists
|
2019-08-08 20:15:06 +02:00
|
|
|
String cleanedPath = path;
|
|
|
|
if (path.startsWith('/')) {
|
|
|
|
cleanedPath = cleanedPath.substring(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cleanedPath.endsWith('/')) {
|
|
|
|
cleanedPath = cleanedPath.substring(0, cleanedPath.length - 1);
|
|
|
|
}
|
|
|
|
List<String> pathParts =
|
|
|
|
filePath.substring(cleanedPath.length + 1).split('/');
|
2019-07-27 00:09:51 +02:00
|
|
|
|
|
|
|
String playerName = 'Lonely Recluse';
|
|
|
|
if (pathParts.length >= 2) {
|
|
|
|
playerName = pathParts.first;
|
|
|
|
}
|
|
|
|
|
|
|
|
return playerName;
|
|
|
|
}
|
|
|
|
|
2019-07-30 00:30:39 +02:00
|
|
|
Future<Version> getLatestVersion(String filePath) async {
|
2019-07-27 00:09:51 +02:00
|
|
|
// Fetch last change and some metainformation of the file
|
|
|
|
http.Response response = await http.get("https://api.github.com/repos/" +
|
|
|
|
repository +
|
|
|
|
"/commits?path=" +
|
|
|
|
filePath);
|
|
|
|
|
|
|
|
if (response.statusCode != 200) {
|
2019-08-01 00:05:42 +02:00
|
|
|
_syncErrors
|
|
|
|
.add(filePath + ': Could not load the warband metadata from GitHub.');
|
2019-08-02 00:56:04 +02:00
|
|
|
notifyListeners();
|
2019-07-27 00:09:51 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// No valid response from GitHub
|
|
|
|
dynamic commits;
|
|
|
|
try {
|
|
|
|
commits = jsonDecode(response.body);
|
|
|
|
} on FormatException catch (e) {
|
2019-08-01 00:05:42 +02:00
|
|
|
_syncErrors
|
|
|
|
.add(filePath + ': Could not parse GitHub response. ' + e.toString());
|
2019-08-02 00:56:04 +02:00
|
|
|
notifyListeners();
|
2019-07-27 00:09:51 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// No commits available
|
|
|
|
if (commits.length == 0) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
dynamic latestCommit = commits.first;
|
|
|
|
return new Version(
|
|
|
|
latestCommit['sha'],
|
|
|
|
latestCommit['commit']['committer']['date'],
|
|
|
|
latestCommit['commit']['author']['name'],
|
|
|
|
latestCommit['commit']['message']);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<WarbandRoster> fetchWarband(String filePath) async {
|
|
|
|
http.Response response;
|
|
|
|
try {
|
|
|
|
response = await http.get("https://raw.githubusercontent.com/" +
|
|
|
|
repository +
|
|
|
|
'/master/' +
|
|
|
|
filePath);
|
|
|
|
} catch (e) {
|
|
|
|
// We ignore this error, because it will handle from the _syncErrors
|
|
|
|
// later (see below).
|
|
|
|
}
|
|
|
|
|
2019-08-01 00:05:42 +02:00
|
|
|
if (response == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-07-27 00:09:51 +02:00
|
|
|
try {
|
2019-08-01 00:05:42 +02:00
|
|
|
YamlMap yamlObject = loadYaml(response.body);
|
|
|
|
return WarbandRoster.fromJson(yamlObject);
|
2019-07-27 00:09:51 +02:00
|
|
|
} catch (e) {
|
2019-08-01 00:05:42 +02:00
|
|
|
_syncErrors.add(filePath + ': ' + e.message);
|
2019-08-02 00:56:04 +02:00
|
|
|
notifyListeners();
|
2019-07-27 00:09:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-08-08 20:15:06 +02:00
|
|
|
Future<void> save() async {
|
|
|
|
await storage.clear();
|
|
|
|
await storage.setItem('lastSync', _lastSync.toIso8601String());
|
|
|
|
await storage.setItem('rosters', _rosters);
|
|
|
|
await storage.setItem('activeRosterFilePath', _activeRosterFilePath);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> load() async {
|
|
|
|
await storage.ready;
|
|
|
|
|
|
|
|
String lastSync = storage.getItem('lastSync');
|
|
|
|
if (lastSync != null) {
|
|
|
|
_lastSync = DateTime.parse(lastSync);
|
|
|
|
}
|
|
|
|
|
|
|
|
_activeRosterFilePath = storage.getItem('activeRosterFilePath');
|
|
|
|
|
|
|
|
List<dynamic> rosters = storage.getItem('rosters');
|
|
|
|
if (rosters != null) {
|
|
|
|
rosters.forEach((warband) {
|
|
|
|
_rosters.add(WarbandRoster.fromJson(warband));
|
|
|
|
});
|
|
|
|
}
|
2019-07-27 00:09:51 +02:00
|
|
|
}
|
2019-07-06 00:44:20 +02:00
|
|
|
}
|