import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:preferences/preferences.dart'; import 'package:localstorage/localstorage.dart'; import 'package:yaml/yaml.dart'; import 'package:toolheim/data/warband_roster.dart'; class GitHubAdapter extends ChangeNotifier { final LocalStorage storage = new LocalStorage('rosters'); List _syncErrors = new List(); DateTime _lastSync; List _rosters = []; String _activeRosterFilePath; String get repository => PrefService.getString('repository'); String get path => PrefService.getString('path'); bool _syncinProgress = false; bool get isSyncInProgress => _syncinProgress; DateTime get lastSync => _lastSync; List get syncErrors => _syncErrors; List 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); } set activeRoster(WarbandRoster roster) { _activeRosterFilePath = roster.filePath; notifyListeners(); } /// 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. void search() async { _syncErrors.clear(); _syncinProgress = true; 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 + "+filename:mordheim.yml+path:\"" + path + "\""); // GitHub is not reachable if (response.statusCode != 200) { _syncErrors.add('Could not find any warband roster files.'); yield null; 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()); yield null; return; } // Find suitable files for examination RegExp fileRegex = new RegExp(r"[a-zA-Z]+\.mordheim\.ya?ml"); for (dynamic searchResult in searchResults['items']) { if (fileRegex.hasMatch(searchResult['name'])) { yield searchResult['path'].toString(); } } } storage.clear(); _rosters.clear(); notifyListeners(); if (_syncErrors.length == 0) { await for (String filePath in warbandFileStream()) { WarbandRoster roster = await fetchWarband(filePath); Version latestVersion = await getLatestVersion(filePath); if (roster != null && latestVersion != null) { roster.playerName = getPlayerNameFromFilePath(filePath); roster.version = latestVersion; roster.filePath = filePath; _rosters.add(roster); notifyListeners(); } //https://github.com/lesnitsky/flutter_localstorage/blob/master/example/lib/main.dart // FIXME: store it correctly //storage.setItem(player['player'] + '-warband-yaml', response.body); //storage.setItem( // player['player'] + '-current-version', roster.currentVersion); //storage.setItem( // player['player'] + '-last-sync-version', roster.lastSyncVersion); } } // Sort by CP _rosters.sort((a, b) => b.campaignPoints.compareTo(a.campaignPoints)); _lastSync = DateTime.now(); _syncinProgress = false; storage.setItem('lastSync', _lastSync.toIso8601String()); notifyListeners(); } Future update() async { _syncinProgress = true; _syncErrors.clear(); notifyListeners(); 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); if (newRoster == null) { continue; } newRoster.playerName = rosters[i].playerName; newRoster.version = newVersion; newRoster.filePath = rosters[i].filePath; rosters[i] = newRoster; } } _rosters.sort((a, b) => b.campaignPoints.compareTo(a.campaignPoints)); _lastSync = DateTime.now(); _syncinProgress = false; notifyListeners(); } String getPlayerNameFromFilePath(String filePath) { // We try to get the name of the player from the name of the folder // in which the file resists List pathParts = filePath.substring(path.length + 1).split('/'); String playerName = 'Lonely Recluse'; if (pathParts.length >= 2) { playerName = pathParts.first; } return playerName; } Future getLatestVersion(String filePath) async { // 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) { _syncErrors .add(filePath + ': Could not load the warband metadata from GitHub.'); return null; } // No valid response from GitHub dynamic commits; try { commits = jsonDecode(response.body); } on FormatException catch (e) { _syncErrors .add(filePath + ': Could not parse GitHub response. ' + e.toString()); 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 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). } if (response == null) { return null; } try { YamlMap yamlObject = loadYaml(response.body); return WarbandRoster.fromJson(yamlObject); } catch (e) { _syncErrors.add(filePath + ': ' + e.message); } return null; } void readWarband(WarbandRoster roster, String yamlContent) { // TODO: Read the warband from the shared preferences } }