import 'dart:collection'; 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 = []; WarbandRoster _activeRoster; 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; UnmodifiableListView get rosters => UnmodifiableListView(_rosters); WarbandRoster get activeRoster => _activeRoster; set activeRoster(WarbandRoster roster) { _activeRoster = roster; 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"). 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 getLatestCommit(filePath); roster.playerName = getPlayerNameFromFilePath(filePath); roster.currentVersion = latestVersion; // On a search, we drop all previous information about the warbands, // So, lastSyncVersion is equal to the currentVersion. roster.lastSyncVersion = roster.currentVersion; _rosters.add(roster); //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); notifyListeners(); } } // Sort by CP _rosters.sort((a, b) => b.campaignPoints.compareTo(a.campaignPoints)); // Select first as active player if no active player is selected if (_rosters.length > 0) { _activeRoster = _rosters.first; } _lastSync = DateTime.now(); _syncinProgress = false; storage.setItem('lastSync', _lastSync.toIso8601String()); notifyListeners(); } void update() async { _syncinProgress = true; // TODO: Loop through the found warbands and update it. _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 getLatestCommit(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('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('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). } try { if (response != null) { YamlMap yamlObject = loadYaml(response.body); return WarbandRoster.fromJson(yamlObject); } } catch (e) { _syncErrors.add(e.toString()); } return null; } void readWarband(WarbandRoster roster, String yamlContent) { // TODO: Read the warband from the shared preferences } }