From 5068592189d710cc375000cb4d3a16fa7fea50ff Mon Sep 17 00:00:00 2001 From: Aaron Fischer Date: Tue, 16 Jul 2019 23:37:18 +0200 Subject: [PATCH] rename roster anf fix search functionality --- mobile-app/lib/data/github_adapter.dart | 108 ++++++++++-------- ...rband_roaster.dart => warband_roster.dart} | 8 +- ...d_roaster.g.dart => warband_roster.g.dart} | 8 +- mobile-app/lib/main.dart | 8 +- mobile-app/lib/screens/settings_screen.dart | 28 ++--- ...screen.dart => warband_roster_screen.dart} | 14 +-- .../lib/widgets/warband_drawer_widget.dart | 88 +++++++++++--- mobile-app/pubspec.lock | 50 +++++--- mobile-app/pubspec.yaml | 2 + 9 files changed, 200 insertions(+), 114 deletions(-) rename mobile-app/lib/data/{warband_roaster.dart => warband_roster.dart} (97%) rename mobile-app/lib/data/{warband_roaster.g.dart => warband_roster.g.dart} (89%) rename mobile-app/lib/screens/{warband_roaster_screen.dart => warband_roster_screen.dart} (87%) diff --git a/mobile-app/lib/data/github_adapter.dart b/mobile-app/lib/data/github_adapter.dart index a22e12d..43d65c9 100644 --- a/mobile-app/lib/data/github_adapter.dart +++ b/mobile-app/lib/data/github_adapter.dart @@ -3,39 +3,40 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; -import 'package:toolheim/data/warband_roaster.dart'; +import 'package:preferences/preferences.dart'; +import 'package:toolheim/data/warband_roster.dart'; import 'package:yaml/yaml.dart'; class GitHubAdapter extends ChangeNotifier { - String _repository = 'Labernator/Mordheim'; - String _path = 'Mordheim-BorderTownBurning/Warband Rosters'; - List _syncErrors = new List(); DateTime _lastSync; - List _roasters = []; + List _rosters = []; String _activePlayerName; - String get repository => _repository; - String get path => _path; + String get repository => PrefService.getString('repository'); + String get path => PrefService.getString('path'); + + bool _syncinProgress = false; + bool get isSyncInProgress => _syncinProgress; DateTime get lastSync => _lastSync; - UnmodifiableListView get syncErrors => _syncErrors; + List get syncErrors => _syncErrors; - UnmodifiableListView get roasters => - UnmodifiableListView(_roasters); + UnmodifiableListView get rosters => + UnmodifiableListView(_rosters); - WarbandRoaster activeRoaster() { - if (_activePlayerName == null || _roasters.length == 0) { + WarbandRoster activeRoster() { + if (_activePlayerName == null || _rosters.length == 0) { return null; } - return _roasters.firstWhere((roaster) { - return roaster.playerName == _activePlayerName; - }); + return _rosters.firstWhere((roster) { + return roster.playerName == _activePlayerName; + }, orElse: () => null); } - void changeActiveRoaster(String playerName) { + void changeActiveRoster(String playerName) { _activePlayerName = playerName; notifyListeners(); } @@ -50,20 +51,21 @@ class GitHubAdapter extends ChangeNotifier { /// the [syncErrors] list. void search() async { _syncErrors.clear(); + _syncinProgress = true; - Stream> roasterStream() async* { + Stream> rosterStream() 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 + + repository + "+filename:mordheim.yml+path:\"" + - _path + + path + "\""); // GitHub is not reachable if (response.statusCode != 200) { - _syncErrors.add('Could not find any warband roaster files'); + _syncErrors.add('Could not find any warband roster files.'); yield {}; return; } @@ -73,7 +75,7 @@ class GitHubAdapter extends ChangeNotifier { try { searchResults = jsonDecode(response.body); } on FormatException catch (e) { - _syncErrors.add('Could not parse GitHub response.' + e.toString()); + _syncErrors.add('Could not parse GitHub response. ' + e.toString()); yield {}; return; } @@ -86,7 +88,7 @@ class GitHubAdapter extends ChangeNotifier { // in which the file resists String completePath = searchResult['path']; List pathParts = - completePath.substring(_path.length + 1).split('/'); + completePath.substring(path.length + 1).split('/'); String playerName; if (pathParts.length >= 2) { @@ -96,7 +98,7 @@ class GitHubAdapter extends ChangeNotifier { // Fetch last change and some metainformation of the file http.Response response = await http.get( "https://api.github.com/repos/" + - _repository + + repository + "/commits?path=" + completePath); @@ -110,7 +112,7 @@ class GitHubAdapter extends ChangeNotifier { try { commits = jsonDecode(response.body); } on FormatException catch (e) { - _syncErrors.add('Could not parse GitHub response.' + e.toString()); + _syncErrors.add('Could not parse GitHub response. ' + e.toString()); continue; } @@ -132,52 +134,66 @@ class GitHubAdapter extends ChangeNotifier { } } - _roasters.clear(); + _rosters.clear(); notifyListeners(); - await for (Map player in roasterStream()) { - http.Response response = await http.get( - "https://raw.githubusercontent.com/" + - _repository + + + if (_syncErrors.length == 0) { + await for (Map player in rosterStream()) { + http.Response response; + try { + response = await http.get("https://raw.githubusercontent.com/" + + repository + '/master/' + player['filePath']); - - try { - YamlMap yamlObject = loadYaml(response.body); - WarbandRoaster roaster = WarbandRoaster.fromJson(yamlObject); - if (player['player'] != '') { - roaster.playerName = player['player']; + } catch (e) { + // TODO: Ignore this error, we catch it elsewhere. } - roaster.currentVersion = new Version(player['shaHash'], player['date'], - player['author'], player['message']); + try { + if (response != null) { + YamlMap yamlObject = loadYaml(response.body); + WarbandRoster roster = WarbandRoster.fromJson(yamlObject); + if (player['player'] != '') { + roster.playerName = player['player']; + } - // On a search, we drop all previous information about the warbands, - // Sp, lastSyncVersion is equal to the currentVersion. - roaster.lastSyncVersion = roaster.currentVersion; + roster.currentVersion = new Version(player['shaHash'], + player['date'], player['author'], player['message']); - _roasters.add(roaster); - notifyListeners(); - } catch (e) { - _syncErrors.add(e.toString()); + // On a search, we drop all previous information about the warbands, + // Sp, lastSyncVersion is equal to the currentVersion. + roster.lastSyncVersion = roster.currentVersion; + + _rosters.add(roster); + notifyListeners(); + } + } catch (e) { + _syncErrors.add(e.toString()); + } } } // Sort by CP - _roasters.sort((a, b) => b.campaignPoints.compareTo(a.campaignPoints)); + _rosters.sort((a, b) => b.campaignPoints.compareTo(a.campaignPoints)); // Select first as active player if no active player is selected - _activePlayerName = _roasters.first.playerName; + if (_rosters.length > 0) { + _activePlayerName = _rosters.first.playerName; + } _lastSync = DateTime.now(); + _syncinProgress = false; notifyListeners(); } void update() async { + _syncinProgress = true; // 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(); + _syncinProgress = false; notifyListeners(); } } diff --git a/mobile-app/lib/data/warband_roaster.dart b/mobile-app/lib/data/warband_roster.dart similarity index 97% rename from mobile-app/lib/data/warband_roaster.dart rename to mobile-app/lib/data/warband_roster.dart index 3578139..6283d58 100644 --- a/mobile-app/lib/data/warband_roaster.dart +++ b/mobile-app/lib/data/warband_roster.dart @@ -3,7 +3,7 @@ import 'dart:collection'; import 'package:flutter/widgets.dart'; import 'package:json_annotation/json_annotation.dart'; -part 'warband_roaster.g.dart'; +part 'warband_roster.g.dart'; // flutter packages pub run build_runner build --delete-conflicting-outputs @@ -166,7 +166,7 @@ class Version { } @JsonSerializable(nullable: true, anyMap: true, createToJson: false) -class WarbandRoaster { +class WarbandRoster { /// Store the complete string of name and race. This will split up into the /// fields name and race. @JsonKey(name: 'warband', fromJson: _warbandNameAndRace) @@ -203,7 +203,7 @@ class WarbandRoaster { @JsonKey(ignore: true) Version currentVersion; - WarbandRoaster( + WarbandRoster( this.nameAndRace, this.campaignPoints, this.objective, @@ -234,5 +234,5 @@ class WarbandRoaster { return nr; } - factory WarbandRoaster.fromJson(yaml) => _$WarbandRoasterFromJson(yaml); + factory WarbandRoster.fromJson(yaml) => _$WarbandRosterFromJson(yaml); } diff --git a/mobile-app/lib/data/warband_roaster.g.dart b/mobile-app/lib/data/warband_roster.g.dart similarity index 89% rename from mobile-app/lib/data/warband_roaster.g.dart rename to mobile-app/lib/data/warband_roster.g.dart index a1b0eae..11acad2 100644 --- a/mobile-app/lib/data/warband_roaster.g.dart +++ b/mobile-app/lib/data/warband_roster.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'warband_roaster.dart'; +part of 'warband_roster.dart'; // ************************************************************************** // JsonSerializableGenerator @@ -25,9 +25,9 @@ Hero _$HeroFromJson(Map json) { Hero._heroHeaderFromJson(json['hero'] as String)); } -WarbandRoaster _$WarbandRoasterFromJson(Map json) { - return WarbandRoaster( - WarbandRoaster._warbandNameAndRace(json['warband'] as String), +WarbandRoster _$WarbandRosterFromJson(Map json) { + return WarbandRoster( + WarbandRoster._warbandNameAndRace(json['warband'] as String), json['campaign'] as int, json['objective'] as String, json['alignment'] as String, diff --git a/mobile-app/lib/main.dart b/mobile-app/lib/main.dart index 83c5b66..6ff7d9e 100644 --- a/mobile-app/lib/main.dart +++ b/mobile-app/lib/main.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:preferences/preferences.dart'; import 'package:provider/provider.dart'; import 'package:toolheim/data/github_adapter.dart'; import 'package:toolheim/screens/settings_screen.dart'; -import 'package:toolheim/screens/warband_roaster_screen.dart'; +import 'package:toolheim/screens/warband_roster_screen.dart'; -void main() { +void main() async { + await PrefService.init(prefix: 'toolheim_'); runApp(Toolheim()); } @@ -21,7 +23,7 @@ class Toolheim extends StatelessWidget { ), initialRoute: '/', routes: { - '/': (context) => WarbandRoasterScreen(), + '/': (context) => WarbandRosterScreen(), '/settings': (context) => SettingsScreen() }, ), diff --git a/mobile-app/lib/screens/settings_screen.dart b/mobile-app/lib/screens/settings_screen.dart index 73c266a..11d1591 100644 --- a/mobile-app/lib/screens/settings_screen.dart +++ b/mobile-app/lib/screens/settings_screen.dart @@ -1,26 +1,22 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import 'package:toolheim/data/github_adapter.dart'; +import 'package:preferences/preferences.dart'; class SettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - GitHubAdapter github = Provider.of(context); - - // TODO: Add this to local storage - //String _repository = 'Labernator/Mordheim'; - //String _path = 'Mordheim-BorderTownBurning/Warband Rosters'; - return Scaffold( appBar: AppBar(title: Text('Settings')), - body: Column(children: [ - TextField( - decoration: InputDecoration(labelText: 'Repository'), - ), - TextField( - decoration: InputDecoration(labelText: 'Path'), - ) + body: PreferencePage([ + PreferenceTitle('GitHub'), + PreferenceText( + 'Provide a valid GitHub repository with username and project (username/repository-name). This repository must contain warband roster files in subfolders. See the sample project for a kickstart.'), + TextFieldPreference('Repository', 'repository'), + PreferenceText( + 'If your warband folders are placed in a subfolder, you can specify it here.'), + TextFieldPreference('Path', 'path', defaultVal: '/') ])); + + //String _repository = 'Labernator/Mordheim'; + //String _path = 'Mordheim-BorderTownBurning/Warband Rosters'; } } diff --git a/mobile-app/lib/screens/warband_roaster_screen.dart b/mobile-app/lib/screens/warband_roster_screen.dart similarity index 87% rename from mobile-app/lib/screens/warband_roaster_screen.dart rename to mobile-app/lib/screens/warband_roster_screen.dart index 3fd92f7..d2c8437 100644 --- a/mobile-app/lib/screens/warband_roaster_screen.dart +++ b/mobile-app/lib/screens/warband_roster_screen.dart @@ -2,16 +2,16 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:toolheim/data/github_adapter.dart'; -import 'package:toolheim/data/warband_roaster.dart'; +import 'package:toolheim/data/warband_roster.dart'; import 'package:toolheim/widgets/warband_drawer_widget.dart'; -class WarbandRoasterScreen extends StatelessWidget { +class WarbandRosterScreen extends StatelessWidget { @override Widget build(BuildContext context) { GitHubAdapter github = Provider.of(context); - WarbandRoaster roaster = github.activeRoaster(); + WarbandRoster roster = github.activeRoster(); - if (roaster == null) { + if (roster == null) { return Scaffold( appBar: AppBar(title: const Text('Toolheim')), body: Builder(builder: (BuildContext context) { @@ -41,7 +41,7 @@ class WarbandRoasterScreen extends StatelessWidget { List tiles = new List(); - roaster.heros.forEach((hero) { + roster.heros.forEach((hero) { tiles.add(new ListTile( title: Text(hero.name), leading: CircleAvatar( @@ -55,7 +55,7 @@ class WarbandRoasterScreen extends StatelessWidget { tiles.add(Divider()); - roaster.henchmenGroups.forEach((henchmenGroup) { + roster.henchmenGroups.forEach((henchmenGroup) { tiles.add(new ListTile( title: Text(henchmenGroup.name), trailing: Chip(label: Text(henchmenGroup.number.toString() + 'x')), @@ -70,7 +70,7 @@ class WarbandRoasterScreen extends StatelessWidget { return Scaffold( appBar: AppBar( - title: Text(roaster.name), + title: Text(roster.name), ), drawer: Drawer(child: SingleChildScrollView(child: WarbandDrawerWidget())), diff --git a/mobile-app/lib/widgets/warband_drawer_widget.dart b/mobile-app/lib/widgets/warband_drawer_widget.dart index 608b197..d58447f 100644 --- a/mobile-app/lib/widgets/warband_drawer_widget.dart +++ b/mobile-app/lib/widgets/warband_drawer_widget.dart @@ -1,18 +1,18 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:badges/badges.dart'; import 'package:toolheim/data/github_adapter.dart'; -import 'package:toolheim/data/warband_roaster.dart'; +import 'package:toolheim/data/warband_roster.dart'; class WarbandDrawerWidget extends StatelessWidget { @override Widget build(BuildContext context) { GitHubAdapter github = Provider.of(context); - WarbandRoaster activeRoaster = github.activeRoaster(); - - if (activeRoaster == null) { - // Add search button + // No settings at all + if (github.repository == null) { return Padding( padding: const EdgeInsets.only(top: 100, left: 30, right: 30), child: Column(children: [ @@ -31,10 +31,68 @@ class WarbandDrawerWidget extends StatelessWidget { ); } - List roasters = github.roasters; + // Never fetched any data + if (github.activeRoster() == null) { + return Padding( + padding: const EdgeInsets.only(top: 100, left: 30, right: 30), + child: Column(children: [ + Text( + 'The repository is set. You can now search for warbands. This can take a while.'), + FlatButton( + onPressed: () { + github.search(); + }, + child: Text( + 'Search for warbands', + style: TextStyle(color: Colors.blue), + ), + ), + FlatButton( + onPressed: () { + Navigator.popAndPushNamed(context, '/settings'); + }, + child: Text( + 'Change settings', + style: TextStyle(color: Colors.blue), + ), + ), + Visibility( + visible: github.isSyncInProgress, + child: CircularProgressIndicator(), + ), + Visibility( + visible: github.syncErrors.length > 0, + child: buildSyncErrors(context), + ) + ]), + ); + } + + return buildRosterList(context); + } + + Widget buildSyncErrors(BuildContext context) { + List syncErrors = new List(); + + GitHubAdapter github = Provider.of(context); + // TODO: Make it pretty + github.syncErrors.forEach((error) { + syncErrors.add(Text(error, style: TextStyle(color: Colors.red))); + }); + + return Column(children: syncErrors); + } + + Widget buildRosterList(BuildContext context) { + GitHubAdapter github = Provider.of(context); + WarbandRoster activeroster = github.activeRoster(); + List rosters = github.rosters; List tiles = new List(); + tiles.add(Visibility( + visible: github.isSyncInProgress, child: LinearProgressIndicator())); + // Show some stats for the own warband tiles.add(UserAccountsDrawerHeader( otherAccountsPictures: [ @@ -60,33 +118,31 @@ class WarbandDrawerWidget extends StatelessWidget { }, ), ], - accountName: Text(activeRoaster.name), - accountEmail: Text(activeRoaster.race), + accountName: Text(activeroster.name), + accountEmail: Text(activeroster.race), )); - // TODO: Order Players on CP or rating - - roasters.forEach((roaster) { + rosters.forEach((roster) { // We mark inactive warbands with a gray acent var textColor = Colors.black; - if (!roaster.active) { + if (!roster.active) { textColor = Colors.black45; } tiles.add(ListTile( onTap: () { - github.changeActiveRoaster(roaster.playerName); + github.changeActiveRoster(roster.playerName); Navigator.of(context).pop(); }, - title: Text(roaster.name + ' (' + roaster.playerName + ')', + title: Text(roster.name + ' (' + roster.playerName + ')', style: TextStyle(color: textColor)), - subtitle: Text(roaster.currentVersion.message), + subtitle: Text(roster.currentVersion.message), isThreeLine: true, trailing: Badge( badgeColor: Colors.black12, shape: BadgeShape.square, borderRadius: 2, - badgeContent: Text(roaster.campaignPoints.toString() + ' CP', + badgeContent: Text(roster.campaignPoints.toString() + ' CP', style: TextStyle(color: Colors.white)), ), )); diff --git a/mobile-app/pubspec.lock b/mobile-app/pubspec.lock index 748f804..a02211f 100644 --- a/mobile-app/pubspec.lock +++ b/mobile-app/pubspec.lock @@ -1,5 +1,5 @@ # Generated by pub -# See https://www.dartlang.org/tools/pub/glossary#lockfile +# See https://dart.dev/tools/pub/glossary#lockfile packages: analyzer: dependency: transitive @@ -7,7 +7,7 @@ packages: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.36.3" + version: "0.36.4" args: dependency: transitive description: @@ -21,7 +21,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.0" badges: dependency: "direct main" description: @@ -42,7 +42,7 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "1.1.4" + version: "1.1.5" build_config: dependency: transitive description: @@ -63,14 +63,14 @@ packages: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.0.6" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "1.6.0" + version: "1.6.1" build_runner_core: dependency: transitive description: @@ -91,7 +91,7 @@ packages: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "6.6.0" + version: "6.7.0" charcode: dependency: transitive description: @@ -140,7 +140,7 @@ packages: name: csslib url: "https://pub.dartlang.org" source: hosted - version: "0.16.0" + version: "0.16.1" cupertino_icons: dependency: "direct main" description: @@ -154,7 +154,7 @@ packages: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "1.2.7" + version: "1.2.9" fixnum: dependency: transitive description: @@ -178,7 +178,7 @@ packages: name: front_end url: "https://pub.dartlang.org" source: hosted - version: "0.1.18" + version: "0.1.19" glob: dependency: transitive description: @@ -255,7 +255,7 @@ packages: name: kernel url: "https://pub.dartlang.org" source: hosted - version: "0.3.18" + version: "0.3.19" logging: dependency: transitive description: @@ -311,7 +311,7 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.7.0" pool: dependency: transitive description: @@ -319,6 +319,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.4.0" + preferences: + dependency: "direct main" + description: + name: preferences + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" provider: dependency: "direct main" description: @@ -346,7 +353,14 @@ packages: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.3+2" shelf: dependency: transitive description: @@ -421,7 +435,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.4" + version: "0.2.5" timing: dependency: transitive description: @@ -449,14 +463,14 @@ packages: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "0.9.7+10" + version: "0.9.7+12" web_socket_channel: dependency: transitive description: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "1.0.13" + version: "1.0.14" yaml: dependency: "direct main" description: @@ -465,5 +479,5 @@ packages: source: hosted version: "2.1.16" sdks: - dart: ">=2.3.0-dev.0.1 <3.0.0" - flutter: ">=0.2.5 <2.0.0" + dart: ">=2.3.0 <3.0.0" + flutter: ">=1.5.0 <2.0.0" diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index cb3a18f..7ee304b 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -24,6 +24,8 @@ dependencies: http: provider: badges: + shared_preferences: + preferences: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons.