Make all data persistent

This commit is contained in:
Aaron Fischer 2019-08-08 20:15:06 +02:00
parent 9369c5012f
commit 5085a2d4b5
12 changed files with 181 additions and 50 deletions

View file

@ -32,8 +32,7 @@ android {
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "net.aaronfischer.toolheim"
applicationId "com.example.toolheim"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 28 targetSdkVersion 28
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()

View file

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.toolheim"> package="net.aaronfischer.toolheim">
<!-- Flutter needs it to communicate with the running application <!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->

View file

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.toolheim"> package="net.aaronfischer.toolheim">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that <!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method. calls FlutterMain.startInitialization(this); in its onCreate method.

View file

@ -1,4 +1,4 @@
package com.example.toolheim; package net.aaronfischer.toolheim;
import android.os.Bundle; import android.os.Bundle;
import io.flutter.app.FlutterActivity; import io.flutter.app.FlutterActivity;

View file

@ -1,7 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.toolheim"> package="net.aaronfischer.toolheim">
<!-- Flutter needs it to communicate with the running application <!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
</manifest> </manifest>

View file

@ -323,7 +323,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.toolheim; PRODUCT_BUNDLE_IDENTIFIER = net.aaronfischer.toolheim;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
@ -448,7 +448,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.toolheim; PRODUCT_BUNDLE_IDENTIFIER = net.aaronfischer.toolheim;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
@ -471,7 +471,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Flutter", "$(PROJECT_DIR)/Flutter",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.example.toolheim; PRODUCT_BUNDLE_IDENTIFIER = net.aaronfischer.toolheim;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };

View file

@ -45,6 +45,10 @@ class GitHubAdapter extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
GitHubAdapter() {
load();
}
/// Search for warband files in the GitHub repository /// Search for warband files in the GitHub repository
/// ///
/// This method will search for matching files and check their content in a /// This method will search for matching files and check their content in a
@ -91,7 +95,6 @@ class GitHubAdapter extends ChangeNotifier {
} }
} }
//storage.clear();
_syncErrors.clear(); _syncErrors.clear();
_rosters.clear(); _rosters.clear();
_syncInProgress = true; _syncInProgress = true;
@ -110,14 +113,6 @@ class GitHubAdapter extends ChangeNotifier {
_rosters.add(roster); _rosters.add(roster);
notifyListeners(); 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 // Sort by CP
@ -125,8 +120,8 @@ class GitHubAdapter extends ChangeNotifier {
_lastSync = DateTime.now(); _lastSync = DateTime.now();
_syncInProgress = false; _syncInProgress = false;
//storage.setItem('lastSync', _lastSync.toIso8601String());
await save();
notifyListeners(); notifyListeners();
} }
@ -167,13 +162,23 @@ class GitHubAdapter extends ChangeNotifier {
_lastSync = DateTime.now(); _lastSync = DateTime.now();
_syncInProgress = false; _syncInProgress = false;
await save();
notifyListeners(); notifyListeners();
} }
String getPlayerNameFromFilePath(String filePath) { String getPlayerNameFromFilePath(String filePath) {
// We try to get the name of the player from the name of the folder // We try to get the name of the player from the name of the folder
// in which the file resists // in which the file resists
List<String> pathParts = filePath.substring(path.length + 1).split('/'); String cleanedPath = path;
if (path.startsWith('/')) {
cleanedPath = cleanedPath.substring(1);
}
if (cleanedPath.endsWith('/')) {
cleanedPath = cleanedPath.substring(0, cleanedPath.length - 1);
}
List<String> pathParts =
filePath.substring(cleanedPath.length + 1).split('/');
String playerName = 'Lonely Recluse'; String playerName = 'Lonely Recluse';
if (pathParts.length >= 2) { if (pathParts.length >= 2) {
@ -248,7 +253,28 @@ class GitHubAdapter extends ChangeNotifier {
return null; return null;
} }
void readWarband(WarbandRoster roster, String yamlContent) { Future<void> save() async {
// TODO: Read the warband from the shared preferences await storage.clear();
await storage.setItem('lastSync', _lastSync.toIso8601String());
await storage.setItem('rosters', _rosters);
await storage.setItem('activeRosterFilePath', _activeRosterFilePath);
}
Future<void> load() async {
await storage.ready;
String lastSync = storage.getItem('lastSync');
if (lastSync != null) {
_lastSync = DateTime.parse(lastSync);
}
_activeRosterFilePath = storage.getItem('activeRosterFilePath');
List<dynamic> rosters = storage.getItem('rosters');
if (rosters != null) {
rosters.forEach((warband) {
_rosters.add(WarbandRoster.fromJson(warband));
});
}
} }
} }

View file

@ -15,6 +15,10 @@ abstract class Unit {
return list.split(new RegExp(r" *, *")); return list.split(new RegExp(r" *, *"));
} }
static String _joinListToJson(List<String> list) {
return list.join(', ');
}
static Stats _statsFromJson(String stats) { static Stats _statsFromJson(String stats) {
RegExp re = new RegExp( RegExp re = new RegExp(
r"\s*M([0-9]+[dD]*[6]*)\s*,\s*WS([0-9]+)\s*,\s*BS([0-9]+)\s*,\s*S([0-9]+)\s*,\s*T([0-9]+)\s*,\s*W([0-9]+)\s*,\s*I([0-9]+)\s*,\s*A([0-9]+)\s*,\s*Ld([0-9]+)\s*,\s*Sv([0-9\-]+)\s*"); r"\s*M([0-9]+[dD]*[6]*)\s*,\s*WS([0-9]+)\s*,\s*BS([0-9]+)\s*,\s*S([0-9]+)\s*,\s*T([0-9]+)\s*,\s*W([0-9]+)\s*,\s*I([0-9]+)\s*,\s*A([0-9]+)\s*,\s*Ld([0-9]+)\s*,\s*Sv([0-9\-]+)\s*");
@ -38,11 +42,19 @@ abstract class Unit {
int.tryParse(matches.group(9)) ?? 0, int.tryParse(matches.group(9)) ?? 0,
int.tryParse(matches.group(10)) ?? 0); int.tryParse(matches.group(10)) ?? 0);
} }
static String _statsToJson(Stats stats) {
return 'M${stats.movement}, WS${stats.weaponSkill}, BS${stats.ballisticSkill}, S${stats.strength}, T${stats.toughtness}, W${stats.wounds}, I${stats.initiative}, A${stats.attacks}, Ld${stats.leadership}, Sv${stats.save}';
}
} }
@JsonSerializable(nullable: true, anyMap: true, createToJson: false) @JsonSerializable(nullable: true, anyMap: true)
class HenchmenGroup extends Unit { class HenchmenGroup extends Unit {
@JsonKey(name: 'group', fromJson: _henchmenHeaderFromJson, required: true) @JsonKey(
name: 'group',
fromJson: _henchmenHeaderFromJson,
toJson: _henchmengroupHeaderToJson,
required: true)
final HashMap<String, String> header; final HashMap<String, String> header;
@JsonKey(ignore: true) @JsonKey(ignore: true)
String name; String name;
@ -53,13 +65,14 @@ class HenchmenGroup extends Unit {
@JsonKey(ignore: true) @JsonKey(ignore: true)
int experience; int experience;
@JsonKey(fromJson: Unit._statsFromJson, required: true) @JsonKey(
fromJson: Unit._statsFromJson, toJson: Unit._statsToJson, required: true)
final Stats stats; final Stats stats;
@JsonKey(fromJson: Unit._splitListFromJson) @JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
final List<String> weapons; final List<String> weapons;
@JsonKey(fromJson: Unit._splitListFromJson) @JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
final List<String> armour; final List<String> armour;
HenchmenGroup(this.header, this.stats, this.weapons, this.armour) { HenchmenGroup(this.header, this.stats, this.weapons, this.armour) {
@ -91,12 +104,22 @@ class HenchmenGroup extends Unit {
return h; return h;
} }
static String _henchmengroupHeaderToJson(
HashMap<String, String> henchmenGroup) {
return '${henchmenGroup['name']} (${henchmenGroup['number']} ${henchmenGroup['type']}) [${henchmenGroup['experience']}XP]';
}
factory HenchmenGroup.fromJson(yaml) => _$HenchmenGroupFromJson(yaml); factory HenchmenGroup.fromJson(yaml) => _$HenchmenGroupFromJson(yaml);
Map<String, dynamic> toJson() => _$HenchmenGroupToJson(this);
} }
@JsonSerializable(nullable: true, anyMap: true, createToJson: false) @JsonSerializable(nullable: true, anyMap: true)
class Hero extends Unit { class Hero extends Unit {
@JsonKey(name: 'hero', fromJson: _heroHeaderFromJson, required: true) @JsonKey(
name: 'hero',
fromJson: _heroHeaderFromJson,
toJson: _heroHeaderToJson,
required: true)
final HashMap<String, String> header; final HashMap<String, String> header;
@JsonKey(ignore: true) @JsonKey(ignore: true)
String name; String name;
@ -105,19 +128,20 @@ class Hero extends Unit {
@JsonKey(ignore: true) @JsonKey(ignore: true)
int experience; int experience;
@JsonKey(fromJson: Unit._statsFromJson, required: true) @JsonKey(
fromJson: Unit._statsFromJson, toJson: Unit._statsToJson, required: true)
final Stats stats; final Stats stats;
@JsonKey(fromJson: Unit._splitListFromJson) @JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
final List<String> skilllists; final List<String> skilllists;
@JsonKey(fromJson: Unit._splitListFromJson) @JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
final List<String> weapons; final List<String> weapons;
@JsonKey(fromJson: Unit._splitListFromJson) @JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
final List<String> armour; final List<String> armour;
@JsonKey(fromJson: Unit._splitListFromJson) @JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
final List<String> rules; final List<String> rules;
@JsonKey(defaultValue: 0) @JsonKey(defaultValue: 0)
@ -134,6 +158,7 @@ class Hero extends Unit {
} }
factory Hero.fromJson(yaml) => _$HeroFromJson(yaml); factory Hero.fromJson(yaml) => _$HeroFromJson(yaml);
Map<String, dynamic> toJson() => _$HeroToJson(this);
static HashMap<String, String> _heroHeaderFromJson(String header) { static HashMap<String, String> _heroHeaderFromJson(String header) {
HashMap<String, String> h = new HashMap(); HashMap<String, String> h = new HashMap();
@ -153,6 +178,10 @@ class Hero extends Unit {
return h; return h;
} }
static String _heroHeaderToJson(HashMap<String, String> heroHeader) {
return '${heroHeader['name']} (${heroHeader['type']}) [${heroHeader['experience']}XP]';
}
} }
@immutable @immutable
@ -189,13 +218,30 @@ class Version {
String message; String message;
Version(this.gitHash, this.date, this.author, this.message); Version(this.gitHash, this.date, this.author, this.message);
factory Version.fromJson(yaml) {
return Version(
yaml['gitHash'], yaml['date'], yaml['author'], yaml['message']);
}
Map<String, dynamic> toJson() {
return {
'gitHash': gitHash,
'date': date,
'author': author,
'message': message
};
}
} }
@JsonSerializable(nullable: true, anyMap: true, createToJson: false) @JsonSerializable(nullable: true, anyMap: true)
class WarbandRoster { class WarbandRoster {
/// Store the complete string of name and race. This will split up into the /// Store the complete string of name and race. This will split up into the
/// fields name and race. /// fields name and race.
@JsonKey(name: 'warband', fromJson: _warbandNameAndRace, required: true) @JsonKey(
name: 'warband',
fromJson: _warbandNameAndRaceFromJson,
toJson: _warbandNameAndRaceToJson,
required: true)
final HashMap<String, String> nameAndRace; final HashMap<String, String> nameAndRace;
@JsonKey(ignore: true) @JsonKey(ignore: true)
String name; String name;
@ -234,17 +280,17 @@ class WarbandRoster {
/// The players name is not defined in the yml file. This will be added later /// The players name is not defined in the yml file. This will be added later
/// from the GitHubAdapter. Same goes for the lastSyncVersion and currentVersion. /// from the GitHubAdapter. Same goes for the lastSyncVersion and currentVersion.
@JsonKey(ignore: true) @JsonKey(required: false, defaultValue: 'Lonely Recluse')
String playerName = 'Lonely Recluse'; String playerName;
@JsonKey(ignore: true) @JsonKey(required: false)
String filePath; String filePath;
@JsonKey(ignore: true) @JsonKey(required: false)
Version version; Version version;
@JsonKey(ignore: true) @JsonKey(required: false, defaultValue: true)
bool unseen = true; bool unseen;
WarbandRoster( WarbandRoster(
this.nameAndRace, this.nameAndRace,
@ -256,7 +302,11 @@ class WarbandRoster {
this.equipment, this.equipment,
this.achievments, this.achievments,
this.heros, this.heros,
this.henchmenGroups) { this.henchmenGroups,
this.playerName,
this.filePath,
this.version,
this.unseen) {
this.name = this.nameAndRace['name']; this.name = this.nameAndRace['name'];
this.race = this.nameAndRace['race']; this.race = this.nameAndRace['race'];
} }
@ -266,7 +316,8 @@ class WarbandRoster {
return 1337; return 1337;
} }
static HashMap<String, String> _warbandNameAndRace(String nameAndRace) { static HashMap<String, String> _warbandNameAndRaceFromJson(
String nameAndRace) {
HashMap<String, String> nr = new HashMap(); HashMap<String, String> nr = new HashMap();
RegExp re = new RegExp(r"(.*) \((.*)\)"); RegExp re = new RegExp(r"(.*) \((.*)\)");
@ -284,5 +335,10 @@ class WarbandRoster {
return nr; return nr;
} }
static String _warbandNameAndRaceToJson(HashMap<String, String> nameAndRace) {
return '${nameAndRace['name']} (${nameAndRace['race']})';
}
factory WarbandRoster.fromJson(yaml) => _$WarbandRosterFromJson(yaml); factory WarbandRoster.fromJson(yaml) => _$WarbandRosterFromJson(yaml);
Map<String, dynamic> toJson() => _$WarbandRosterToJson(this);
} }

View file

@ -16,6 +16,14 @@ HenchmenGroup _$HenchmenGroupFromJson(Map json) {
); );
} }
Map<String, dynamic> _$HenchmenGroupToJson(HenchmenGroup instance) =>
<String, dynamic>{
'group': HenchmenGroup._henchmengroupHeaderToJson(instance.header),
'stats': Unit._statsToJson(instance.stats),
'weapons': Unit._joinListToJson(instance.weapons),
'armour': Unit._joinListToJson(instance.armour),
};
Hero _$HeroFromJson(Map json) { Hero _$HeroFromJson(Map json) {
$checkKeys(json, requiredKeys: const ['hero', 'stats']); $checkKeys(json, requiredKeys: const ['hero', 'stats']);
return Hero( return Hero(
@ -30,6 +38,17 @@ Hero _$HeroFromJson(Map json) {
); );
} }
Map<String, dynamic> _$HeroToJson(Hero instance) => <String, dynamic>{
'hero': Hero._heroHeaderToJson(instance.header),
'stats': Unit._statsToJson(instance.stats),
'skilllists': Unit._joinListToJson(instance.skilllists),
'weapons': Unit._joinListToJson(instance.weapons),
'armour': Unit._joinListToJson(instance.armour),
'rules': Unit._joinListToJson(instance.rules),
'warbandaddition': instance.warbandaddition,
'hiredsword': instance.hiredSword,
};
WarbandRoster _$WarbandRosterFromJson(Map json) { WarbandRoster _$WarbandRosterFromJson(Map json) {
$checkKeys(json, requiredKeys: const [ $checkKeys(json, requiredKeys: const [
'warband', 'warband',
@ -39,7 +58,7 @@ WarbandRoster _$WarbandRosterFromJson(Map json) {
'henchmen' 'henchmen'
]); ]);
return WarbandRoster( return WarbandRoster(
WarbandRoster._warbandNameAndRace(json['warband'] as String), WarbandRoster._warbandNameAndRaceFromJson(json['warband'] as String),
json['campaign'] as int ?? 0, json['campaign'] as int ?? 0,
json['objective'] as String, json['objective'] as String,
json['alignment'] as String, json['alignment'] as String,
@ -53,5 +72,28 @@ WarbandRoster _$WarbandRosterFromJson(Map json) {
(json['henchmen'] as List) (json['henchmen'] as List)
?.map((e) => e == null ? null : HenchmenGroup.fromJson(e)) ?.map((e) => e == null ? null : HenchmenGroup.fromJson(e))
?.toList(), ?.toList(),
json['playerName'] as String ?? 'Lonely Recluse',
json['filePath'] as String,
json['version'] == null ? null : Version.fromJson(json['version']),
json['unseen'] as bool ?? true,
)..active = json['active'] as bool ?? true; )..active = json['active'] as bool ?? true;
} }
Map<String, dynamic> _$WarbandRosterToJson(WarbandRoster instance) =>
<String, dynamic>{
'warband': WarbandRoster._warbandNameAndRaceToJson(instance.nameAndRace),
'active': instance.active,
'campaign': instance.campaignPoints,
'objective': instance.objective,
'alignment': instance.alignment,
'achievments': instance.achievments,
'gc': instance.gc,
'shards': instance.shards,
'equipment': instance.equipment,
'heros': instance.heros,
'henchmen': instance.henchmenGroups,
'playerName': instance.playerName,
'filePath': instance.filePath,
'version': instance.version,
'unseen': instance.unseen,
};

View file

@ -17,6 +17,7 @@ class Toolheim extends StatelessWidget {
return ChangeNotifierProvider( return ChangeNotifierProvider(
builder: (context) => GitHubAdapter(), builder: (context) => GitHubAdapter(),
child: MaterialApp( child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Toolheim', title: 'Toolheim',
theme: ThemeData( theme: ThemeData(
primarySwatch: Colors.brown, primarySwatch: Colors.brown,

View file

@ -8,16 +8,23 @@ class SettingsScreen extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
GitHubAdapter github = Provider.of<GitHubAdapter>(context); GitHubAdapter github = Provider.of<GitHubAdapter>(context);
//String _repository = 'Labernator/Mordheim';
//String _path = 'Mordheim-BorderTownBurning/Warband Rosters';
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Settings')), appBar: AppBar(title: Text('Settings')),
body: PreferencePage([ body: PreferencePage([
PreferenceTitle('GitHub'), PreferenceTitle('GitHub'),
PreferenceText( 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.'), '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'), TextFieldPreference(
'Repository',
'repository',
defaultVal: 'f0086/toolheim-example',
),
PreferenceText( PreferenceText(
'If your warband folders are placed in a subfolder, you can specify it here.'), 'If your warband folders are placed in a subfolder, you can specify it here.'),
TextFieldPreference('Path', 'path', defaultVal: '/'), TextFieldPreference('Path', 'path', defaultVal: '/players'),
PreferenceTitle('Search for Warbands'), PreferenceTitle('Search for Warbands'),
PreferenceText( PreferenceText(
'Search the given GitHub repository for valid Warband files (ending with .warband.yml). This step can be done at any time.'), 'Search the given GitHub repository for valid Warband files (ending with .warband.yml). This step can be done at any time.'),
@ -34,9 +41,6 @@ class SettingsScreen extends StatelessWidget {
child: child:
Text('Start search', style: TextStyle(color: Colors.blue))), Text('Start search', style: TextStyle(color: Colors.blue))),
])); ]));
//String _repository = 'Labernator/Mordheim';
//String _path = 'Mordheim-BorderTownBurning/Warband Rosters';
} }
static Widget buildSyncErrors(BuildContext context) { static Widget buildSyncErrors(BuildContext context) {

View file

@ -89,6 +89,7 @@ class WarbandDrawerWidget extends StatelessWidget {
onTap: () { onTap: () {
roster.unseen = false; roster.unseen = false;
github.activeRoster = roster; github.activeRoster = roster;
github.save();
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
title: Text(roster.name + ' (' + roster.playerName + ')', title: Text(roster.name + ' (' + roster.playerName + ')',