toolheim/mobile-app/lib/data/github_adapter.dart

281 lines
7.6 KiB
Dart
Raw Normal View History

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;
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;
List<WarbandRoster> _rosters = [];
String _activeRosterFilePath;
2019-07-06 00:44:20 +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;
List<String> get syncErrors => _syncErrors;
2019-07-10 14:16:13 +02:00
List<WarbandRoster> get rosters => _rosters;
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-27 00:09:51 +02:00
set activeRoster(WarbandRoster roster) {
_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:" +
repository +
2019-07-06 00:44:20 +02:00
"+filename:mordheim.yml+path:\"" +
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) {
_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) {
_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();
_rosters.clear();
2019-08-02 00:56:04 +02:00
_syncInProgress = true;
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
_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;
_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
if (newVersion == null) {
2019-08-01 00:05:42 +02:00
rosters.removeAt(i);
2019-08-02 00:56:04 +02:00
notifyListeners();
continue;
}
2019-08-01 00:05:42 +02:00
// New version found, so we fetch the updated roster
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();
}
}
_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-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;
}
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
}