Skip to content

Commit 06bc79c

Browse files
committed
Add importing and exporting accounts
1 parent 501708e commit 06bc79c

File tree

8 files changed

+189
-24
lines changed

8 files changed

+189
-24
lines changed

api/lib/src/models/data.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const kPackTranslationsPath = 'translations';
2222
const kPackBackgroundsPath = 'backgrounds';
2323
const kPackScriptsPath = 'scripts';
2424
const kPackModesPath = 'modes';
25+
const kPackAccountsPath = 'accounts';
2526

2627
const kGameTablePath = 'tables';
2728
const kGameTeamPath = 'teams.json';
@@ -312,6 +313,32 @@ class SetonixData extends ArchiveData<SetonixData> {
312313
if (mode == null) return null;
313314
return MapEntry(e, mode);
314315
}).nonNulls);
316+
317+
SetonixData addAccount(SetonixAccount setonixAccount) {
318+
final accountId = setonixAccount.name;
319+
return setAsset(
320+
'$kPackAccountsPath/$accountId.key', setonixAccount.privateKey)
321+
.setAsset(
322+
'$kPackAccountsPath/$accountId.pub', setonixAccount.publicKey);
323+
}
324+
325+
Iterable<SetonixAccount> getAccounts() sync* {
326+
const kKeySuffix = '.key';
327+
final privateKeys = getAssets('$kPackAccountsPath/', true)
328+
.where((e) => e.endsWith(kKeySuffix));
329+
for (final path in privateKeys) {
330+
final name = path.substring(0, path.length - kKeySuffix.length);
331+
final privateKey = getAsset(path);
332+
if (privateKey == null) continue;
333+
final publicKey = getAsset('$kPackAccountsPath/$name.pub');
334+
if (publicKey == null) continue;
335+
yield SetonixAccount(
336+
privateKey: privateKey,
337+
publicKey: publicKey,
338+
name: name,
339+
);
340+
}
341+
}
315342
}
316343

317344
class SetonixFile {

api/lib/src/models/meta.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:typed_data';
2+
13
import 'package:dart_mappable/dart_mappable.dart';
24

35
part 'meta.mapper.dart';
@@ -9,6 +11,7 @@ enum FileType {
911
pack,
1012
game,
1113
template,
14+
accounts,
1215
}
1316

1417
@MappableClass()
@@ -65,3 +68,15 @@ final class DataMetadata with DataMetadataMappable {
6568
return serversLastUsed.values.fold(addedAt, (a, b) => a.isAfter(b) ? a : b);
6669
}
6770
}
71+
72+
final class SetonixAccount {
73+
final Uint8List privateKey;
74+
final Uint8List publicKey;
75+
final String name;
76+
77+
SetonixAccount({
78+
required this.privateKey,
79+
required this.publicKey,
80+
required this.name,
81+
});
82+
}

api/lib/src/models/meta.mapper.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class FileTypeMapper extends EnumMapper<FileType> {
3131
return FileType.game;
3232
case r'template':
3333
return FileType.template;
34+
case r'accounts':
35+
return FileType.accounts;
3436
default:
3537
throw MapperException.unknownEnumValue(value);
3638
}
@@ -45,6 +47,8 @@ class FileTypeMapper extends EnumMapper<FileType> {
4547
return r'game';
4648
case FileType.template:
4749
return r'template';
50+
case FileType.accounts:
51+
return r'accounts';
4852
}
4953
}
5054
}

app/lib/api/open.dart

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,29 +93,31 @@ Future<void> importFileData(BuildContext context, SetonixFileSystem fileSystem,
9393
final data = file.load();
9494
final metadata = data.getMetadataOrDefault();
9595
final type = metadata.type;
96+
final loc = AppLocalizations.of(context);
9697
final result = await showDialog<bool>(
9798
context: context,
9899
builder: (context) => AlertDialog(
99100
title: Text(switch (type) {
100-
FileType.pack => AppLocalizations.of(context).importPack,
101-
FileType.game => AppLocalizations.of(context).importGame,
102-
FileType.template => AppLocalizations.of(context).importTemplate,
101+
FileType.pack => loc.importPack,
102+
FileType.game => loc.importGame,
103+
FileType.template => loc.importTemplate,
104+
FileType.accounts => loc.importAccounts,
103105
}),
104106
content: Text(switch (type) {
105-
FileType.pack => AppLocalizations.of(context).importPackDescription,
106-
FileType.game => AppLocalizations.of(context).importGameDescription,
107-
FileType.template =>
108-
AppLocalizations.of(context).importTemplateDescription,
107+
FileType.pack => loc.importPackDescription,
108+
FileType.game => loc.importGameDescription,
109+
FileType.template => loc.importTemplateDescription,
110+
FileType.accounts => loc.importAccountsDescription,
109111
}),
110112
actions: [
111113
TextButton.icon(
112114
onPressed: () => Navigator.of(context).pop(false),
113-
label: Text(AppLocalizations.of(context).cancel),
115+
label: Text(loc.cancel),
114116
icon: Icon(PhosphorIconsLight.prohibit),
115117
),
116118
ElevatedButton.icon(
117119
onPressed: () => Navigator.of(context).pop(true),
118-
label: Text(AppLocalizations.of(context).import),
120+
label: Text(loc.import),
119121
icon: Icon(PhosphorIconsLight.boxArrowDown),
120122
),
121123
],
@@ -130,5 +132,7 @@ Future<void> importFileData(BuildContext context, SetonixFileSystem fileSystem,
130132
await fileSystem.templateSystem.createFile(metadata.name, data);
131133
case FileType.game:
132134
await fileSystem.worldSystem.createFile(metadata.name, data);
135+
case FileType.accounts:
136+
await fileSystem.importAccountsFromData(data);
133137
}
134138
}

app/lib/l10n/app_en.arb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,5 +252,9 @@
252252
"comfortable": "Comfortable",
253253
"stackedCards": "Stacked cards",
254254
"accounts": "Accounts",
255-
"create": "Create"
255+
"create": "Create",
256+
"backupKey": "Backup key",
257+
"importAccounts": "Import accounts",
258+
"importAccountsDescription": "Are you sure you want to import the accounts?",
259+
"backupAllKeys": "Backup all keys"
256260
}

app/lib/main.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import 'package:flutter/foundation.dart';
44
import 'package:flutter/material.dart';
55
import 'package:flutter_bloc/flutter_bloc.dart';
66
import 'package:go_router/go_router.dart';
7+
import 'package:setonix/pages/settings/data.dart';
8+
import 'package:setonix/pages/settings/general.dart';
79
import 'package:setonix/pages/settings/input.dart';
10+
import 'package:setonix/pages/settings/personalization.dart';
811
import 'package:setonix/src/generated/i18n/app_localizations.dart';
912
import 'package:flutter_web_plugins/url_strategy.dart';
1013
import 'package:material_leap/material_leap.dart';

app/lib/pages/settings/accounts.dart

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_bloc/flutter_bloc.dart';
33
import 'package:lw_file_system/lw_file_system.dart';
4+
import 'package:setonix/api/open.dart';
5+
import 'package:setonix/api/save.dart';
46
import 'package:setonix/src/generated/i18n/app_localizations.dart';
57
import 'package:material_leap/material_leap.dart';
68
import 'package:phosphor_flutter/phosphor_flutter.dart';
@@ -42,6 +44,30 @@ class _AccountsSettingsPageState extends State<AccountsSettingsPage> {
4244
inView: widget.inView,
4345
backgroundColor: widget.inView ? Colors.transparent : null,
4446
title: Text(AppLocalizations.of(context).accounts),
47+
actions: [
48+
IconButton(
49+
icon: const PhosphorIcon(PhosphorIconsLight.arrowSquareIn),
50+
tooltip: AppLocalizations.of(context).import,
51+
onPressed: () async {
52+
await importFile(
53+
context,
54+
_fileSystem,
55+
);
56+
setState(() {
57+
_buildKeysFuture();
58+
});
59+
},
60+
),
61+
IconButton(
62+
icon: const PhosphorIcon(PhosphorIconsLight.export),
63+
tooltip: AppLocalizations.of(context).backupAllKeys,
64+
onPressed: () async {
65+
final data = await _fileSystem.exportAccounts();
66+
if (!context.mounted) return;
67+
exportData(context, data, 'accounts');
68+
},
69+
),
70+
],
4571
),
4672
body: FutureBuilder<List<String>>(
4773
future: _keysFuture,
@@ -51,18 +77,40 @@ class _AccountsSettingsPageState extends State<AccountsSettingsPage> {
5177
itemCount: keys.length,
5278
itemBuilder: (context, index) {
5379
final key = keys[index];
80+
void deleteKey() {
81+
_privateKeyFileSystem.deleteFile(key);
82+
_publicKeyFileSystem.deleteFile(key);
83+
setState(() {
84+
keys.removeAt(index);
85+
});
86+
}
87+
5488
return Dismissible(
5589
key: Key(key),
56-
child: ListTile(
57-
title: Text(key.substring(1)),
90+
child: ContextRegion(
91+
builder: (context, button, controller) => ListTile(
92+
title: Text(key.substring(1)),
93+
trailing: button,
94+
),
95+
menuChildren: [
96+
MenuItemButton(
97+
leadingIcon:
98+
const PhosphorIcon(PhosphorIconsLight.export),
99+
onPressed: () async {
100+
final data = await _fileSystem.exportAccounts([key]);
101+
if (!context.mounted) return;
102+
exportData(context, data, key);
103+
},
104+
child: Text(AppLocalizations.of(context).backupKey),
105+
),
106+
MenuItemButton(
107+
leadingIcon: const PhosphorIcon(PhosphorIconsLight.trash),
108+
onPressed: deleteKey,
109+
child: Text(AppLocalizations.of(context).delete),
110+
),
111+
],
58112
),
59-
onDismissed: (direction) {
60-
_privateKeyFileSystem.deleteFile(key);
61-
_publicKeyFileSystem.deleteFile(key);
62-
setState(() {
63-
keys.removeAt(index);
64-
});
65-
},
113+
onDismissed: (direction) => deleteKey(),
66114
);
67115
},
68116
);

app/lib/services/file_system.dart

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class SetonixFileSystem {
4343
event.database.createObjectStore('packs-data');
4444
}
4545
if (event.oldVersion < 3) {
46-
event.database.createObjectStore('keys');
46+
event.database.createObjectStore('accounts');
4747
}
4848
}
4949

@@ -125,9 +125,9 @@ class SetonixFileSystem {
125125
privateKeySystem = KeyFileSystem.fromPlatform(
126126
FileSystemConfig(
127127
passwordStorage: SecureStoragePasswordStorage(),
128-
storeName: 'keys',
128+
storeName: 'accounts',
129129
getDirectory: (storage) async =>
130-
'${await getSetonixDirectory()}/Keys',
130+
'${await getSetonixDirectory()}/Accounts',
131131
database: 'setonix.db',
132132
databaseVersion: kDatabaseVersion,
133133
keySuffix: '.key',
@@ -137,9 +137,9 @@ class SetonixFileSystem {
137137
publicKeySystem = KeyFileSystem.fromPlatform(
138138
FileSystemConfig(
139139
passwordStorage: SecureStoragePasswordStorage(),
140-
storeName: 'keys',
140+
storeName: 'accounts',
141141
getDirectory: (storage) async =>
142-
'${await getSetonixDirectory()}/Keys',
142+
'${await getSetonixDirectory()}/Accounts',
143143
database: 'setonix.db',
144144
databaseVersion: kDatabaseVersion,
145145
keySuffix: '.pub',
@@ -237,4 +237,64 @@ class SetonixFileSystem {
237237
await publicKeySystem
238238
.createFileWithName(Uint8List.fromList(publicKey.bytes), name: name);
239239
}
240+
241+
Future<SetonixAccount?> getAccount(String name) async {
242+
final privateKey = await privateKeySystem.getFile(name);
243+
if (privateKey == null) return null;
244+
final publicKey = await publicKeySystem.getFile(name);
245+
if (publicKey == null) return null;
246+
return SetonixAccount(
247+
privateKey: privateKey,
248+
publicKey: publicKey,
249+
name: name,
250+
);
251+
}
252+
253+
Future<void> deleteAccount(String name) async {
254+
await privateKeySystem.deleteFile(name);
255+
await publicKeySystem.deleteFile(name);
256+
}
257+
258+
Future<void> importAccountsFromData(SetonixData data) =>
259+
importAccounts(data.getAccounts().toList());
260+
261+
Future<void> importAccounts(List<SetonixAccount> accounts) async {
262+
for (final account in accounts) {
263+
final name = await privateKeySystem.createFileWithName(
264+
account.privateKey,
265+
name: account.name,
266+
);
267+
await publicKeySystem.updateFile(
268+
name,
269+
account.publicKey,
270+
);
271+
}
272+
}
273+
274+
Future<SetonixData> exportAccounts(
275+
[List<String>? names, List<SetonixAccount>? accounts]) async {
276+
var data = SetonixData.empty().setMetadata(FileMetadata(
277+
type: FileType.accounts,
278+
));
279+
names ??= await privateKeySystem.getKeys();
280+
final allAccounts = accounts ??
281+
(await Future.wait(
282+
names.map((name) => getAccount(name)),
283+
))
284+
.whereType<SetonixAccount>()
285+
.toList();
286+
for (final account in allAccounts) {
287+
final privateKey = account.privateKey;
288+
final publicKey = account.publicKey;
289+
if (privateKey.isEmpty || publicKey.isEmpty) continue;
290+
data = data.addAccount(
291+
SetonixAccount(
292+
privateKey: privateKey,
293+
publicKey: publicKey,
294+
name: account.name,
295+
),
296+
);
297+
}
298+
return data;
299+
}
240300
}

0 commit comments

Comments
 (0)