rename roster anf fix search functionality

This commit is contained in:
Aaron Fischer 2019-07-16 23:37:18 +02:00
parent 51196f7c8c
commit 5068592189
9 changed files with 200 additions and 114 deletions

View file

@ -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<String> _syncErrors = new List<String>();
DateTime _lastSync;
List<WarbandRoaster> _roasters = [];
List<WarbandRoster> _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<String> get syncErrors => _syncErrors;
List<String> get syncErrors => _syncErrors;
UnmodifiableListView<WarbandRoaster> get roasters =>
UnmodifiableListView(_roasters);
UnmodifiableListView<WarbandRoster> 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<Map<String, String>> roasterStream() async* {
Stream<Map<String, String>> 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<String> 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<String, String> player in roasterStream()) {
http.Response response = await http.get(
"https://raw.githubusercontent.com/" +
_repository +
if (_syncErrors.length == 0) {
await for (Map<String, String> 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'];
}
roster.currentVersion = new Version(player['shaHash'],
player['date'], player['author'], player['message']);
// On a search, we drop all previous information about the warbands,
// Sp, lastSyncVersion is equal to the currentVersion.
roaster.lastSyncVersion = roaster.currentVersion;
roster.lastSyncVersion = roster.currentVersion;
_roasters.add(roaster);
_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();
}
}

View file

@ -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);
}

View file

@ -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,

View file

@ -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()
},
),

View file

@ -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<GitHubAdapter>(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: <Widget>[
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';
}
}

View file

@ -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<GitHubAdapter>(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<Widget> 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())),

View file

@ -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<GitHubAdapter>(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: <Widget>[
@ -31,10 +31,68 @@ class WarbandDrawerWidget extends StatelessWidget {
);
}
List<WarbandRoaster> 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: <Widget>[
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<Widget> syncErrors = new List();
GitHubAdapter github = Provider.of<GitHubAdapter>(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<GitHubAdapter>(context);
WarbandRoster activeroster = github.activeRoster();
List<WarbandRoster> rosters = github.rosters;
List<Widget> tiles = new List();
tiles.add(Visibility(
visible: github.isSyncInProgress, child: LinearProgressIndicator()));
// Show some stats for the own warband
tiles.add(UserAccountsDrawerHeader(
otherAccountsPictures: <Widget>[
@ -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)),
),
));

View file

@ -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"

View file

@ -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.