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 {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.toolheim"
applicationId "net.aaronfischer.toolheim"
minSdkVersion 16
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()

View file

@ -1,5 +1,5 @@
<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
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"
package="com.example.toolheim">
package="net.aaronfischer.toolheim">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
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 io.flutter.app.FlutterActivity;

View file

@ -1,7 +1,9 @@
<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
to allow setting breakpoints, to provide hot reload, etc.
-->
<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>

View file

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

View file

@ -45,6 +45,10 @@ class GitHubAdapter extends ChangeNotifier {
notifyListeners();
}
GitHubAdapter() {
load();
}
/// Search for warband files in the GitHub repository
///
/// 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();
_rosters.clear();
_syncInProgress = true;
@ -110,14 +113,6 @@ class GitHubAdapter extends ChangeNotifier {
_rosters.add(roster);
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
@ -125,8 +120,8 @@ class GitHubAdapter extends ChangeNotifier {
_lastSync = DateTime.now();
_syncInProgress = false;
//storage.setItem('lastSync', _lastSync.toIso8601String());
await save();
notifyListeners();
}
@ -167,13 +162,23 @@ class GitHubAdapter extends ChangeNotifier {
_lastSync = DateTime.now();
_syncInProgress = false;
await save();
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<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';
if (pathParts.length >= 2) {
@ -248,7 +253,28 @@ class GitHubAdapter extends ChangeNotifier {
return null;
}
void readWarband(WarbandRoster roster, String yamlContent) {
// TODO: Read the warband from the shared preferences
Future<void> save() async {
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" *, *"));
}
static String _joinListToJson(List<String> list) {
return list.join(', ');
}
static Stats _statsFromJson(String stats) {
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*");
@ -38,11 +42,19 @@ abstract class Unit {
int.tryParse(matches.group(9)) ?? 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 {
@JsonKey(name: 'group', fromJson: _henchmenHeaderFromJson, required: true)
@JsonKey(
name: 'group',
fromJson: _henchmenHeaderFromJson,
toJson: _henchmengroupHeaderToJson,
required: true)
final HashMap<String, String> header;
@JsonKey(ignore: true)
String name;
@ -53,13 +65,14 @@ class HenchmenGroup extends Unit {
@JsonKey(ignore: true)
int experience;
@JsonKey(fromJson: Unit._statsFromJson, required: true)
@JsonKey(
fromJson: Unit._statsFromJson, toJson: Unit._statsToJson, required: true)
final Stats stats;
@JsonKey(fromJson: Unit._splitListFromJson)
@JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
final List<String> weapons;
@JsonKey(fromJson: Unit._splitListFromJson)
@JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
final List<String> armour;
HenchmenGroup(this.header, this.stats, this.weapons, this.armour) {
@ -91,12 +104,22 @@ class HenchmenGroup extends Unit {
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);
Map<String, dynamic> toJson() => _$HenchmenGroupToJson(this);
}
@JsonSerializable(nullable: true, anyMap: true, createToJson: false)
@JsonSerializable(nullable: true, anyMap: true)
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;
@JsonKey(ignore: true)
String name;
@ -105,19 +128,20 @@ class Hero extends Unit {
@JsonKey(ignore: true)
int experience;
@JsonKey(fromJson: Unit._statsFromJson, required: true)
@JsonKey(
fromJson: Unit._statsFromJson, toJson: Unit._statsToJson, required: true)
final Stats stats;
@JsonKey(fromJson: Unit._splitListFromJson)
@JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
final List<String> skilllists;
@JsonKey(fromJson: Unit._splitListFromJson)
@JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
final List<String> weapons;
@JsonKey(fromJson: Unit._splitListFromJson)
@JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
final List<String> armour;
@JsonKey(fromJson: Unit._splitListFromJson)
@JsonKey(fromJson: Unit._splitListFromJson, toJson: Unit._joinListToJson)
final List<String> rules;
@JsonKey(defaultValue: 0)
@ -134,6 +158,7 @@ class Hero extends Unit {
}
factory Hero.fromJson(yaml) => _$HeroFromJson(yaml);
Map<String, dynamic> toJson() => _$HeroToJson(this);
static HashMap<String, String> _heroHeaderFromJson(String header) {
HashMap<String, String> h = new HashMap();
@ -153,6 +178,10 @@ class Hero extends Unit {
return h;
}
static String _heroHeaderToJson(HashMap<String, String> heroHeader) {
return '${heroHeader['name']} (${heroHeader['type']}) [${heroHeader['experience']}XP]';
}
}
@immutable
@ -189,13 +218,30 @@ class Version {
String 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 {
/// Store the complete string of name and race. This will split up into the
/// 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;
@JsonKey(ignore: true)
String name;
@ -234,17 +280,17 @@ class WarbandRoster {
/// 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.
@JsonKey(ignore: true)
String playerName = 'Lonely Recluse';
@JsonKey(required: false, defaultValue: 'Lonely Recluse')
String playerName;
@JsonKey(ignore: true)
@JsonKey(required: false)
String filePath;
@JsonKey(ignore: true)
@JsonKey(required: false)
Version version;
@JsonKey(ignore: true)
bool unseen = true;
@JsonKey(required: false, defaultValue: true)
bool unseen;
WarbandRoster(
this.nameAndRace,
@ -256,7 +302,11 @@ class WarbandRoster {
this.equipment,
this.achievments,
this.heros,
this.henchmenGroups) {
this.henchmenGroups,
this.playerName,
this.filePath,
this.version,
this.unseen) {
this.name = this.nameAndRace['name'];
this.race = this.nameAndRace['race'];
}
@ -266,7 +316,8 @@ class WarbandRoster {
return 1337;
}
static HashMap<String, String> _warbandNameAndRace(String nameAndRace) {
static HashMap<String, String> _warbandNameAndRaceFromJson(
String nameAndRace) {
HashMap<String, String> nr = new HashMap();
RegExp re = new RegExp(r"(.*) \((.*)\)");
@ -284,5 +335,10 @@ class WarbandRoster {
return nr;
}
static String _warbandNameAndRaceToJson(HashMap<String, String> nameAndRace) {
return '${nameAndRace['name']} (${nameAndRace['race']})';
}
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) {
$checkKeys(json, requiredKeys: const ['hero', 'stats']);
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) {
$checkKeys(json, requiredKeys: const [
'warband',
@ -39,7 +58,7 @@ WarbandRoster _$WarbandRosterFromJson(Map json) {
'henchmen'
]);
return WarbandRoster(
WarbandRoster._warbandNameAndRace(json['warband'] as String),
WarbandRoster._warbandNameAndRaceFromJson(json['warband'] as String),
json['campaign'] as int ?? 0,
json['objective'] as String,
json['alignment'] as String,
@ -53,5 +72,28 @@ WarbandRoster _$WarbandRosterFromJson(Map json) {
(json['henchmen'] as List)
?.map((e) => e == null ? null : HenchmenGroup.fromJson(e))
?.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;
}
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(
builder: (context) => GitHubAdapter(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Toolheim',
theme: ThemeData(
primarySwatch: Colors.brown,

View file

@ -8,16 +8,23 @@ class SettingsScreen extends StatelessWidget {
Widget build(BuildContext context) {
GitHubAdapter github = Provider.of<GitHubAdapter>(context);
//String _repository = 'Labernator/Mordheim';
//String _path = 'Mordheim-BorderTownBurning/Warband Rosters';
return Scaffold(
appBar: AppBar(title: Text('Settings')),
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'),
TextFieldPreference(
'Repository',
'repository',
defaultVal: 'f0086/toolheim-example',
),
PreferenceText(
'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'),
PreferenceText(
'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:
Text('Start search', style: TextStyle(color: Colors.blue))),
]));
//String _repository = 'Labernator/Mordheim';
//String _path = 'Mordheim-BorderTownBurning/Warband Rosters';
}
static Widget buildSyncErrors(BuildContext context) {

View file

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