diff --git a/mobile-app/lib/github_reader.dart b/mobile-app/lib/data/github_adapter.dart similarity index 67% rename from mobile-app/lib/github_reader.dart rename to mobile-app/lib/data/github_adapter.dart index 0650125..702311e 100644 --- a/mobile-app/lib/github_reader.dart +++ b/mobile-app/lib/data/github_adapter.dart @@ -1,26 +1,39 @@ +import 'dart:collection'; import 'dart:convert'; +import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; -import 'package:toolheim/warband_roaster.dart'; +import 'package:toolheim/data/warband_roaster.dart'; import 'package:yaml/yaml.dart'; -enum SyncState { - unknown, - running, - success, - error, -} +class GitHubAdapter extends ChangeNotifier { + String _repository = 'Labernator/Mordheim'; + String _path = 'Mordheim-BorderTownBurning/Warband Rosters'; -class GitHubAdapter { - // TODO: Store this in the SharedPreferences - final String repository; - final String path; + List _syncErrors = new List(); + DateTime _lastSync; - static SyncState syncState = SyncState.unknown; - List syncErrors = new List(); - static DateTime lastSync; + List _roasters = []; + String _activePlayerName = 'Aaron'; - GitHubAdapter(this.repository, this.path); + String get repository => _repository; + String get path => _path; + + DateTime get lastSync => _lastSync; + UnmodifiableListView get syncErrors => _syncErrors; + + UnmodifiableListView get roasters => + UnmodifiableListView(_roasters); + WarbandRoaster get activeRoaster => _roasters.firstWhere((roaster) { + return roaster.playerName == _activePlayerName; + }); + + void changeActiveRoaster(String playerName) { + _activePlayerName = playerName; + notifyListeners(); + } + + // TODO: Add persistence layer here /// Search for warband files in the GitHub repository /// @@ -28,22 +41,22 @@ class GitHubAdapter { /// 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. - Future> search() async { - syncErrors.clear(); + void search() async { + _syncErrors.clear(); Stream> roasterStream() 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 roaster files'); yield {}; return; } @@ -53,7 +66,7 @@ class GitHubAdapter { 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; } @@ -66,7 +79,7 @@ class GitHubAdapter { // 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) { @@ -76,12 +89,12 @@ class GitHubAdapter { // 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); if (response.statusCode != 200) { - syncErrors.add('Could not load the warband metadata from GitHub.'); + _syncErrors.add('Could not load the warband metadata from GitHub.'); continue; } @@ -90,7 +103,7 @@ class GitHubAdapter { 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; } @@ -112,12 +125,12 @@ class GitHubAdapter { } } - List roasters = new List(); - // TODO: Read params from share preferences + _roasters.clear(); + notifyListeners(); await for (Map player in roasterStream()) { http.Response response = await http.get( "https://raw.githubusercontent.com/" + - repository + + _repository + '/master/' + player['filePath']); @@ -135,21 +148,23 @@ class GitHubAdapter { // Sp, lastSyncVersion is equal to the currentVersion. roaster.lastSyncVersion = roaster.currentVersion; - roasters.add(roaster); + _roasters.add(roaster); + notifyListeners(); } catch (e) { - syncErrors.add(e.toString()); + _syncErrors.add(e.toString()); } } - return roasters; + _lastSync = DateTime.now(); + notifyListeners(); } - Future update() async { + void update() async { // 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(); - return SyncState.success; + _lastSync = DateTime.now(); + notifyListeners(); } } diff --git a/mobile-app/lib/warband_roaster.dart b/mobile-app/lib/data/warband_roaster.dart similarity index 98% rename from mobile-app/lib/warband_roaster.dart rename to mobile-app/lib/data/warband_roaster.dart index 8c86621..3578139 100644 --- a/mobile-app/lib/warband_roaster.dart +++ b/mobile-app/lib/data/warband_roaster.dart @@ -1,5 +1,6 @@ import 'dart:collection'; +import 'package:flutter/widgets.dart'; import 'package:json_annotation/json_annotation.dart'; part 'warband_roaster.g.dart'; @@ -128,6 +129,7 @@ class Hero extends Unit { } } +@immutable class Stats { final int movement; final int weaponSkill; @@ -216,6 +218,11 @@ class WarbandRoaster { this.race = this.nameAndRace['race']; } + int experience() { + // TODO: Calculate + return 1337; + } + static HashMap _warbandNameAndRace(String nameAndRace) { HashMap nr = new HashMap(); RegExp re = new RegExp(r"(.*) \((.*)\)"); diff --git a/mobile-app/lib/warband_roaster.g.dart b/mobile-app/lib/data/warband_roaster.g.dart similarity index 100% rename from mobile-app/lib/warband_roaster.g.dart rename to mobile-app/lib/data/warband_roaster.g.dart diff --git a/mobile-app/lib/main.dart b/mobile-app/lib/main.dart index dc725d0..83c5b66 100644 --- a/mobile-app/lib/main.dart +++ b/mobile-app/lib/main.dart @@ -1,163 +1,30 @@ import 'package:flutter/material.dart'; -import 'package:toolheim/github_reader.dart'; -import 'package:toolheim/warband_roaster.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'; -void main() => runApp(Toolheim()); +void main() { + runApp(Toolheim()); +} class Toolheim extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Toolheim', - theme: ThemeData( - primarySwatch: Colors.brown, - accentColor: Colors.grey, + return ChangeNotifierProvider( + builder: (context) => GitHubAdapter(), + child: MaterialApp( + title: 'Toolheim', + theme: ThemeData( + primarySwatch: Colors.brown, + accentColor: Colors.grey, + ), + initialRoute: '/', + routes: { + '/': (context) => WarbandRoasterScreen(), + '/settings': (context) => SettingsScreen() + }, ), - home: RoasterWidget(), ); } } - -class RoasterWidget extends StatefulWidget { - @override - _RoasterWidgetState createState() => _RoasterWidgetState(); -} - -class _RoasterWidgetState extends State { - // TODO: Read this from SharedPreferences - String activePlayer = 'Aaron'; - GitHubAdapter github = GitHubAdapter( - 'Labernator/Mordheim', 'Mordheim-BorderTownBurning/Warband Rosters'); - - List playerList(List roasters) { - List tiles = new List(); - - WarbandRoaster myWarband = roasters.firstWhere((roaster) { - return roaster.playerName == activePlayer; - }); - - // Show some stats for the own warband - tiles.add(UserAccountsDrawerHeader( - otherAccountsPictures: [ - IconButton( - icon: Icon(Icons.refresh), - color: Colors.white, - tooltip: 'Refresh warbands', - onPressed: () { - setState(() { - //roasters = await github.update(); - }); - }, - ), - IconButton( - icon: Icon(Icons.search), - color: Colors.white, - tooltip: 'Read warbands', - onPressed: () { - setState(() { - //roasters = await github.search() - }); - }, - ) - ], - accountName: Text(myWarband.name), - accountEmail: Text(myWarband.race), - currentAccountPicture: CircleAvatar( - child: Text('Aa'), - ), - )); - - // TODO: Order Players on CP or rating - - roasters.forEach((roaster) { - // We mark inactive warbands with a gray acent - var textColor = Colors.black; - if (!roaster.active) { - textColor = Colors.black45; - } - - tiles.add(ListTile( - title: Text(roaster.name + ' (' + roaster.playerName + ')', - style: TextStyle(color: textColor)), - subtitle: Text(roaster.currentVersion.message), - isThreeLine: true, - trailing: Chip( - label: Text( - '326 XP', - )))); - }); - - tiles.add(Divider()); - - return tiles; - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: github.search(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.none: - case ConnectionState.waiting: - case ConnectionState.active: - return Center(child: CircularProgressIndicator()); - case ConnectionState.done: - List roasters = snapshot.data; - - if (roasters.length == 0) { - return Text('No warbands found'); - } - - // TODO: Replace with router - WarbandRoaster warband = roasters.firstWhere((roaster) { - return roaster.playerName == this.activePlayer; - }); - - return Scaffold( - appBar: AppBar( - title: Text(warband.name), - ), - drawer: Drawer( - child: SingleChildScrollView( - child: Column(children: playerList(roasters))), - ), - body: ListView.builder( - itemCount: - warband.heros.length + warband.henchmenGroups.length, - itemBuilder: (BuildContext context, int index) { - // TODO: Sort by initiative - if (index < warband.heros.length) { - var hero = warband.heros[index]; - - return ListTile( - title: Text(hero.name), - leading: CircleAvatar( - child: Text(hero.experience.toString()), - backgroundColor: Colors.green, - foregroundColor: Colors.greenAccent, - ), - subtitle: Text(hero.type), - ); - } else { - var henchmenGroup = warband - .henchmenGroups[index - warband.heros.length]; - - return ListTile( - title: Text(henchmenGroup.name), - trailing: Chip( - label: Text( - henchmenGroup.number.toString() + 'x')), - leading: CircleAvatar( - child: Text(henchmenGroup.experience.toString()), - backgroundColor: Colors.orange, - foregroundColor: Colors.white, - ), - subtitle: Text(henchmenGroup.type), - ); - } - })); - } - }); - } -} diff --git a/mobile-app/lib/screens/settings_screen.dart b/mobile-app/lib/screens/settings_screen.dart new file mode 100644 index 0000000..07fc0f4 --- /dev/null +++ b/mobile-app/lib/screens/settings_screen.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:toolheim/data/github_adapter.dart'; + +class SettingsScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + GitHubAdapter github = Provider.of(context); + + return Scaffold( + appBar: AppBar(title: Text('Settings')), body: Text(github.repository)); + } +} diff --git a/mobile-app/lib/screens/warband_roaster_screen.dart b/mobile-app/lib/screens/warband_roaster_screen.dart new file mode 100644 index 0000000..8aab303 --- /dev/null +++ b/mobile-app/lib/screens/warband_roaster_screen.dart @@ -0,0 +1,68 @@ +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/widgets/warband_drawer_widget.dart'; + +class WarbandRoasterScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + GitHubAdapter github = Provider.of(context); + + if (github.lastSync == null) { + return Scaffold( + appBar: AppBar(title: Text('Toolheim')), + body: Center( + child: IconButton( + icon: Icon(Icons.search), + color: Colors.black, + tooltip: 'Search for warbands', + onPressed: github.search, + ), + )); + } + + WarbandRoaster roaster = github.activeRoaster; + + return Scaffold( + appBar: AppBar( + title: Text(roaster.name), + ), + drawer: + Drawer(child: SingleChildScrollView(child: WarbandDrawerWidget())), + body: ListView.builder( + itemCount: roaster.heros.length + roaster.henchmenGroups.length, + itemBuilder: (BuildContext context, int index) { + // TODO: Sort by initiative + if (index < roaster.heros.length) { + var hero = roaster.heros[index]; + + return ListTile( + title: Text(hero.name), + leading: CircleAvatar( + child: Text(hero.experience.toString()), + backgroundColor: Colors.green, + foregroundColor: Colors.greenAccent, + ), + subtitle: Text(hero.type), + ); + } else { + var henchmenGroup = + roaster.henchmenGroups[index - roaster.heros.length]; + + return ListTile( + title: Text(henchmenGroup.name), + trailing: + Chip(label: Text(henchmenGroup.number.toString() + 'x')), + leading: CircleAvatar( + child: Text(henchmenGroup.experience.toString()), + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + ), + subtitle: Text(henchmenGroup.type), + ); + } + })); + } +} diff --git a/mobile-app/lib/widgets/warband_drawer_widget.dart b/mobile-app/lib/widgets/warband_drawer_widget.dart new file mode 100644 index 0000000..8be6983 --- /dev/null +++ b/mobile-app/lib/widgets/warband_drawer_widget.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:toolheim/data/github_adapter.dart'; +import 'package:toolheim/data/warband_roaster.dart'; + +class WarbandDrawerWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + GitHubAdapter github = Provider.of(context); + + if (github.lastSync == null) { + // Add search button + return Column(children: []); + } + + WarbandRoaster activeRoaster = github.activeRoaster; + List roasters = github.roasters; + + List tiles = new List(); + + // Show some stats for the own warband + tiles.add(UserAccountsDrawerHeader( + //otherAccountsPictures: [ + // IconButton( + // icon: Icon(Icons.refresh), + // color: Colors.white, + // highlightColor: Colors.brown, + // tooltip: 'Refresh warbands', + // onPressed: github.update, + // ), + // IconButton( + // icon: Icon(Icons.search), + // color: Colors.white, + // tooltip: 'Read warbands', + // onPressed: github.search, + // ) + //], + accountName: Text(activeRoaster.name), + accountEmail: Text(activeRoaster.race), + )); + + // TODO: Order Players on CP or rating + + roasters.forEach((roaster) { + // We mark inactive warbands with a gray acent + var textColor = Colors.black; + if (!roaster.active) { + textColor = Colors.black45; + } + + tiles.add(ListTile( + onTap: () { + github.changeActiveRoaster(roaster.playerName); + Navigator.of(context).pop(); + }, + title: Text(roaster.name + ' (' + roaster.playerName + ')', + style: TextStyle(color: textColor)), + subtitle: Text(roaster.currentVersion.message), + isThreeLine: true, + trailing: + Chip(label: Text(roaster.campaignPoints.toString() + ' CP')))); + }); + + tiles.add(Divider()); + + return Column(children: tiles); + } +} diff --git a/mobile-app/pubspec.lock b/mobile-app/pubspec.lock index 0cb7be0..14c18e4 100644 --- a/mobile-app/pubspec.lock +++ b/mobile-app/pubspec.lock @@ -312,6 +312,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.4.0" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0+1" pub_semver: dependency: transitive description: diff --git a/mobile-app/pubspec.yaml b/mobile-app/pubspec.yaml index 8071320..ab6eb22 100644 --- a/mobile-app/pubspec.yaml +++ b/mobile-app/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: yaml: checked_yaml: http: + provider: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons.